December 3, 2023

Let's learn USB! -- linker tricks

Before I get into details, I want to say some things about C coding style, and coding in general. It is often a mistake to "get cute" and do tricky things for the sake of being tricky. One lesson I have learned over decades of programming is that making your code readable is almost always the most important thing.

One thing that really helps code readability is having all the related things grouped together in the same file. If it makes sense to put all of the macros, structure definitions, and functions in the same file (and close together), by all means do so! Some people are not very smart and think that all macro definitions must be in a header file -- even if that header is only included by one C file! Much worse are the people who also think that all header files should be placed in a directory of their own! There can be reasons for this in some big projects, but it is generally just stupidity enforced by bad policy.

All that being said, I am going to show an example of a coding method I have seen used in several embedded projects and decided to try myself in a limited way. The game involves defining harware addresses in a linker script and then referencing them in C code as an extern of some type. I doubt that this makes much sense without an example, so here goes.

I just added the following lines (for the USB PMA ram in the STM32F103 chip) to my linker control file:

PERIPH_BASE = 0x40000000;
APB1PERIPH = PERIPH_BASE;
USB    = APB1PERIPH + 0x5C00;
USB_PMA = APB1PERIPH + 0x6000;

PMA_buf = USB_PMA;
PMA_btable = USB_PMA;
Then I placed the following code in my C source:
typedef unsigned int u32;

struct pma_buf {
        u32 buf[32];
};

/* 4 * 4 bytes = 16 bytes in ARM addr space.
 */
struct btable_entry {
        u32     tx_addr;
        u32     tx_count;
        u32     rx_addr;
        u32     rx_count;
};

extern struct pma_buf PMA_buf[8];
extern struct btable_entry PMA_btable[8];
This works perfectly. Note that PMA_buf and PMA_btable have the same address and lie on top of each other. The story on that is that the first PMA_buf entry (i.e. PMA_buf[0]) must not be used. We can use the last 7 of the 8. This could have been done differently, but corresponds to some existing code. Even more importantly, it corresponds to the way the hardware is designed.

Prior to using this technique, I wrote code like this:

#define USB_RAM         (u32 *) 0x40006000

        struct btable_entry *bte;

        bte = & ((struct btable_entry *) USB_RAM) [ep];
Now I can just write:
	struct btable_entry *bte;

	bte = &PMA_btable[ep]
The new code is certainly short and clean. Is it more readable? Does the compiler generate better code?

I like it, my only complaint being the "behind the scenes" trickery using the linker script file. This violates my previously stated guidelines about keeping all you need to wrap your head around in the same file.

Note that I mix case in the symbol names (PMA_buf and PMA_btable), which may gently hint that something is going on. I will adopt it as a convention and it will probably only help me, which is nothing to sneeze at.


Feedback? Questions? Drop me a line!

Tom's Computer Info / tom@mmto.org