Author Topic: Resolving contention - create a unique/random sequence from a 96bit ID? use CRC?  (Read 3879 times)

0 Members and 1 Guest are viewing this topic.

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
I've got a dozens of MCUs that don't have a hardware random number generator.

These dozens of MCUs operate power supplies that all "fight" for their share of power from a current limited supply by monitoring current and voltage. (Perhaps a bad idea, but this post is about resolving that)

Each MCU has a unique 96-bit ID. They cannot communicate with one another.

I'm curious what are some "good" ways to use the unique ID to generate a random and completely unique sequences of numbers?


In my specific application, I might have each power supply have a random restart/startup/soft-start delay + random brown out reset voltage somehow based on the unique ID (and if the power rail voltage went too low, some of the power supplies would cut out first, and would restart more slowly than others). The goal is to make each unit behave differently at different times. Maybe ADC noise might help to make things random.

I feel like this problem has been solved before.

I do have a CRC generator. Maybe a running CRC each millisecond based on the unique ID, XORed with some ADC noise? (the 4-least significant bits of all the ADC channels XORed together overtop of different portions of the ID)
« Last Edit: January 29, 2025, 11:24:21 pm by incf »
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4382
  • Country: us
Re: Resolving contention - create a unique/random sequence from a 96-bit ID?
« Reply #1 on: January 29, 2025, 11:01:26 pm »
Quote
what are some "good" ways to use the unique ID to generate a random and completely unique sequences of numbers?
In theory, you can just use your unique ID to seed a standard  Pseudo-Random Number Generator.

The usual problems are that the "unique IDs" are not the same size as most PRNGs, so you can't use the whole ID, and the IDs are NOT "random", but instead include things like wafer number and die position and such (so picking a subset of the ID for the RNG seed is "fraught".)

