Busses consist of both bus masters, and bus slaves. Masters command which addresses need to be read from or written to, while slaves respond to the requests of the bus master(s). For this article, we’ll discuss the logic necessary to implement a very simple bus slave. Why? Because at the core of every memory mapped peripheral is just a touch of simple bus logic.
To understand this concept, let’s consider a very simple bus. Three things can only ever happen on this bus on any given clock cycle. 1) The vast majority of the time, the bus will be idle. 2) You might write to the bus. Or 3), you might read from the bus. On a simple bus, this type of interaction can easily be understood with just a few simple wires:
i_wr: True any time a write is requested. If true for two clocks in a row, then two writes should take place in a row.
i_rd: True any time a read is requested
i_addr: The address of the value to read/write
i_data: The data to be written on any write access
o_data: The data to be returned on any read access
A Simple Write Cycle
Using these values, writing to a memory might be as simple as:
Writing to an arbitrary peripheral has much the same logic, but what it does is a little bit different:
Specifically, writes to a peripheral specify some logic to be done for each possible address you might write to.
In this example, writing to address zero simply sets a configuration register. Writes to addresses one and two are ignored: these addresses reference read only registers. Finally, address three sets a strobe value indicating that some action is to take place, together with the data necessary for that action.
Indeed, from this perspective, the write cycle is pretty simple.
A Simple Read Cycle
The read cycle isn’t really that much more complicated either. For a simple memory, it would look like:
Within a more generic peripheral, where reads might have side-effects, the read logic might look instead like:
You may notice that o_data gets set any time the address changes. What if nothing is being read? In this case, it’s not a problem. The right answer is being placed on the bus either way. However, if you have an input queue, such as in a read FIFO, you might wish to advance that queue on reads only. This is the idea behind the advance_data_stb logic above.
Shared data lines
Having separate input data lines, i_data, and output data lines, o_data, makes a lot of sense within an FPGA.
However, between components on a circuit board, cost may be measured in both area and the number of wires routed from one place to another.
For this reason, when communicating over an external bus, the simple bus we have been describing has one more step:
This last step determines which of multiple digital pieces of logic drives the bus.
That’s all there is to working with a bus. You need to read the address, check if you are being read or written to, and return a value and act appropriately. At some level, every bus acts in this fashion. Hence, if you can understand how to interact with a bus like this, you’ve gone through the first step of ever interacting with any bus.
Now if any man build upon this foundation gold, silver, precious stones, wood, hay, stubble; every man's work shall be made manifest: for the day shall declare it, because it shall be revealed by fire; and the fire shall try every man's work of what sort it is. (1Cor 3:12-13)