The trifecta of ARM, Assembly, and C: Pointers, Registers, and …

Our sincerest request to the reader is to go through the previous post The trifecta of ARM, Assembly, and C. This discussion builds up on top of it.

We often tend to develop for a particular OS on regular development cycles. It may sound improbable, but often most embedded developers find dealing in tandem with constructs on an operating system (Embedded Linux or RTOS or equivalent Baremetal OS) with some resource management. Maybe not that often but if there comes a time to execute the assembly constructs on more upper layers of OS our discussion today may help in that.

Assumptions and Pretext

We have used BeagleBone Black as our targeted hardware with host development machine as Intel – Windows

  • Operating System: Linux / Embedded Linux
  • Operating Hardware: ARM – A8 – 32 Bit (ARM v7-A)

Problem Statement

Access the USER_LED GPIOs from the Linux user space.

Bifurcation of Problem Statement

Access to USER_LED GPIOs requires access to peripheral GPIO registers and their configuration.

  1. Virtualization of memory by Linux makes access to the actual register addresses nearly impossible
  2. If direct driver support of configuration/modification of the dedicated register is not available we need to establish the functionality in the userspace layer

Probable Solution

  1. ARM supports memory map IOs and “/dev/mem” represents the physical memory in Linux. We can use these constructs with the help of mmap.
  2. For the sake of demonstration and our discussion, we will assume there is no library and underlying driver support for the register configurations. Hence we will implement the same in assembly and C equivalent.

The combination of 1 & 2 in our probable solution gives rise to the problem of C and Assembly interconnect – very specifically we may need to pass the assigned mapped memory by mmap from the Linux kernel to the assembly. We will explore the possibilities to circumvent this and what is required to archive our solution. For ease of following the solution, we would break down the implementation in the below format.

  1. Implement physical access with Assembly and C interconnect.
  2. Know hardware details
  3. Bring everything together
Implement physical access with Assembly and C interconnect

The below code block implements the C and Assembly interconnect. Note that this implements pointer utilization of C in assembly (as mentioned in the heading).

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

#define BYTE_REQUEST 10
#define BYTE_OFFSET 0

void main() {
	
	char *logFormat = "value @%d\n";
	int finalValue = 0;
	
	int fd = open("/dev/mem", O_RDWR | O_SYNC); // Open /dev/mem
	if (fd == -1) {
		perror("Error in opening /dev/mem : ");
		return;
	}

	void *start_addr = mmap(NULL, BYTE_REQUEST, 
			        PROT_READ | PROT_WRITE,
				MAP_SHARED|MAP_ANONYMOUS, fd,BYTE_OFFSET);
	if (start_addr == MAP_FAILED) {
		perror("Error in start_addr ");
		return;
	}

	int *intPtr = (int *)start_addr;
	intPtr[0] = 10;  /* the allocated bytes would have junk values - allocate
		            4 bytes for confirmation */
	printf(logFormat,intPtr[0]);
	
	//Register Setup for verification
	asm volatile (
		"Prepare:\n\t"
		"MOV v1, #10 \n\t"
		"MOV v2, #11 \n\t"
		 );
		
	//Interconnect with a local variables - Example
	asm volatile( 
		"Local_Copy:\n\t"
		"ADD %[finalValue], v2, v1 \n\t":[finalValue] "=r" (finalValue)
						:/*input*/:/*clobber*/
		);

    // Interconnect with a local pointer variables - Example
	// The below block performs addition of v1, v2 registers and stores in v3
	// MOV instruction store the address in pointer 'start_addr' to v1 
	asm volatile(
		"Pointer_Store:\n\t"
		"ADD v3, v2, v1 \n\t"
		"MOV v1, %[start_addr]\n\t" :/*output*/
					    :[start_addr] "r" (start_addr)/*input*/
		       			    :/*clobber*/
		);
	// Executes storage of v3 register content to address in v1
	asm volatile (
		"Pointer_Copy:\n\t"
		"STR v3, [v1]\n\t"
		);
	printf(logFormat, *(int *)start_addr);

	//unmap the mapped address region from /dev/mem
	munmap(start_addr, BYTE_REQUEST);
}

Some key takeaways and elaborations

  • The assembly is written from the perspective of the instruction set. The use of variables in the syntax is explained by ARM(here).
  • The storage of a pointer variable via Assembly is a multi-step process and has been demonstrated by two asm blocks with labels ‘Pointer_Store’ followed by ‘Pointer_Copy’.

Below is the disassembly of asm blocks written above for a more elaborate outlook. For better read on disassembly the register map V1(R4), V2(R5), and V2(R6).

00010516 <Prepare>:
   10516:	f04f 040a 	mov.w	r4, #10
   1051a:	f04f 050b 	mov.w	r5, #11