You can probably run the ID through some sort of hash function that reduces the size (a CRC would be one choice; various crypto algorithms another.  But you'd need to do some experimenting to see what sort of collision rate you get, especially after whatever your code does to reduce the PRNG to a manageble size.  ("x" having few collisions doesn't necessarily mean that "x mod 100" has few collisions.)
Or you could just look for a 96bit PRNG (though it might have the same "reduction problem."
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4382
  • Country: us
This doc contains LFSR tap info for RNGs up to 168 bits (!)
https://docs.amd.com/v/u/en-US/xapp052
 
The following users thanked this post: incf

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
This doc contains LFSR tap info for RNGs up to 168 bits (!)
https://docs.amd.com/v/u/en-US/xapp052
That document  "knocked my socks off" - I would have never thought to search for that from Xilinx/AMD. This seems a lot easier than pursuing CRCZoo looking for max length polynomials.

(While I don't know anything about LFSRs except for having read a story involving old gambling machines, I'm sure I can figure it out)
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 16126
  • Country: fr
Yes my first approach would also be to seed a decent PRNG with the unique ID.
If you can't customize the PRNG to use a 96-bit seed but don't want to truncate the UID arbitrarily, you can reduce the latter to say, 64 bits or 32 bits, with a *much* better result than truncating, using a Fletcher checksum on the UID.
This is exactly what I do personally to generate 32-bit UIDs for a communication protocol from 96-bit UIDs (coming from STM32 MCUs).

https://en.wikipedia.org/wiki/Fletcher%27s_checksum
 
The following users thanked this post: woofy

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
Apologies to those who have heard me describe this before, but:

Xorshift64* is a very simple LFSR pseudo-random number generator, whose upper 32-40 bits pass all known randomness tests, and is thus better than e.g. the industry standard, Mersenne Twister (MT19937).

It is not cryptographically secure, but it is random enough even for scientific simulations and numerical work; again, both faster and "more random" than the industry standard.  Just do not assume that the next number in a sequence cannot be predicted, as that would require a cryptographically secure PRNG.

It uses 64 bit state, and has a period of 2⁶⁴-1.  The only disallowed state is all zeros.  (No other state will ever lead to the all-zeros state, so all other seed states will give the full sequence of 2⁶⁴-1 numbers.)

The state is updated using three exclusive OR operations and binary shifts:
    state = state ^ (state >> 12);
    state = state ^ (state << 25);
    state = state ^ (state >> 27);
To generate the PRNG corresponding to the (old or new) state, it is multiplied by a 64-bit value 2685821657736338717, and bits 32..63 (or 24..63) are used:
    number = (uint64_t)(state * UINT64_C(2685821657736338717)) >> 32;

To use a 96-bit seed, you can use the first 32 bits and fixed (nonzero) lower 32 bits as the initial seed, then update the state roughly dozen times.  Then, exclusive-OR the state with the next 32 bits (combined with fixed low 32 bits to form a 64-bit value), and update the state some dozen times or so again.  Repeat that with the last 32 bits, too.  The result is suitable for use as a Xorshift64* PRNG state, yielding the full 2⁶⁴-1 sequence from that point forwards.

On a 32-bit architecture, each state update involves nine bit shifts (by 5, 20, or 25 bits left/up; or by 7, 12, or 27 bits right/down), six exclusive-OR operations, and three OR operations, using four 32-bit registers.  The multiplication is fast if you have a 32×32=64-bit or 32×32=high/low 32-bit operation, but slower if you only have a truncating 32×32=32-bit multiplication.  (On ARMv6-m/Cortex-M0/M0+ it is slow, but on ARMv7e-m/Cortex-M3/M4/M7 and better it is just four instructions: muls, mla, umull, and adds.)

Example C code:
Code: [Select]
#include <stdint.h>

uint32_t  prng_u32(uint64_t *state) {
    uint64_t  x = *state;
    x ^= x >> 12;
    x ^= x << 25;
    x ^= x >> 27;
    *state = x;
    return ((x * UINT64_C(2685821657736338717))) >> 32;
}

uint64_t  hash_96to64(const uint32_t seed[3], const uint32_t fixed[3], const uint8_t rounds[3]) {
    union {
        uint64_t  u64;
        uint32_t  u32[2];
    } state = { .u32 = { 0, 0 } };

    uint_fast8_t  w = 2;
    do {
        state.u32[0] = state.u32[0] ^ fixed[w];
        state.u32[1] = state.u32[1] ^ seed[w];

        uint_fast8_t  r = rounds[w];
        do {
            state.u64 ^= state.u64 >> 12;
            state.u64 ^= state.u64 << 25;
            state.u64 ^= state.u64 >> 27;
        } while (r-->0);
    } while (w-->0);

    return state.u64;
}
The hash_96to32() is a parametrized hash function, letting you tune each use case using the fixed and rounds parameters.  I've found that something like a dozen iterations suffices for a full avalanche effect (at least half the bits differing, whenever the seed value differs by one bit).  Each of the fixed values should be nonzero, to ensure the state update rounds have full effect; preferably with both set and clear bits in the least and most significant bits (instead of all-zeros or all-ones).

While I am not a mathematician, nor a cryptologist, a lot of stuff I do relies on "random" pseudorandom number sequences, and me understanding the details, so I've had to learn to work with the various methods.  Linear feedback shift registers are definitely the fastest.  As Xorshift and Xoroshiro variants show, they can be extremely random, too.  The two industry benchmarks for empirical randomness testing are the TestU01 framework's BigCrush test suite (which includes over a hundred individual tests), and the Diehard tests by Marsaglia et al. and its derivatives like Dieharder.  Using only 32 to 40 high bits of Xorshift64* is not the absolute best (least "weak" results in Dieharder while passing all BigCrush tests), nor the fastest, but it really has no "poor" seeds (except for zero, which is not allowed, and will not occur by itself during normal operation), while the better ones tend to suffer from some "poor" seed states generating less-random sequences so that such seeds need to be "avoided" or worked around.  The relative density of poor seeds is not a problem; it is really only the initial sequence that is impacted by "poor" seeds – but we tend to use those initial sequences the most.

Also, apologies for the wall of text: I had a lot to say.
« Last Edit: January 30, 2025, 08:32:12 am by Nominal Animal »
 
The following users thanked this post: bingo600, ksjh, incf

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
Note that for hashing keywords (ASCII strings and similar), I still use the XOR variant of DJB2 hash (DJBX33X), which is simply
Code: [Select]
uint32_t  djb2_hash(const char *src) {
    if (!src)
        return 0;  // NULL hashes to 0, while "" hashes to 5381.  NA's arbitrary choice.

    uint32_t  result = 5381;
    while (*src)
        result += (result << 5) ^ (unsigned char)(*(src++));

    return result;
and is not at all random; but the distribution of values is excellent, and is very suitable for use as a hash table hash function.

Similarly to a hash table, if you use the 96-bit value as a seed to some good PRNG to get your random sequence, you do not necessarily need an unpredictable or random mapping from the 96-bit value to the seed; you only need most/all 96-bit values to generate "good" seeds.  For a hash function, it means that typical values hashed won't produce the same indexes while not producing other indexes.  Randomness is not needed, just non-periodicity.

In that sense, my reply #5 above is way overkill.  In real life, you'd likely get quite good results if you convert the 96-bit value to a seed by using something as simple as
    uint64_t  seed = (uint64_t)(word[0]) * UINT64_C(odd1)
                   ^ (uint64_t)(word[1]) * UINT64_C(odd2)
                   ^ (uint64_t)(word[2]) * UINT64_C(odd3);
making sure the resulting seed is nonzero, perhaps exclusive-ORing the low bits with 0xAAAAAAAA and the high bits with 0x55555555 if they have fewer than 5 or more than 27 bits set, and just discarding the first hundred or so numbers from the generated sequence.
« Last Edit: January 30, 2025, 08:52:14 am by Nominal Animal »
 
The following users thanked this post: ksjh, incf

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4518
  • Country: gb
  • Doing electronics since the 1960s...
CPU IDs tend to be very predictable, but they are sure to all be different :) The change from one chip to the next might be just one bit.

When I had to do this sort of stuff in the past, I just encrypted (with DES, nowadays AES256 which is much smaller especially if you use Tiny AES) the CPU ID (or whatever unique number was to hand) with itself, and just kept repeating that, getting 64 bits of entirely (and very provably) random data each time.

Then if say you need just 16 random bits, just use 16 of the 64 bits.

« Last Edit: January 31, 2025, 01:09:07 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
I didn't know AES was suitably random when repeatedly called upon only a few bytes of (zero padded?) input.

AES( AES( AES( [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78] ) ) ) etc.

I suppose random number generation is much more forgiving than encryption.
« Last Edit: January 31, 2025, 02:08:15 pm by incf »
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
It's the "avalanche effect" required from cryptographic hash functions.  A single bit difference in the plaintext needs to change about/roughly half the bits in the ciphertext.

Note that even though that uses AES256, the sequence generated is not cryptographically safe.  Anyone knowing any full hash, will be able to generate the rest of the sequence following that hash.  Fortunately, I don't think you have any reason to want a cryptographically secure sequence here!
 
The following users thanked this post: ksjh

Offline ksjh

  • Regular Contributor
  • *
  • Posts: 63
  • Country: de

Xorshift64* is a very simple LFSR pseudo-random number generator, whose upper 32-40 bits pass all known randomness tests, and is thus better than e.g. the industry standard, Mersenne Twister (MT19937).


In my job as a computer scientist doing research in network performance evaluation, we also use a lot of (discrete event) simulations and also need PRNGs. The PCG family of PRNGs is also very promising, see https://www.pcg-random.org/
This web site also provides useful details of different random number generators and their implementation, especially in the linked PCG paper. I see no real need for a cryptographically secure generator in this use case.
 
The following users thanked this post: Nominal Animal

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4518
  • Country: gb
  • Doing electronics since the 1960s...
The STM CPUs, for example, 32F417, use a bit of hardware which makes some noise. But you can fake it; the algorithm which consumes this has no way of testing the randomness.

For your purpose you just need data which keeps changing in a random way.

You can guard against your concern by changing any 0x00 to 0x01 etc :) Anyway you can test it here
https://encode-decode.com/aes256-encrypt-online/
and there are others.

Another thing I did in years past was to take the product S/N (which was factory set and always unique) and do CRC16 or CRC32 on it.

Also see this
https://www.eevblog.com/forum/microcontrollers/32f417-cpu-id-and-creating-a-product-sn-out-of-it/
« Last Edit: January 31, 2025, 04:24:29 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline ksjh

  • Regular Contributor
  • *
  • Posts: 63
  • Country: de
While all these solutions are quite interesting, I think that for this use case, the individual numbers generated by different systems need not necessarily be truely random. As I see it, it is enough that the probability of different systems generating the same numbers is low. So, I think that the approach proposed by Nominal Animal and westf is more than sufficient: Just take the ID as a seed for a PRNG and use this PRNG to generate the different numbers you need. I am not even sure all properties of the PRNG are relevant here. For example, depending on the number of invocations of the PRNG (number of needed pseudo-random numbers), the period of the PRNG for a given seed does not necessarily be large. Perhaps, it does not even matter when the numbers generated by a single system repeat after some time. Even a very primitive LCG could be enough here.
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 21737
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
If you are to avoid this famous example https://xkcd.com/221/ , you need a source of entropy. And obviously that cannot be an algorithm such as PRBS or LFSR, whether implemented in hardware or software.

If you do have a source of entropy, do you then need a PRBS/LFSR?

Without knowing the available hardware, an entropy source cannot be defined.
« Last Edit: January 31, 2025, 05:26:46 pm by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline ksjh

  • Regular Contributor
  • *
  • Posts: 63
  • Country: de
... you need a source of entropy.

