APB Connected LED Controller

Desiging an APB LED Controller Slave and Connecting it to the Zynq Processing System

1812

Creating An APB Peripheral

The ARM Peripheral bus is a de-facto standard used for microcontroller peripherals. Many microcontrollers and processor will have multiple buses; Typically there will one high performance bus (typically AXI or AHB) connecting the core to on chip memories and memory controllers. For connecting to peripherals such as GPIO controllers or timers, the high speed bus will be bridged into one or more slower speed ‘peripheral’ buses. Many of these peripherals don’t require the high bandwidth provided by the system main bus and by using a simpler, slower bus and bridging to the main bus, it eases the integration of peripheral IP into the larger system.

For many designs you may want to create a peripheral with an APB interface, rather than an axi interface.

The APB

Create A project for your APB IP

Create a Vivado project as usual, do not add any constraints, and select a part(this will not matter because the project will be turned into an IP project).

Define Top-Level Signals

Now define the top module for your system. The Top-level signals will be the only signals accessible when the module is packaged as an IP block. In this case we will write an APB slave, so at a bare minimum we will need the signals to interface with the APB. In our example we also add outputs specific to the peripheral.

module apb_led_ctl(

	input rst,
	
	//apb bus signals
	input clk,

	input [31:0] paddr,
	input psel,
	input penable,
	input pwrite,
	output pready,
	output pslverr,
	input  [31:0] pwdata,
	output [31:0] prdata,

	//led output signals
	output [7:0] led,
	output [2:0] rgbA,
	output [2:0] rgbB
);

//insantiate modules here


endmodule

Now we can write logic to go in your IP. In this case we are writing a simple set of addressable registers which hold output data. If we allocate an address to each set of outputs we will need 3 discrete addresses. To decode 3 addresses we will need 2 bits of address space: ceil(log2(n)), so our module will have 4-discrete addresses allocated to it, regardless if we use all 4. This last address could be used for an additional control register or can simply be left unused (reads return 0, writes do nothing). For now we will leave it unused.

In the code below, we create a module definition with the necessary ports and create storage elements required for our registers


module led_ctl(
	input clk,
	input rst,

	input [1:0] addr,
	input [7:0] write_data,
	output [7:0] read_data,
	input write_en,


	output [7:0] led,
	output [2:0] rgbA,
	output [2:0] rgbB
);

	reg [7:0] led_data;
	reg [2:0] rgbA_data;
	reg [2:0] rgbB_data;

	assign led = led_data;
	assign rgbA = rgbA_data;
	assign rgbB = rgbB_data;


endmodule

Write Logic

