November 15, 2023

Let's learn USB! -- Regbits (papoon)

The papoon USB code introduces a scheme the author calls "regbits". Ultimately this results is a weird C++ dialect we need to learn how to understand. Our goal is to learn how to read his code, not to continue writing more code in this style.
Let's attack this by analyzing some relevant examples.

Testing a bit in the interrupt status register

if (stm32f103xb::usb->istr.any(Usb::Istr::RESET)) {
Here is an example from usb_dev.cxx as most of these probably will be. We know what this does. It tests the interrupt status register (istr) in the usb controller and tests if the RESET bit is set. We know this by context and "squinting" at this code rather than understanding it.

We can follow this back to a definition in stm32f103xb.h that looks like this. Just as a note, this struct is inside of struct Usb. You might guess this from the syntax "Usb::Istr::RESET" above.

   struct Istr {
        using              pos_t = Pos;
        static constexpr   pos_t
               EP_ID_POS = pos_t( 0),
                 DIR_POS = pos_t( 4),
                ESOF_POS = pos_t( 8),
                 SOF_POS = pos_t( 9),
               RESET_POS = pos_t(10),
                SUSP_POS = pos_t(11),
                WKUP_POS = pos_t(12),
                 ERR_POS = pos_t(13),
              PMAOVR_POS = pos_t(14),
                 CTR_POS = pos_t(15);

        using              bits_t = Bits;
        static constexpr   bits_t
        DIR              = bits_t(1,          DIR_POS),
        DIR_OUT          = bits_t(1,          DIR_POS),
        ESOF             = bits_t(1,         ESOF_POS),
        SOF              = bits_t(1,          SOF_POS),
        RESET            = bits_t(1,        RESET_POS),
        SUSP             = bits_t(1,         SUSP_POS),
        WKUP             = bits_t(1,         WKUP_POS),
        ERR              = bits_t(1,          ERR_POS),
        PMAOVR           = bits_t(1,       PMAOVR_POS),
        CTR              = bits_t(1,          CTR_POS);

        static const uint32_t
              EP_ID_MASK =       0xF;

        using      mskd_t = Mskd;
        using      shft_t = Shft;

        REGBITS_MSKD_RANGE("istr::EpId",
                           EP_ID,
                           ep_id,
                           EP_ID_MASK,
                           EP_ID_POS,
                           EP_ID_MASK);
    };  // struct Istr
    using istr_t = Reg;
          istr_t   istr;
Wow! Now there is a lot of tricky C++ code. Let's discuss some of it. "Pos" is a template class defined in regbits.h. You might guess this from the angle brackets. I won't (for now) dive into the way that is set up in regbits.h, but the template facility is used like a fancy sort of macro substitution. It isn't hard to see what is doing on. The bit positions in the istr register are being made into pos_t objects, then these are used to generate definitions like "RESET"

The "using" statements are converting the template names into something shorter and handier (pos_t instead of Pos). The Bits class gives us masks of a certain length ata certain position. It all sort of makes sense, but certainly is long and elaborate.

The method "any()" tests for any bits being set in a word, in this case in the word "stm32f103xb::usb->istr". This is defined in stm32f103xb.h with code like:

static const uint32_t   PERIPH_BASE         = 0x40000000U;
static const uint32_t   APB1PERIPH_BASE     = PERIPH_BASE;
static const uint32_t  USB_BASE            = APB1PERIPH_BASE + 0x00005C00U;

STM32F103XB_PERIPH( Usb,                usb,            USB_BASE          );
The final macro in the above expands to:
static volatile Usb* const usb = reinterpret_cast(USB_BASE);
So, "usb" is a handy pointer to a "struct Usb", which happens to contain "Istr", our interrupt status register.

Extracting the endpoint number

What about this code in the ctr() method?

Usb::istr_t istr = usb->istr;
uint8_t     eprn_ndx = istr >> Usb::Istr::EP_ID_SHFT;
What can I say -- this just looks totally wrong. The endpoint ID is in the last 4 bits of "istr", so it doesn't need to be shifted, it needs to get masked out. On top of that, I don't see that EP_ID_SHFT gets defined anywhere. Somehow the code compiles (and seems to work).

I add a printf and discover a shift value of 15. This shift would give us the CTR bit. It turns out this is entirely bogus, unexplainable, and to be ignored.
Adding more printf, I get:

istr = 00008012
eprn_ndx = 2
EP_ID_SHFT = 15
Getting 2 out of the lower 4 bits of 0x8012 is certainly correct, but how we get it by shifting 15 bits is far far beyond my understanding.

The answer lies in overloading of the ">>" operator in regbits.h -- the operator actually does this:

return (_word & shft._mask) >> shft._pos._pos;
Holy cow. This sort of thing hardly makes for readable code (unless you are an expert in the regbits dialect). Knowing there is magic here and in the REGBITS_MSKD_RANGE sort of explains this. The mystery is how EP_ID_SHFT belched out the irrelevant and misleading value "15".

So I feel angry. I have been lied to. I'll forgive most sins, but I won't forgive being lied to and tricked like this.

Any normal human being with knowledge of the C language seeing the shift operator alongside a constant (or what looks like a constant) containing "SHFT" in the name would hardly expect masking and no shifting at all.

The decision to overload the ">>" operator was a very bad one. Why not just introduced a method called "extract" that would actually help a person to understand the code. This is a case where advanced language features tempt a person to be far too cute.

This entirely ends any interest I might have had in regbits and even papoon. This kind of unreadable coding and developing of dialects with ugly surprises is the worst possible thing imaginable. Badly named variables are one thing, but deliberate deception, trickery, and twisting of the language is inexcusable.

Endpoint registers

After all of that, I am in a bad mood and ready to pour out merciless criticism of the regbits scheme used in papoon. Here is an example of testing a bit in an endpoint register:
if (usb->EPRN<0>().any(Usb::Epr::CTR_RX)) {
What is odd (to me anyway) is that he uses the C++ template mechanism rather that a simple array to refer to endpoint registers. An odd choice, but certainly something we can learn how to read. And we are using his "any()" method to test whether a single bit is set. I can certainly read this now, but I would simply use an array and write code like:
if ( usb->epreg[0] & EP_CTR_RX ) {
I am skeptical that the C++ source would produce equally good assembly code but I could be wrong. (Actually I did peek and the code doesn't look all that bad). In any case, good generated code doesn't excuse using an unreadable dialect.
Feedback? Questions? Drop me a line!

Tom's Computer Info / [email protected]