Why? I see no reason at all. The starting point was that different systems should have different startup times. If you need just one number that is different on each and every system, indeed, something similar to the xkcd will work, the unique ID is unique. If you need multiple different numbers, you can algorithmically derive a sequence of pseudo-random numbers from this ID, i.e., use it as a seed for a PRNG. They are not truely random, since there is no entropy, you are right. But why would you need entropy? If you have a discrete true random process (e.g., rolling dice), the probability that two systems start with the same number is non-zero, whereas the unique ID is supposed to be unique. We are not gambling or doing crypto here.
Even in simulation, you might want to repeat simulation runs. You can simply do this by using the same seeds for the PRNGs (for each stream of pseudo-random numbers). For independent replications, you would need different seeds in each run, you would use a source of entropy. But in the given use-case, I see no need for that.
 

Offline fourfathom

  • Super Contributor
  • ***
  • Posts: 2023
  • Country: us
Why not just use a hashing function?  I use one for product ID numbers (seeded from the ucontroller internal serial number).  I borrowed liberally from this: http://isthe.com/chongo/tech/comp/fnv/

[edit:]

Sorry, I didn't notice the part about the unique sequence of numbers.
Never mind.
« Last Edit: February 01, 2025, 12:45:52 am by fourfathom »
We'll search out every place a sick, twisted, solitary misfit might run to! -- I'll start with Radio Shack.
 

Offline bson

  • Supporter
  • ****
  • Posts: 2517
  • Country: us
MD5 or SHA-1 (both are very easy to implement and efficient) and use the resulting digest as the initialization vector for ARC4.  Any symmetric stream cipher initialized with a random seed will make an excellent and fast rng.  Encrypt a sequence of 0's and out comes a random sequence.  (Although not of great cryptographic strength these days, but perfectly fine for a random sequence.)  Easily found as portable C/C++ code.

In addition to die numbers, you can also use a dangling ADC input and just read a few k, then digest it down to an IV.  With cryptographic digests a single bit change on the input, on average, will change half the output bits so it takes very little random noise to completely change the IV.

CRC isn't that great; it's generally not large enough, not even CRC-32.
« Last Edit: January 31, 2025, 09:35:25 pm by bson »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 21737
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
... you need a source of entropy.

Why? I see no reason at all. The starting point was that different systems should have different startup times. If you need just one number that is different on each and every system, indeed, something similar to the xkcd will work, the unique ID is unique.

A unique identifier can be a sufficient source of entropy provided different ids are uncorrelated.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 16126
  • Country: fr
From the latests posts, yes, many MCUs these days embed a hardware RNG of some kind and these are usually pretty good, being based on noise.

Since this all started with the unique ID, I think we got into this idea of seeding a PRNG, but while you'll have a very low probability of generating the same sequence with two different devices, it will generate the same sequence for each of them every time the PRNG is seeded.

Using a hardware RNG will avoid this if that matters at all (but depending on the application, having a predictable sequence for each device may be an issue even if they are all different, or it may not matter).

I haven't run into a MCU that didn't embed a hardware RNG lately, among STM32, WCH, NXP and a few others. There sure are, but a hardware RNG is certainly not a rarity. That can be a bit "slow" to generate values though, so if you need a very fast sequence of "random" numbers, you can use a decent PRNG and seed it with a random value generated with the hardware RNG. You can even reseed it on a regular basis to improve the "randomness".
« Last Edit: January 31, 2025, 10:51:09 pm by SiliconWizard »
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
Smaller STM32 devices don't have RNG. (Mine doesn't!)

If AES isn't too slow, I think that is likely to be the least error prone to implement.

I can just copy my 96 bit ID into a zero padded input buffer at startup. Then once every 1 to 10 milliseconds run AES, and then copy the output back into the input.

Not sure how many cycles it would take to run AES-128/256/etc. on a cortex-M0 with single-cylce hardware multiply at 8MHz, but I'm sure it can't be that bad. I'm guessing 4000 instruction cycles at most for a 16-byte block.

I only expect to need a random number once every few seconds at most.
« Last Edit: January 31, 2025, 11:35:23 pm by incf »
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4382
  • Country: us
Quote
Why not just use a hashing function?
Um, OP wants a SEQUENCE.
Nearly ALL of the things mention (CRC, LFSR, assorted Crypto algorithms) can be considered to be "hashing functions."  So we're sort of down to "which hashing functions are most applicable to converting my 96bit ID to a sequence of much smaller numbers.  (ie are liable to produce the most unique sequence.)"

The FNV hash you refer to involves a bunch of 64bit multiplies; not necessarily friendly to a microcontroller environment.
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 16126
  • Country: fr
I think we suggested a few methods that should be more than good enough while being relatively cheap, I'm not sure why incf ended up selecting AES.

In any case, I just suggest evaluating different approaches first on a computer rather than directly on the final target. That'll save a lot of time and will lend itself way better to checking the fitness of the approach. Just use your favorite scripting language or C, generate long sequences that you can analyze directly or save to a file and analyze with other tools.
 

Offline westfw

  • Super Contributor
  • ***
  • Posts: 4382
  • Country: us
Quote
suggest evaluating different approaches first on a computer
The computer won’t have the 96bit IDs…
Some manufacturers document what goes into forming each byte of the id (wafer#, x,y,etc), but others just say “all 96bits will be a unique id”…

 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4518
  • Country: gb
  • Doing electronics since the 1960s...
From my project notes (32F417 168MHz)

The "tiny AES256" https://github.com/kokke/tiny-AES-c does 100kbytes/sec. It is 900 bytes in size. The AES256 which comes with MbedTLS does 800kbytes/sec.  It is 4026 bytes in size.
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
... I'm not sure why incf ended up selecting AES.

I started leaning towards it for three (edit: to be honest) four reasons (1) familiarity (2) ease of use (3) size of the state variable (edit:) (4) the vague feeling that it is so inscrutably complex that it 'must be better'.

I played around a bit and it "seemed" occasionally that certain generators (which I am most certainly using wrong due to a lack of familiarly) can put out spans of numbers that look a bit repetitive or predictable (LFSRs*). I'm not confident that a random sequence generator that uses a 32 bit or 64 bit number internally is 'as good at' representing a 96 bit ID as one that uses 128 bits. For certain algorithms certain ranges of bits in the output looked very predicable and repetitive at times. I think I could easily get it wrong and never know despite rigorous testing until long after it gets out into the field.

*(I know certain bit ranges of certain LFSRs are widely aclaimed as being industry standard level of 'good')

For better or worse I've used AES before and there are a lot of ready to use implementations to 'copy 'n paste' into the firmware. The fact that it operates on 128 bit numbers make it possible to be able to copy in the full unique ID and be confident that no bits of state are being lost due to truncation.

side note: I suppose I absolutely need to be 'mixing in' the unique ID every so often with many of these CRC-like functions, otherwise, they would eventually collide and get stuck in a loop [ AES(AES(AES(ID))) vs AES(ID+AES(ID+AES(ID))) ]
edit: in the case of TinyAES I guess I'd be using AES128 in counter mode with the key set to MCU_ID and initial value set to zero. And just operating on the same buffer repeatedly (output=input).

I feel like I can't simulate this system as well as I'd like (and I certainly can't say that it meets mathematical constraints that I'd like it to meet), so I'm compensating by going overkill on the underlying number generator.

I definitely agree that there are significantly cheaper, simpler, and more efficient methods - I just lack the knowledge, familiarity, etc. to be very confident that they are worth the additional effort.
« Last Edit: February 01, 2025, 01:09:01 pm by incf »
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
You don't need AES, you don't need any cryptography at all. The very simplest 32-bit random number generator will do well for your purpose:

x(n+1) = x(n)*a + b

With a good choice of a and b, it'll produce very good sequences, which will have much better uniform distribution than you need for your tasks.

You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up. Xor it with your unique id if you will.

 
The following users thanked this post: ksjh

Offline Sacodepatatas

  • Regular Contributor
  • *
  • Posts: 116
  • Country: es
Smaller STM32 devices don't have RNG. (Mine doesn't!)