Now we can add the write decoding logic. based on the address, if the write enable signal is driven, the write_data from the bus will be stored in the corresponding register. Notice there is no logic for (addr==2’d3), as we are simply ignoring writes to this address.

	always @(posedge clk)
	begin
		//write decoding for leds
		if(rst)
			led_data<=8'd0;
		else if(write_en & (addr==2'd0) )
			led_data<=write_data;

		//write decoding for rgbA
		if(rst)
			rgbA_data<=2'd0;
		else if(write_en & (addr==2'd1) )
			rgbA_data<=write_data[2:0];

		//write decoding for rgbB
		if(rst)
			rgbB_data<=2'd0;
		else if(write_en & (addr==2'd2) )
			rgbB_data<=write_data[2:0];
	end

Read Logic

For the read logic, we only need combinatorial logic. Based on the address, we can drive the data bus with the appropriate register data. Unlike the write logic, we need to cover for unused addresses (something always needs to be driven on the read data bus).

	reg [7:0] in_rdata;
	assign read_data = in_rdata;

	always @(*)
	begin
		case(addr)
			0:
				in_rdata= led_data;
			1:
				in_rdata = {5'd0,rgbA_data};
			2:
				in_rdata = {5'd0,rgbB_data };
			3:
				in_rdata = 8'd0;
		endcase
	end

Instantiating logic and connecting to the APB.

Now we can instantiate our controller module and wire it up to the APB. The APB provides a 32-bit address space, however our module has only 2 bits of address necessary. If we map the two-lowest bits of the address,

Take care when assigning address bits. The ARM’s address space is byte-addressed, meaning every discrete address is (). The Zynq only allows 32-bit aligned accesses to the programmable logic, so you need to insure your registers are addressed on 32-bit boundaries (address is a multiple of four). This is easily accomplished by assuming the lowest 2 address bits will be zero, and using only higher order bits in addressing.

For driving the pready signal, we can tie it high; Our module has no need to extend any transactions. Similarly pslverr can be tied low, as no cases exist for needing an error condition.

The write enable signal to your registers should be driven when the slave is selected, the transaction is in the access phase (penable is 1), and the transaction is a write (pwrite is 1).

module apb_led_ctl(

	input rst,

	//apb bus signals
	input clk,
	input [31:0] paddr,
	input psel,
	input penable,
	input pwrite,
	output pready,
	output pslverr,
	input  [31:0] pwdata,
	output [31:0] prdata,

	//led output signals
	output [7:0] led,
	output [2:0] rgbA,
	output [2:0] rgbB
);

	wire write_en;
	wire [1:0] addr;

	//IMPORTANT, make sure you don't use bits [1:0]
	assign addr = paddr[3:2];
	assign write_en = (psel & penable & pwrite);

	assign pready=1'b1;
	assign pslverr=1'b0;




	led_ctl led_inst(
		.clk(clk),
		.rst(rst),
		.addr(addr),
		.write_data(pwdata[7:0]),
		.read_data(prdata[7:0]),
		.write_en(write_en),

		.led(led),
		.rgbA(rgbA),
		.rgbB(rgbB)
	);

	//make sure remaining data bits are driven
	assign prdata[31:8]=0;



endmodule

Creating an IP Project

Now we can package our module as an IP. Select Tools->create and package new IP, this will open a dialogue to configure your IP.

Figure 1. Create and Package New IP.

In ‘Packaging Options’ select “Package your current Project”. In the next dialogue you will be asked for a location for your IP. You can place it in the default location, or move it elsewhere. Either way, make sure you can find this location later. Complete the wizard’s steps and Vivado will open a new IP project.

Figure 2. Package Current Project.

In an IP project you can edit your HDL as usual but also configure how your IP will be packaged. To do this, open up the IP-XACT folder and click on “component.xml”. This will bring up the options for packaging your IP.

Figure 3. Opening the IP-XACT File.

Defining the APB interface and mapping signals to it

To make for easy connection in the block design, we can add an ‘apb’ interface to our IP. Open the ‘ports and interfaces’ tab. In here you will see the top-level signals of your module. Select “auto-infer interface”.

Figure 4. Ports and Interfaces, Auto-infer Ports button.

In the dialog select Advanced->apb_rtl.

Figure 5. Selecting port interface type.

This should create an apb subgroup in your port list, and automatically add apb signals to it (It does this based off of the signal’s name, so you need to be careful when defining top level signals).

Figure Figure 6. APB interface added and ports mapped to it.

If not all of your apb signals are mapped, you can open the port-mapping tool and manually map the remaining ports. In the port mapping screen you can also confirm the ports have been mapped correctly to the APB interface.

Figure 7. Manually Mapping Interface Ports.

Configuring Addressing

The block design tool in Vivado can map the address space of slaves into the address space of a bus master such as the Zynq’s ARM Processor. In the IP packager, a logical interface needs to be given an address block so the block design tool knows how to map the slaves addresses. This does not affect how the core is synthesized, but it does affect how the bus it is connected to is configured. In the IP packager select the ‘Addressing and Memory’ Tab, and run the Addressing and Memory Map Wizard.

Figure 8. Addressing And Memory Configuration.

Select the APB interface and give it a name. Click next and then finish.

Figure 9. Selecting an Interface to Configure Addressing for.

Now your interface will show up in the Addressing and Memory pane. Right click on the interface and select “add address block”.

Figure 10. Adding an Address block to an interface.

In the address block you can modify the properties of the IP’s address space. In our case we have 4-accessible 32-bit addresses, which is 16 memory locations for the ARM (though only 4 will be accessed). Set range to 16. For IP cores that need more memory space this can be modified (The Minimum range should at least cover any addressable memory, but can be greater).

Figure 11. IP Interface with a configured Address Block.

Customization GUI

Now you can open the ‘customization GUI’ tab. Here you can verify the ports and interfaces available on your IP. If everything looks correct, you can move on to ‘Review and Package’ where you can package your IP as a self-contained unit.

Figure 12. IP Customization GUI.

After you click “package IP” Vivado will tell you where to find your IP. Make sure you remember this location so you can add it to your IP catalog.

Figure 13. Packaging an IP project.

Add your IP to a block design

Now you can create a project for your Zynq system block design. Once you have created your project, Open the IP Catalog. Right click and select “add repository”. Navigate to the location you packaged your IP and select the folder. Now in the block design you can add your IP block.

Figure 14. Adding an IP repository.

Adding your IP to the block Design

Once your IP repository is added, you can find your IP in the IP catalog and add it into a block design

Figure 15. Adding custom IP core to block design.
Figure 16. Custom APB core added to block design .

Connecting your IP to the Zynq Processing system

If you haven’t already added a Zynq IP block to your design, do so now. Make sure to apply the correct presets file and run block automation. Since the Zynq has AXI ports you will need to add an axi bridge to your system. If your design does not already have an AXI interconnect, add one to your design.

Make sure the Master port and clocks of the Zynq are connected properly to the interconnect.

Figure 17. Adding an AXI interconnect to the block design.
Figure 18. Connected AXI interconnect IP Block.

Now add an AXI-APB bridge and connect it to one of the interconnect’s master ports.

Figure 19. Adding an AXI-APB bus bridge.
Figure 20. AXI APB Bridge Connected to AXI interconnect.

You can now connect your apb ip to the bridge. Make sure to connect your module’s clock to the same clock as the bridge and a reset of the correct polarity. You can configure the AXI-APB bridge for additional slave ports, should you need more. You can also reduce the number of Available ports.

Figure 21. APB Slave IP Core Connected through AXI-APB Bridge.

Mapping an APB Slave

Once Connected to the Zynq system, you can map the slave into the address space of the processor. Open the Address Editor tab. Here you should see the apb ip is in the category of ‘unmapped slaves’. To map it into the processor’s address space, select the ip and click on “auto assign address”.

Figure 22. Address Editor, Showing Auto-assign button.

This will give the IP a base address and a range. Any accesses in this address space from the processor will drive the ‘psel’ for that peripheral high.

Figure 23. Slave now Mapped into PS7 address space.

Now click Validate Design. If any errors pop-up you will need to correct them.

Figure 24. Validate Block Design.

Once you have validated your design, you can verify the address you mapped to your slave device by opening the configuration page for the AXI-APB bridge. (Changes to the Bridge’s Addressing are made when the design is validated)

Figure 25. Updated AXI-APB Bridge Addresses.

Complete Your block design

Now you can complete your system’s block design and connect any necessary inputs and outputs. Once you generate a bitstream you can export the hardware to Vitis where you can write a software program. From a software viewpoint, any APB slave is accessed the same as an AXI IP (using loads and stores to memory).

Figure 26. Completed PS7 block design with a connected APB Slave.