In the default gateware, I have noticed that there are corePWM IP instantiated which is has an APB slave interface.
This APB is routed all the way to the SoC, and even has a mention in the device tree overlay, does this mean I can control the generated PWM using APB?
My main aim is to learn on how to use the APB to control things on the FPGA. Any resources to help me understand will be appreciated!
APB: AMBA Peripheral Bus. APB is a “slow” bus used for control and status. The APB interface is what you would use to make your FPGA logic appear in the processor’s memory map.
The AMBA APB specifications are published by ARM it is a fairly easy bus to understand. The Verilog tutorial cape includes a Verilog example of how to interface to control and status registers.
I went through the Verilog tutorial cape to understand the APB example, and this is what I’ve understood so far:
It all starts from the PF_SOC_MSS where the APB Master bus is generated. It’s passed to an APB Arbiter which receives another APB Master from MSS Interrupts.
From the APB Arbiter, the master is connected to a CoreAPB3 module named FIC3_Initiator. This is done to connect a single APB master to multiple APB Slave bus (the cape, the M.2, the CSI, and the HSI slaves).
From there, the Cape Slave bus go to the cape module, where it is connected to the apb_ctrl_status module. This module has three registers internally, a status register at 0x20 address, a control_1 R/W register at 0x10 and a control_0 R/ register at 0x00 which only outputs 0xDEADBEEF.
This clears up my understanding of what’s going onto the fabric, but how can we control the APB master from the CPU? Is the device tree overlay of help here?
I understand it now. The APB interface becomes a memory mapped region for the CPU. Since FIC3 has an offset 0x40000000 and we configure the slave to be at 0x1100000 offset, it can be accessed at 0x41100000 address. Each register in the example is at 0x0, 0x10, 0x20 offset. I wrote the following C code to access the data in the registers.
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#define MAP_SIZE 4096 // 4096 bytes as per DTSO file
#define BASE_ADDRESS 0x41100000
#define OFFSET_REG1 0x00 // Read only register which contains 0xDEADBEEF
#define OFFSET_REG2 0x10 // Read/write register
#define OFFSET_STATUS 0x20 // Read only register which contains the status of the last read operation
int main() {
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
if (mem_fd == -1) {
printf("Error: cannot open /dev/mem\n");
return -1;
}
// Calculate the offset within the mapped region
off_t offset = BASE_ADDRESS;
size_t length = MAP_SIZE;
void *mapped_base = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, mem_fd, offset);
if (mapped_base == MAP_FAILED) {
perror("Failed to map memory");
close(mem_fd);
return -1;
}
//Read REG1 and verify if it contains 0xDEADBEEF
uint32_t value = *((volatile uint32_t *)(mapped_base + OFFSET_REG1));
if(value == 0xdeadbeef){
printf("REG1 contains 0xDEADBEEF\n");
}else{
printf("REG1 does not contain 0xDEADBEEF\n");
}
//Write 0x12345678 to REG2
*((volatile uint32_t *)(mapped_base + OFFSET_REG2)) = 0x12345678;
//Read REG2 and verify if it contains 0x12345678
value = *((volatile uint32_t *)(mapped_base + OFFSET_REG2));
if(value == 0x12345678){
printf("REG2 contains 0x12345678\n");
}else{
printf("REG2 does not contain 0x12345678\n");
}
//Read STATUS and print the value
value = *((volatile uint32_t *)(mapped_base + OFFSET_STATUS));
printf("STATUS: 0x%x\n", value);
munmap(mapped_base, length);
close(mem_fd);
return 0;
}
I think the address for the particular APB bus is configured in the CoreAPB3 module but I’ll have to read the documentation to understand it more clearly.
Still not sure how the address is known. From the technical reference, I know that FIC3 addresses lie in the 0x4000_0000 range, Looking at the Memory Map in the SmartDesign, I know that the address space for the CAPE_APB_MTARGET is 0x0100_0000 to 0x01FF_FFFF. But that still makes the start address 0x4100_0000, while in the DTSO file it’s given 0x4110_0000.
I see cape_gpios_p8 as being at 0x411000 in the dtso file. If you look at the CoreAPB3_CAPE component, you will see it has six outputs. cape_gpios_p8 is driven by APBmslave1. If you bring up the memory map on the CAPE SmartDesign sheet, you will see that P8_GPIO_UPPER_0 has an offset of 0x0010_0000.
0x4100_0000 + 0x0010_0000 = 0x4110_0000, the address in the dtso. Similarly, PWM_2 driven by APBmslave5, shows in the memory map as PWM_2 with an offset of 0x0050_0000. 0x4100_0000 + 0x0050_0000 = 0x4150_0000 agrees with the dtso showing bone_pwm_2 at 0x41500000.
The way you figure out the address map is by looking at the CoreAPB3 Configurator. The important fields are in Address Configuration. The “Position in slave address of upper 4 bits of master address” value is the one of interest. It specifies which nibble in the address will be used to select between each of the downstream ports. Below if the configurator for the FIC3_INITIATOR:
This tells us that the second last nibble (0x?n?????) will be used to select between downstream ports at this level of the bus hierarchy.
We already know that we are connected to FIC3 which has base address 0x4000_0000. So, We are looking at addresses 0x4n?????. Since the cape is connected to port 1, the cape shows up in the 0x41??_??? address range.
Which show bits [23:20] are used to select the downstream ports. Telling us that the APB peripherals within the cape are in the 0x41n?_??? address range where n is the CoreAPB3 port the peripheral is connected to.