The STM32G030F6P6 has one TRNG, but It is undocumented, you need to use the datasheet of the STM32G041x6/x8 as a guide.
 

Offline ksjh

  • Regular Contributor
  • *
  • Posts: 63
  • Country: de
I definitely agree that there are significantly cheaper, simpler, and more efficient methods - I just lack the knowledge, familiarity, etc. to be very confident that they are worth the additional effort.

Computationally, the effort is much much lower with all presented non-crypto methods (i.e., they are much faster). I would say that they are also very easy to implement. I am not sure why some of the criteria you now mentioned are required for your use-case at all. For example: Why would it matter if you can predict the next pseudo-random number? For the application you described, I would see this as a benefit, not a drawback.

If you want to get an insight into the underlying mathematical foundation (for LFSRs, CRCs etc.), I can highly recommend the book "Finite Fields" by Rudolf Lidl and Harald Niederreiter, https://doi.org/10.1017/CBO9780511525926
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4518
  • Country: gb
  • Doing electronics since the 1960s...
Quote
You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up

That's quite an assumption.

The advantage of using a crypto algorithm (which could be a strong hash too) is that ciphertext is as random as you could ever want. If it wasn't then the cryptosystem is crap. And you need to look no further...
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline Siwastaja

  • Super Contributor
  • ***
  • Posts: 9551
  • Country: fi
As ksjh says, I also see a repeated pattern as positive, not negative feature. It allows you to simulate the behavior and get the same result in real life. And if there ever is a problem with sequencing, it will be repeatable. Conversely, absence of a problem is also repeatable.
 

Offline ejeffrey

  • Super Contributor
  • ***
  • Posts: 4074
  • Country: us
You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up. Xor it with your unique id if you will.

I wouldn't do that unless I had no other choice.  It's not reliably random nor reliably predictable.  And after a non-power-on  reset it won't be randomized at all, just hold whatever was in the memory before.   Using uninitialized ram is fine if you are building a complex RNG that mixes entropy from multiple sources, since at least it won't hurt.  But I wouldn't use it directly as a seed.

If having the same sequence every startup for a given processor is acceptable using the MCU unique ID, hashed down to the target word size is a good option.  If you have an Ethernet interface, the MAC address can be used in the same way.

If you want randomized seeds there are better sources of randomness such as timer events.  Or as I said you can combine multiple entropy sources such that if one of them is bad it doesn't break your system.

If you need secure randomnesss then that is a whole different ballgame.  Almost all sources of easily available randomness are in principle subject to outside interference.  So there is not really any point to discussing without clarifying your threat model.
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 21737
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up.

Everything I have seen leads me to doubt that. Do you have a good source for that assertion?
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Online peter-h

  • Super Contributor
  • ***
  • Posts: 4518
  • Country: gb
  • Doing electronics since the 1960s...
Quote
If you have an Ethernet interface, the MAC address can be used in the same way.

In most embedded systems, the MAC is set by the software :)

Well, you can buy a block of MAC numbers, even chips containing them...
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline ejeffrey

  • Super Contributor
  • ***
  • Posts: 4074
  • Country: us
Depends.  You can buy i2c eeproms with pre programmed unique MAC addresses.  I've used them when I needed a persistent but unique MAC address.  For other applications where I had a good RNG and didn't need persistent addresses I have used randomized addresses.
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
Something that I imagine I might get from AES* is that I could take any N bits of its output (for any value of N at any position in the block) and 'feel pretty confident' that it's going to have good properties** no matter what the MCU ID happens to be.

Suppose my application had 20 parameters of different sizes that I want to be random-ish (sequence unique to that device that varies over time). With something like AES I would take the 16 byte block that it generates and slice it up into bitfields, one per parameter. This would let me generate (what appears to be) a totally unique sequence of 'sets of parameters' that update each tick.

What I perceive (correct me if I am wrong) is that this is a low (programmer/designer) effort way to get lots unique sequences of random-ish numbers of different sizes. All of the parameters update at a uniform rate, and probably very little effort has to go into understanding or verifying it's "suitability" since its AES.

*AES128 in counter mode using the 96-bit MCU ID as the key and starting from a zeroed buffer/initial values and copying the output block back into the input.
**As a unique sequence numbers that vary in a non-trival fashion from tick to tick
« Last Edit: February 01, 2025, 07:54:46 pm by incf »
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
The advantage of using a crypto algorithm (which could be a strong hash too) is that ciphertext is as random as you could ever want. If it wasn't then the cryptosystem is crap. And you need to look no further...

The advantage of Ferrari's motor is that it's hard enough for cracking nuts on its surface. Look no further.
 
The following users thanked this post: mskeete

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up. Xor it with your unique id if you will.

I wouldn't do that unless I had no other choice.  It's not reliably random nor reliably predictable. 

Unpredictable is exactly what you want. Otherwise, use a fixed seed.

And after a non-power-on  reset it won't be randomized at all, just hold whatever was in the memory before.

Which is a random value from your previous run which will work just fine.
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up.

Everything I have seen leads me to doubt that. Do you have a good source for that assertion?

Yes, every time I used it it was random. I have also seen research on the subject. You can search the Internet and I'm sure you'll find some. What is that you have seen which makes you doubt that?

You wouldn't use it for encryption as it may be accessed/stuffed by the attacker. But there's no security risk in the OP's case.

In the OP's cases, where consumers fight for a resource, they should be able to back off if there's too much load. Thus, they can tell when there was a peak load. For that reason, it would be better to provide a regular pattern (as opposed to random) which can be deducted by other units. Such as, if the pub is very busy every Friday, why don't I go to the pub on Thursday.
 

