TS-4710 Accessing Hardware Registers

From embeddedTS Manuals

The standard assumption in Linux is that kernel drivers are required in order to control hardware. However, it is also possible to talk to hardware devices from user space. In doing so, one does not have to be aware of the Linux kernel development process. This is the recommended way of accessing hardware on a TS-SOCKET system. The special /dev/mem device implements a way to access the physical memory from the protected user space, allowing reading and writing to any specific memory register. Applications may be allowed temporary access through memory space windows granted by the mmap() system call applied to the /dev/mem device node.

The following C code is provided as an example of how to set up user space access to the SYSCON registers at base address 0x80004000:

#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
static volatile unsigned short *syscon;
static unsigned short peek16(unsigned int adr) {
	return syscon[adr / 2];
}
static void poke16(unsigned int adr, unsigned short val) {
	syscon[adr / 2] = val;
}

int main(void) {
        int devmem = open("/dev/mem", O_RDWR|O_SYNC);

	assert(devmem != -1);
	syscon = (unsigned short *) mmap(0, 4096,
	  PROT_READ | PROT_WRITE, MAP_SHARED, devmem, 0x80004000);

        poke16(0x6, 0x3); // disable watchdog
        poke16(0x12, peek16(0x12) | 0x1800); // turn on both LEDs

        return 0;
}

Important Notes about the preceding example:

  • The peek16 and poke16 wrapper functions make the code more readable due to how pointer arithmetic/array indexing works in C, since the same offsets from the register map appear in the code.
  • Make sure to open using O_SYNC, otherwise you may get a cachable MMU mapping which, unless you know what you're doing, probably is not what you want when dealing with hardware registers.
  • mmap() must be called only on pagesize (4096 byte) boundaries and size must at least have pagesize granularity.
  • Only the root user can open '/dev/mem'. For testing, this just means the tester needs to be root, which is normal in embedded Linux. For deployment in the field under Debian, this can be an issue because the init process does not have root privileges. To get around this, make sure the binary is owned by root and has the setuid bit set. The command 'chmod +s mydriver' will set the setuid flag.
  • The pointers into memory space should have the same bit width as the registers they are accessing. In the example above, the TS-4710 FPGA registers are 16 bits wide, so an unsigned short pointer is used. With very few exceptions, FPGA registers on TS-SOCKET macrocontrollers will be 16 bits wide and CPU registers will be 32 bits wide. Unsigned int, unsigned short, and unsigned char pointers should be used for 32, 16, and 8 bit registers, respectively.
  • When compiling ARM code that emits 16 bit or 8 bit hardware register accesses, it is important to add the compiler switch -mcpu=arm9. Otherwise the wrong opcodes may be emitted by the compiler and unexpected behavior will occur.
  • Pointers into memory space must be declared as volatile.