0001051e <Local_Copy>:
   1051e:	eb05 0304 	add.w	r3, r5, r4
   10522:	613b      	str	r3, [r7, #16]
   10524:	68bb      	ldr	r3, [r7, #8]

00010526 <Pointer_Store>:
   10526:	eb05 0604 	add.w	r6, r5, r4
   1052a:	461c      	mov	r4, r3

0001052c <Pointer_Copy>:
   1052c:	6026      	str	r6, [r4, #0]

With the above blocks in place and the last stepping stone to solving our problem is to locate the register address of the LED and configure it for desired behavior. This should be piece of cake or a can of worms depending on the availability of documents. Luckily we are using a very standardized hardware BeagleBone  Black and most of the document and community support is available.

Know hardware details

For any hardware details, the reference manual and design diagrams of that hardware are a must. In an organization, the dedicated hardware teams publish these diagrams for the use of embedded software designers. For open source, these details are available at the discretion of the community. BBB(Beagle Bone Black) community is very good in such a context. I am referring to the documentation site of the Beagle Bone hardware. Specific to our problem statement i.e. USER_LED hardware information can be found here(doc link). The document suggests the LEDs are attached to GPIO1 Pad and also this suggestion posed a question that how to access GPIO1 Pad on BBB. The answer to this question is not on BBB hardware but the SoC (System on Chip) attached to the hardware i.e. AM335x Sitara processor by Texas Instrument. We need to access the datasheet or technical reference manual of the processor (link).

This gives us a GPIO1 register pad Start address of 0x4804_C000 and an end address of 0x4804_CFFF. Also, further looking at the datasheet – we will arrive at the conclusion that the modification & configuration setting registers have a relative address to the start address. Notable would be:

  • GPIO output enable register at an offset of 134h named GPIO_OE
  • GPIO set data out register at an offset of 194h named GPIO_SETDATAOUT
  • Optional read on GPIO_CLEARDATAOUT we would like you to explore and try out modifications.
Bring everything together

Utilizing all the discussion above we can write a simple C program as below to set and reset all the GPIO pins of the GPIO1 pad. Why not set specific to just 4 GPIOs- fair enough to ask, but we leave this problem for you.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

#define ONE_32BIT 0xFFFFFFFF
#define ZERO_32BIT 0U
#define GPIO1_START_ADDRESS 0x4804C000
#define GPIO1_END_ADDRESS 0x4804CFFF
#define GPIO_OE_REGISTER_OFFSET  0x134
#define GPIO_SET_OUTPUT_REGISTER_OFFSET  0x194
#define GPIO_CLEARDATAOUT 0x190
									
void main() {
	int fd = open("/dev/mem", O_RDWR);

	if (fd == -1) {
		perror("Error in opening /dev/mem : ");
		return;
	}

	// Must Read why volatile	
	volatile void *gpioStartAddr = mmap(NULL,
					2*(GPIO1_END_ADDRESS-GPIO1_START_ADDRESS),PROT_READ | PROT_WRITE, MAP_SHARED, fd,
					GPIO1_START_ADDRESS);
	if (gpioStartAddr == MAP_FAILED) {
		perror("Error in mapping gpio start_addr ");
		return;
	}
	
	volatile unsigned int  *gpioSetOutputRegister = gpioStartAddr + GPIO_SET_OUTPUT_REGISTER_OFFSET;
	volatile unsigned int  *gpioResetOutputRegister = gpioStartAddr + GPIO_CLEARDATAOUT;
	volatile unsigned int  *gpioOERegister = gpioStartAddr + GPIO_OE_REGISTER_OFFSET;
	printf("gpioOERegister %X\n", *(int *)gpioOERegister);
	printf("gpioAddr %p %p\n", (int *)(gpioSetOutputRegister ), gpioStartAddr);
	for (int i =0; i < 20; i++) {
		*gpioOERegister = 0U;
		*gpioSetOutputRegister =  ONE_32BIT;
		printf("on %X\n", *(int *)gpioSetOutputRegister);
		sleep(1);		
	
		*gpioSetOutputRegister = (*gpioSetOutputRegister) & 0U;;
		printf("off %X\n", *(int *)gpioSetOutputRegister);
		sleep(1);
	}
	munmap((void * )gpioStartAddr, 2*(GPIO1_END_ADDRESS-GPIO1_START_ADDRESS));

}

The above block of C code can be converted to C and assembly code with slight modifications as we discussed.

Hope this helps you to understand how these interconnects work.

We will be back with more, till then – Cheers.

One thought on “The trifecta of ARM, Assembly, and C: Pointers, Registers, and …

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s