Offline voltsandjolts

  • Supporter
  • ****
  • Posts: 2655
  • Country: gb
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 21737
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
You can get an initial seed (that is x(0)) from uninitialized RAM which will be random at power-up.

Everything I have seen leads me to doubt that. Do you have a good source for that assertion?

Yes, every time I used it it was random. I have also seen research on the subject. You can search the Internet and I'm sure you'll find some. What is that you have seen which makes you doubt that?

Personal observation, coupled with noting that manufacturers make zero guarantees about uninitialized RAM contents.

Plus you don't give any indication whether your observations are random/identical/different for one device each time it is initialized, ditto batch of devices.

Quote
In the OP's cases, where consumers fight for a resource, they should be able to back off if there's too much load. Thus, they can tell when there was a peak load. For that reason, it would be better to provide a regular pattern (as opposed to random) which can be deducted by other units. Such as, if the pub is very busy every Friday, why don't I go to the pub on Thursday.

There's a general principle... If you know how to optimise, optimise. If you don't know how to optimise, randomise.
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
Plus you don't give any indication whether your observations are random/identical/different for one device each time it is initialized, ditto batch of devices.

It has nothing to do with initialization. At power up, each bit of SRAM randomly assumes either '0' or '1'. When you turn the power off, the value is preserved for some time, but eventually get lost, so the value at new power up assumes a new value which is independent of the previous one. If you wish you can get a real MCU or FPGA and experiment with that.
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 21737
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Plus you don't give any indication whether your observations are random/identical/different for one device each time it is initialized, ditto batch of devices.

It has nothing to do with initialization. At power up, each bit of SRAM randomly assumes either '0' or '1'. When you turn the power off, the value is preserved for some time, but eventually get lost, so the value at new power up assumes a new value which is independent of the previous one. If you wish you can get a real MCU or FPGA and experiment with that.

Er, that is initialisation. Not by software, but initialisation  nonetheless.

Your argument presumes there is no imbalance in the memory cells. Any imbalance will bias the initial value to be one way or the other. There is usually some physical parameter that is imbalanced, be it registration of the various masks and layers, or number of doping atoms. The latter is to be expected; current FET gates have hit a physical limit at five atoms thick.

IMNSNO it would be unwise to go into production of a device where correct operation depends on undefined behaviour. Corporate reputation and product recalls are both expensive!
« Last Edit: February 01, 2025, 10:50:51 pm by tggzzz »
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
I would simply initialize a Xorshift64* PRNG with the 96-bit ID.

If you can spare an input pin or two, you can also connect a physical oscillator or a white noise generator circuit, to produce random bits you can mix to the PRNG.

As ksjh mentioned, PCG is another PRNG one can use.  The main difference is the state transition function:
Code: [Select]
#include <stdint.h>

uint64_t  xorshift64_state, pcg_state, pcg_inc;

void xorshift64_step(void) {
    uint64_t  tmp = xorshift64_state;
    tmp ^= tmp >> 12;
    tmp ^= tmp << 25;
    tmp ^= tmp >> 27;
    xorshift64_state = tmp;
}

void pcg_step(void) {
    uint64_t  tmp = pcg_state;
    tmp = tmp * UINT64_C(6364136223846793005) + (pcg_inc | 1);
    pcg_state = tmp;
}
I recommend using e.g. godbolt.org to compile them for your target architecture (example is for ARMv7e-m, specifically Cortex-M4), to see if one is superior for your target architecture.  Xorshift64* uses multi-bit shifts, which for some architectures are costly (although on many 8-bit architectures, they simplify to 4-bit, 1-bit, and 3-bit shifts, as entire bytes can be skipped); PCG uses a 64×64=64-bit multiply, which can be extremely costly on some architectures (namely, ARMv6-m/Cortex-M0/M0+, but not so on ARMv7e-m/Cortex-M3/M4/M7 and later).

I do prefer Xorshift64*, because it has no "bad seeds" except for zero, which is not allowed.

Even if the unique 96-bit IDs were sequential or otherwise perfectly correlated, all you need to do is advance the PRNG states a dozen steps or more, and the sequences will be completely different.  Sequences starting from consecutive seeds are NOT similar.  These two PRNG's outputs are also linearly independent, so skipping every N'th number in the sequence will not make it any less random.  If one wants to manipulate the state directly, then lots of warnings do apply: you do need to avoid "bad states" – which, for Xorshift64*, is the single forbidden zero state.
 
The following users thanked this post: ksjh, incf

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
Since I prefer Xorshift64* for its robust state, I would use the following initialization and entropy-adding functions myself:
Code: [Select]
// SPDX-License-Identifier: CC0-1.0
#include <stdint.h>

typedef union {
    uint64_t  u64;
    uint32_t  u32[2];
    uint16_t  u16[4];
    uint8_t   u8[8];
    int64_t   i64;
    int32_t   i32[2];
    int16_t   i16[4];
    int8_t    i8[8];
    unsigned char  uc[8];
    signed char    sc[8];
    char           c[8];
} word64;

static word64  prng_state;

void prng_initialize(const uint32_t  id[3]) {
    static const uint32_t  fixed[3] = {
        // Your favourite nonzero 32-bit constants here
        0xDEADBEEF, 0xF00DCAFE, 0x0600DD06,
    };
    word64  seed = { .u32 = { 0, 0 } };

    for (int_fast8_t w = 2; w >= 0; w--) {
        if (seed.u32[0] != id[w])
            seed.u32[0] ^= id[w];
        if (seed.u32[1] != fixed[w])
            seed.u32[1] ^= fixed[3];
        for (int_fast8_t n = 16; n >= 0; n--) {
            seed.u64 ^= seed.u64 >> 12;
            seed.u64 ^= seed.u64 << 25;
            seed.u64 ^= seed.u64 >> 27;
        }
    }

    // TODO: the following store should be atomic
    prng_state.u64 = seed.u64;
}

void prng_step(void) {

    // TODO: the following load should be atomic
    uint64_t  tmp = prng_state.u64;

    tmp ^= tmp >> 12;
    tmp ^= tmp << 25;
    tmp ^= tmp >> 27;

    // TODO: the following store should be atomic
    prng_state.u64 = tmp;
}

uint32_t  prng_u32(void) {

    // TODO: the following load should be atomic
    uint64_t  tmp = prng_state.u64;

    tmp ^= tmp >> 12;
    tmp ^= tmp << 25;
    tmp ^= tmp >> 27;

    // TODO: the following store should be atomic
    prng_state.u64 = tmp;

    return (tmp * UINT64_C(2685821657736338717)) >> 32;
}

void prng_perturb_by_u32(const uint32_t  value) {

    // TODO: the following load should be atomic
    word64  tmp = prng_state;

    for (int_fast8_t  w = 1; w >= 0; w--) {
        if (tmp.u32[w] != value)
           tmp.u32[w] ^= value;
        for (int_fast8_t  r = 5; r >= 0; r--) {
            tmp.u64 ^= tmp.u64 >> 12;
            tmp.u64 ^= tmp.u64 << 25;
            tmp.u64 ^= tmp.u64 >> 27;
        }
    }

    // TODO: the following store should be atomic
    prng_state.u64 = tmp.u64;
}

void prng_perturb_by_u8(const uint8_t  value) {

    // TODO: the following load should be atomic
    word64  tmp = prng_state;

    for (int_fast8_t  w = 7; w >= 0; w--) {
        if (tmp.u8[w] != value)
           tmp.u8[w] ^= value;
        for (int_fast8_t  r = 3; r >= 0; r--) {
            tmp.u64 ^= tmp.u64 >> 12;
            tmp.u64 ^= tmp.u64 << 25;
            tmp.u64 ^= tmp.u64 >> 27;
        }
    }

    // TODO: the following store should be atomic
    prng_state.u64 = tmp.u64;
}
SPDX-License-Identifier: CC0-1.0 tells you this code is in Public Domain, per Creative Commons Zero v1.0 Universal.

The notes about atomic loads and stores means that if you want to call these functions from an interrupt context, or if you use an RTOS kernel, the loads and stores need to be atomic/guarded.  On ARM, for example, you might wish to save the interrupt mask, disable interrupts, do the load/store, and finally restore the saved mask.  It depends on your use case, development environment, and the hardware architecture you use.

The 16 iterations in initialization, and 5 and 3 iterations in the perturbations, are not magic numbers.  For initialization, I do recommend anything at or above a dozen (12), but for the perturbations, any positive number of iterations should work well, although I'd be suspicious of less than two iterations.  (I did not put them as named preprocessor macros, because making them larger does not produce any better results.  They only exist to ensure consecutive initializers or state perturbations lead to completely unrelated sequences.)

Because prng_step() only generates but discards a single number, to properly add entropy you can use prng_perturb_by_u32(value) or prng_perturb_by_u8(value).  The former modifies the state twice by 32-bit value (once shifted left by 32 bits), and advances the PRNG both times by 5 steps (arbitrarily chosen, no deeper meaning but to ensure even prng_perturb(1) has significant effect on the sequence generated).  The latter does the same with an 8-bit value, except on each byte of the 64-bit state, advancing by 3 steps each time.  How often you call these should not affect the randomness of the sequence generated, but I wouldn't bother doing it more than once per second or so.  There is nothing magic in the perturb functions: they are safe only because Xorshift64* does not have weak/bad states, except for all zeros, and these avoid it by never clearing the 32-bit/8-bit seed part they are about to modify.  The same approach is not safe to do with all PRNGs!  It is safe here exactly because we know that there are no "poor" states, only the all-zero forbidden one.

Note that when generating pseudorandom values for a specific range, the standard ((prng_u32() % range) + base) actually biases the resulting distribution for the smallest values, whenever range is not a power of two.  The weight of the bias is small for small ranges, but up to half for 2³¹ < range < 2³².  With very fast PRNGs like Xorshift64* and PCG, the mask/shift and exclude method is superior.  It does mean that on average, up to two numbers are generated for each value in range, but these PRNGs are so fast it really does not matter at all; and on some calls, somewhat more numbers may be generated and thus runtime be occasionally longer.

In practice, I use a structure that defines the range generated:
Code: [Select]
typedef struct {
    int32_t   base;   // Minimum value generated.
    uint32_t  mask;   // 0xFFFFFFFF >> ((limit) ? __builtin_clz(limit) : 32)
    uint32_t  limit;  // Maximum value generated is base + limit.
} i32_range_spec;
where mask is effectively limit with all bits less than the most significant bit set, set; you can use
Code: [Select]
uint32_t  u32_mask_for(uint32_t  limit) {
    limit |= limit >> 1;
    limit |= limit >> 2;
    limit |= limit >> 4;
    limit |= limit >> 8;
    limit |= limit >> 16;
    return limit;
}
The generator function itself is
Code: [Select]
int32_t  prng_i32_range(const i32_range_spec r) {
    while (1) {
        uint32_t  tmp = prng_u32() & r.mask;
        if (tmp <= r.limit)
            return r.base + tmp;
    }
}
Because r.limit <= r.mask < 2*r.limit, on average less than two iterations are needed.  If r.limit==0, then r.mask==0, and the function will always return 0.  If prng_u32() returns a random sequence that includes zero, the loop is quaranteed to terminate.

Fixed-time variants are more difficult, although there are a number of approaches.  One is to scale the range and redistribute the bias, i.e. bias = prng_u32(), then base + (((((uint64_t)bias) << 32) + (uint64_t)range * (uint64_t)prng_u32()) >> 32) - bias.  Scaling alone will bias individual values at regular intervals, but biasing the range before the division causes the biased values to vary randomly, thus distributing the bias across the entire range more or less uniformly.  Note that the 64-bit multiplication is actually 32×32=high-32, something natively supported by e.g. ARMv7e-m, so it is quite fast, too, with exactly two 32-bit pseudorandom numbers used for each result.  This isn't perfect, and I haven't run Xorshift64*-high-32bits through BigCrunch or Dieharder to see exactly how good it is, but it is definitely better than the modulo method.  (Main reason is that one needs to run it for a number of ranges, and I only have an aging i5-7200U laptop to number-crunch.  I can only dream of a ThreadRipper..)
« Last Edit: February 02, 2025, 07:31:32 am by Nominal Animal »
 
The following users thanked this post: SiliconWizard, ksjh, incf

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
If one has an entropy source, a CMOS/TTL -level output one can sample in say the 1ms timer tick interrupt or similar, and use to perturb the PRNG state, you would not even need unique seeds at all.  A good option would be to collect 32 or 64 bits to form a word, then use the word to perturb the Xorshift64* state, for example with exclusive-or or addition (using modulo 2⁶⁴ arithmetic, as C does when using unsigned integer types; I do suggest addition here).  The only check needed for Xorshift64* is to ensure the state does not become zero.  (I haven't looked at PGC to see how sensitive its seeds are.)

For a source of entropy, a good option would be an ATtiny in SOT23-6, or perhaps Padauk PMS150C-U06 (SOT23-6, one-time programmable using Free PDK), clocked from its own RC oscillator, generating a suitable LFSR bit sequence.  The entropy would then come from the differences in the two MCU's timings.  You could also use a yet separate RC oscillator as an input to the SOT23-6 MCU, to get even more entropy (from the timing differences between the two MCUs).

If there is free board space, I'd have the MCU drive a charge pump via PWM to 12-18V for a supply to a standard transistor base-emitter avalanche breakdown white noise generator, amplified and buffered to 2.5V to 5V, sampled by the SOT23-6 MCU (either via a comparator, GPIO pin, or ADC), digitally filtered ("whitened") and combined to 8/32-bit words, and used to perturb the LFSR/Xorshift64* state.

(Sincere apologies to those who hate my wall-of-text posts.  Still working on trying to be less verbose.)

« Last Edit: February 02, 2025, 08:57:47 am by Nominal Animal »
 
The following users thanked this post: ksjh, incf

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 16126
  • Country: fr
As ksjh says, I also see a repeated pattern as positive, not negative feature. It allows you to simulate the behavior and get the same result in real life. And if there ever is a problem with sequencing, it will be repeatable. Conversely, absence of a problem is also repeatable.

Exactly. That, this ability to check the behavior on a very large number of successive values and seeds, is why I suggested doing this on computer first. Use C, exact-width integers, and the result will be exactly the same.

I was apparently told (if that was for me) that you don't have the UIDs on a computer. Uh. You just generate a bunch of them randomly (making sure those are unique, by stuffing a table with them and inserting only non-duplicates), and there you go. You can make it analyze statistics on that with a few extra lines of code. Very handy, and a good practice in general (implementing your algorithms first on computer), that some of us recommend on a regular basis.

If you directly code this kind of stuff on the (small) target, you will have a lot of difficulties achieving the same level of verification in any kind of reasonable amount of time, that will look more like shooting in the dark.

My personal "default" for a PRNG is currently Xoshiro256++, which is very good and lightweight enough. It's on par with Xorshift64* IIRC, so either would be fine.

Note: you can use this website to pick and/or test a PRNG: http://www.cacert.at/cgi-bin/rngresults
(you need to generate long sequences for this to be relevant).
« Last Edit: February 02, 2025, 10:23:04 am by SiliconWizard »
 
The following users thanked this post: ksjh

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
My personal "default" for a PRNG is currently Xoshiro256++, which is very good and lightweight enough. It's on par with Xorshift64* IIRC, so either would be fine.
Yes, noting that Xorshift64* is only "fully random" in its 32 to 40 most significant bits (which is what my suggestions use), and that you are referring to the following 64-bit PRNG that passes all the randomness tests:
Code: [Select]
#include <stdint.h>

typedef union {
    uint64_t     u64[4];
    int64_t      i64[4];
    uint32_t     u32[8];
    int32_t      i32[8];
    uint16_t     u16[16];
    int16_t      i16[16];
    uint8_t       u8[32];
    int8_t        i8[32];
    unsigned char uc[32];
    signed char   sc[32];
    char           c[32];
} word256;

static inline uint64_t u64_rol23(const uint64_t value) { return (value << 23) | (value >> 41); }
static inline uint64_t u64_rol45(const uint64_t value) { return (value << 45) | (value >> 19); }

uint64_t prng_u64(word256 *const state) {
    // TODO: Lock state
    const uint64_t  result = state->u64[0] + u64_rol23(state->u64[0] + state->u64[3]);
    const uint64_t  tmp = state->u64[1] << 17;

    state->u64[2] ^= state->u64[0];
    state->u64[3] ^= state->u64[1];
    state->u64[1] ^= state->u64[2];
    state->u64[0] ^= state->u64[3];

    state->u64[2] ^= tmp;
    state->u64[3] = u64_rol45(state->u64[3]);

    // TODO: Unlock state
    return result;
}
It has a very large period, 2²⁵⁶-1, and all states except zero are valid.  After modifying the state, a dozen iterations suffices to get it to produce good sequences; see fig. 7 in Vigna's paper.

That is, after whatever modifications you do to the state (besides generating 64-bit pseudo-random numbers), do 12 or more iterations discarding the result, and the rest of the sequence will be good.  No need to worry about bad states, except avoid the all-zeros state like for Xorshift64*.

It is extremely interesting for architectures without a fast multiplication.  Even on 8-bit ones, u64_rol23() simplifies to a rotation of the eight bytes with one shift-right-through-carry (and set most significant bit if carry set); u64_rol45() to a rotation of the eight bytes with three shift-right-through-carry (+set MSB if carry) over the eight bytes.  The downside is that for multitasking or use in interrupt context, you either need to lock the 256-bit/16-byte structure for the whole generation, or you need to use 16 to 24 bytes of temporary memory or registers.  I definitely need to investigate this for AVR, especially for use in ATtiny's, for random bit sequence generation (2²⁶²-1 bit period) as I described earlier.
 
The following users thanked this post: ksjh

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
Imagine you have an X intersection of 2 roads where you must put through N cars every hour in each direction, and you want to minimize the number of collisions by controlling the time every individual car crosses the intersection, but without knowing when the cars in opposite direction cross. This is akin of the task OP wants to solve.

If you try to  estimate the probability of a collision, you'll soon realize that it does depend on the number of cars per hour, but is completely independent of the timing of car crossing - in other words, for any given number of cars per hour, it plainly doesn't matter what you do - the probability of a collision is the same.  Hence you have no way to influence the average number of collisions (unless you find a way to pass information from one road to the other).

Then why in the world would you need a random number generator with superb statistical properties?
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 7468
  • Country: fi
    • My home page and email address
Then why in the world would you need a random number generator with superb statistical properties?
Because if you simulate the same situation with correlated sequences of car intervals, you'll discover how to maximize the probability of a traffic jam.

Add even one random interval in the mix, and the probability instantly shrinks, for as long as that interval is within the observable range.

In other words, the problem is not when the random number generator is poor, it is when its output is periodic with respect to other related generators: when the incoming car interval sequences correlate, synchronize.  And when you want to avoid that, why not go for a well-known good, extremely fast PRNG?

If your comment was to my suggestion of using the exclusion method instead of modulo operation, the bias indeed is irrelevant for traffic control.  It just happens that unless you have fast hardware division, or the range generated is a constant that the compiler can turn into multiplication by fractional reciprocal, the exclusion method is faster.  The scaling method (on architectures with "mulhi", 32×32=32 high bits of result multiplication) can be even faster.  As usual, I tried to explain what the problem is, and what the alternatives are, so you can choose for yourself.
« Last Edit: February 02, 2025, 03:18:36 pm by Nominal Animal »
 
The following users thanked this post: ksjh, incf

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
I don't think NorthGuy's analogy is quite right.

Roughly speaking, there is a system comprised of a variable power source, and dozens of variable loads.
There are hundreds of these systems. Each has unique properties that cannot be predicted and do change over time.
 
The supplied voltage (and impedance/current limit) will suddenly rise or drop due to external factors - this process is entirely random from the perspective of the loads. Think solar panels.
Supply voltage/impedance It also can suddenly rise or drop due to changes in loading. Loading changes suddenly and randomly due to external factors outside my control or visibility.
My units have to use all available extra power all of the time up to the limit supported and each load (and the source).
This dictates certain total current which changes drastically over time.

I am seeking to avoid are three conditions [for which I have made up arbitrary names]: oscillation, crashing (voltage goes low enough to cause hard reset/fault/'panic stop'), and lockout ('getting stuck in low efficiency regimes of operation').

As Nominal Animal points out, there are some similar conditions that are commonly observed in traffic jams and other similar systems. "The divers are trying to go as fast as possible on an icy road in traffic, but not so fast that they run into the car in front of them and cause a 30 car pile-up"


If all of the units behave identically (or just too similarly, periodically, etc.) some combination of oscillation, crashing, and lockout is practically guaranteed.

There are many ways to deal with it, and everything that has been posted can be made to work with some effort.

-
addendum: the units use 'perturb and observe' type control loops. If they all perturb (or observe) at the same time at the same time or by the same amount problems happen. If they all 'panic stop' (or resume) at the same low voltage problems happen. If cycles occur due to stopping/starting of units issues will happen. etc. etc.

There is a limit to how 'gentle' / 'overdamped' I can make the system.

This some of why I am enamored by the idea of having ~20 independent randomized parameter sequences that are unique per unit. Bad combinations of parameters can still occasionally occur, but only temporarily.

It's a crutch for me feeling that it's not feasible to fully understand or accurately model every possible permutation of the system.



« Last Edit: February 02, 2025, 05:21:48 pm by incf »
 

Online tggzzz

  • Super Contributor
  • ***
  • Posts: 21737
  • Country: gb
  • Numbers, not adjectives
    • Having fun doing more, with less
Then why in the world would you need a random number generator with superb statistical properties?

In my opinion, you don't.

It may be worth pointing out that ethernet manages very well with a non-random backoff, exponential backoff to be exact. https://en.wikipedia.org/wiki/Exponential_backoff
There are lies, damned lies, statistics - and ADC/DAC specs.
Glider pilot's aphorism: "there is no substitute for span". Retort: "There is a substitute: skill+imagination. But you can buy span".
Having fun doing more, with less
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
I am seeking to avoid are three conditions [for which I have made up arbitrary names]: oscillation, crashing (voltage goes low enough to cause hard reset/fault/'panic stop'), and lockout ('getting stuck in low efficiency regimes of operation').

So, you don't really care if some of the loads are off for too long, do you? In such cases, it is not very hard to maintain MPPT voltage by prioritizing loads.
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
When the available current output drops the goal is for some of them to gracefully reduce power more than others - but not go offline. (They share to the extent that they can modulate their power consumption - but if pushed too far some should eventually drop offline first)

A fixed identical exponential back-off would probably run into the three conditions I mentioned previously. There is some lag/propagation delay between when the controller decides to do something and its effect on supply voltage/current/etc.

There are various scenarios:

1. Suppose a unit can consume between 250 and 3000mA. The supply can provide 1000mA and there is one unit is consuming 1000mA. A second unit tries to come online drawing 250mA and causes the supply voltage to drop.
2. Same but 250mA.
3. Suppose three units are consuming 1000mA (and saturating the supply), suddenly the supply current drops by half.
3abc. Same but 500mA/333mA/250mA
4. Suppose that supply voltage crashes despite the best efforts of the control loop and resets some or all of the units
5. Suppose three units are consuming 1000mA, suddenly the available supply current doubles.
6. etc.

(these are just hypothetical examples of the problems I think I might be addressing with "good" randomized sequences of parameters)
I don't think there any any fixed magic values/constants for solving the scenarios that the system faces. (ie. a fixed identical startup delay from 'supply voltage good' to 'applying the load' would not work)
« Last Edit: February 02, 2025, 08:38:29 pm by incf »
 

Offline ejeffrey

  • Super Contributor
  • ***
  • Posts: 4074
  • Country: us
Of course ethernet uses random back off!  It just scales the back off range exponentially.  These serve two different purposes.  The randomization avoids retransmit collisions.  The exponential scaling acts to throttle all nodes when there is contention to avoid the situation where there are many stations trying to transmit and the probability of collisions increases dramatically even with randomization.
 
The following users thanked this post: Siwastaja

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
If you have one 3000 mA load which consists of 12 250 mA stages, mathematically this may be modelled as 12 independent loads of 250 mA each. You can assign to each of these 12 a fixed threshold where you deem there's enough power to turn this particular load on. So, all the loads will be turning on in the fixed order when the power increases, then off in the same order as power decreases. What is wrong with this?

What kind of loads are they - are they resistive?
« Last Edit: February 02, 2025, 09:46:10 pm by NorthGuy »
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
A variable output SMPS charging a bank of batteries. (ideally the batteries eventually get changed and units drop their consumption, but, that can take a very long time - and power budgets are such that 'throwing away' a large amount of available power has a meaningful negative impact on the system. Not to mention that it is preferable to have charging spread out over multiple units)

The 'gist' of the problem is that we don't know what the supply voltage or current is supposed to be for max power. It changes randomly and the unit has no visibility of anything except the supply voltage and it's current consumption. So we are stuck with 'guess and check' using random parameters distributed across dozens of units.

As far as I know there is no way of knowing. The best the unit can do is measure the slope of the IV curve, try to consume as much as possible without dropping the supply voltage too low, try perform measurements in a fashion that does not influence and is not influenced by other units, and ideally not be 'too greedy' so that a degree of sharing occurs.

In reality, the act of measuring will influence the measurements of all of the units on the line. As will any reaction to increased or decreased supply voltage. Very easy to get an unintended 'chain reaction' where the system oscillates, crashes, or 'get stuck' in non-ideal region of operation.

Random/unique timings and thresholds are the main line of defense against units interfering with each other and against things unintentionally 'running away'
« Last Edit: February 02, 2025, 09:25:04 pm by incf »
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
A variable output SMPS charging a bank of batteries.

Typically batteries in a bank are connected together and charge simultaneously which helps to maintain the DoD (Depth of Discharge) of the batteries at the same level, which is somewhat important for the bank. This way they don't fight each other. Why do you want them to fight?

The solar controller is typically a buck device which maintains the solar side at MPPT voltage and the battery side is naturally at the bank voltage. This ensures maximum production. When the bank reaches the desired DoD level, the controller simply disconnects - even though the energy is available, you cannot use it.
 

Offline incfTopic starter

  • Regular Contributor
  • *
  • Posts: 172
  • Country: us
  • ASCII > UTF8
Power sources are shared by multiple independent consumers, it just the system architecture.
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3313
  • Country: ca
Power sources are shared by multiple independent consumers, it just the system architecture.

I'd say selecting a random number generator with superb statistical properties, as opposed to the simplest RNG won't make any difference.

You need a clever approach to selecting your parameters. You can try to detect oscillations by monitoring voltage and bail out if you suspect that your unit causes them, or move to a different switching frequency. Or, you can even try to dump oscillations by adjusting your switching frequency and phase.
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf