Design your own memory using ABEL - Embedded.com

Design your own memory using ABEL

Designing memory devices would seem to be beyond the abilities of most embedded systems developers. These devices are packed with an enormous amount of interconnected logic that makes them almost impossible to design 100% schematically. Designing memory chips involves iterating millions of storage cells and implementing sophisticated address decoding for accessing each row of cells. Moreover, multiplexing data out of those storage cells to the external data bus also requires awesome logic.

In this article, I'll demonstrate how to design an 8MB static random access memory (SRAM) using a combination of schematic design and a bit of ABEL (Advanced Boolean Expression Language) programming to overcome the aforementioned hardships. ABEL provides a means of making behavioral descriptions of a logic circuit. For purposes of demonstration I'll be using an ispLEVER starter module from Lattice Semiconductor.

SRAM functional blocks
Our memory device is organized as a dual-bank array (1,024 rows by 512 columns) of byte-wide slices. Figure 1 illustrates the basic functional blocks. Each bank slice (“a” and “b”) can be accessed separately by activating a corresponding input signal (“Ba” or “Bb”). A byte slice in either bank is selected for read or write operations according to the decoded row and column addresses (A0:9 and A10:18, respectively) input to the device. The write-enable signal WE latches input data in the selected slice; data read from that slice is passed to the system bus by activating the OE signal. Activating the chip-enable signal CE permits read and write operations on storage banks. Storage array “a” is assigned the lower half of the data bus (D0:7) and array “b” is assigned the upper half (D8:15). A bus controller switches data between storage arrays and the system data bus. A chip controller combines the input signals mentioned earlier to accomplish read and write operations. Internal data buses from the bus controller provide data paths to and from the storage array.

View the full-size image

A hierarchical design approach proves most sensible for complex designs like this one. We start with by preparing circuit schematics for some building blocks of the device in hand. Those blocks are then compiled into a module that produces a higher-level block within the device. Embedding the resulting block in another schematic or program module can continue more or less indefinitely, forming a hierarchy of blocks leading to the final device design, or even a printed-circuit board design containing the device itself along with other chips or boards.

Adopting this approach, I chose to design the chip controller and a single byte slice of the storage array using circuit schematics. Accordingly, I produced two schematics at the bottom of the hierarchy, on top of which I prepared an ABEL module that combines and iterates them as well as generating the remaining chip blocks, ending up with a complete chip design.

Schematics
The byte slice is constructed of an 8-bit D-type flip-flop, as shown in Figure 2. Input data is latched on the rising edge of the WR signal while the byte address- and bank-selection signals (AD and EN, respectively) are active. Note that those signals are combinatorial, generated from the chip controller described below. I prepared the schematic of the byte slice using Lattice's schematic editor. I deliberately designed this simple granule of the memory device schematically (rather than in ABEL) to illustrate the marriage between schematics and programming that can prove useful in more complex designs.

View the full-size image

The chip controller, shown in Figure 3, receives five control signals from some outside memory-controller chip and decodes these signals into six output signals that drive the read/write operations on byte slices. The WR signal drives the clock CK of the byte slice, while either Ba or Bb signals are used to drive the enable signal EN of the byte slices in banks A or B, respectively. The Do signal controls tri-state I/O buffers that switch read data from either bank to the system data bus.

View the full-size image

Note that I deliberately introduced a mismatch between the names of the signals in the controller and the byte slices. You should expect to see similar naming mismatches between the SRAM chip pins and the chip-controller input signals. For example, Sel-A and Sel-B (selection signals for banks A and B) connect to chip pins BA and BB, respectively, which were shown in Figure 1.

The reason for this little pseudo-mistake is that I like to see design modularity and reusability in the building blocks of any project. I take care to resolve signal connections in the programming modules of each project.

The programming module
The top of the SRAM design hierarchy is the ABEL module shown in Listing 1 in which I replicate the byte slice into two banks. The module sram_top_module.abl appears in the ispLEVER's Project Navigator window (shown in Figure 4), superseding the schematic modules sram-slice.sch and sram-control.sch .

Listing 1 ABEL listing for SRAM byte slices

0001 |0002 |0003 |TITLE 'SRAM Design ABL'0004 |0005 |DECLARATIONS0006 |0007 |  Dob15..Dob0  NODE istype 'buffer'; "buffer for output data lines from memory array0008 |  Dib15..Dib0  NODE istype 'buffer'; "buffer for input data lines from external data bus0009 |0010 |"declare chip blocks designed in schematics0011 |  sram_bank    INTERFACE ([EN,Di_7_,Di_6_,Di_5_,Di_4_,Di_3_,Di_2_,Di_1_,Di_0_,CK,AD] ->        [Do_7_,Do_6_,Do_5_,Do_4_,Do_3_,Do_2_,Do_1_,Do_0_]);0012 |  sram_control INTERFACE ([WE,Sel_B,Sel_A,OE,CE] -> [WR,RDb,RDa,DO,Bb,Ba]);0013 |0014 |"declare a macro for creating a vector of Bank A memory cells0015 |  MBA macro (x)0016 |  { @expr {MBA}?x; };0017 |0018 |"declare a macro for creating a vector of Bank B memory cells0019 |  MBB macro (x)0020 |  { @expr {MBB}?x; };0021 |0022 |"declare 1024x512 memory bytes for each bank (A and B) in the chip0023 |  @const rows = 1024;0024 |  @const columns = 512;0025 |  @const rep = rows * columns;    "defines the total bytes in each bank0026 |0027 |  @const x = 1;  "index used for processing bank cells in each vector0028 |0029 |  @repeat rep0030 |  { MBA(x-1) FUNCTIONAL_BLOCK sram_bank;     "Bank A declaration statements generation0031 |  MBB(x-1) FUNCTIONAL_BLOCK sram_bank;       "Bank B declaration statements generation0032 |     @const x = x + 1; };0033 |0034 |  MC FUNCTIONAL_BLOCK sram_control;	"chip controller declaration statement0035 |0036 |"input pins of package0037 |  WE           PIN;   "write enable signal0038 |   OE          PIN;   "read enable signal0039 |   CE          PIN;   "chip enable signal0040 |   BA, BB	PIN;   "bank enable signals0041 |   Ai18..Ai0 	PIN;   "byte address lines 0042 | 0043 |"bidirectional 3-state data bus of memory chip 0044 |  Dio15..Dio0   PIN    istype 'com';0045 |0046 |"sets0047 |  DLio = [Dio7..Dio0];   "lower set of bidirectional 3-state data bus of memory chip0048 |  DHio = [Dio15..Dio8];  "higher set of bidirectional 3-state data bus of memory chip0049 |  DLob = [Dob7..Dob0];   "lower set of buffered data lines output from memory array0050 |  DHob = [Dob15..Dob8];  "higher set of buffered data lines output from memory array 0051 |  DLib = [Dib7..Dib0];   "lower set of buffered data lines input from external data bus0052 |  DHib = [Dib15..Dib8];  "higher set of buffered data lines input from external data bus0053 |  AR = [Ai9..Ai0];	"set of input row address signals to memory array0054 |  AC = [Ai18..Ai10];	"set of input column address signals to memory array0055 |  CS = [WE,OE,CE,BA,BB];	"chip set of input control signals0056 |0057 |EQUATIONS0058 |"connections to chip controller0059 |  MC.[WE,OE,CE,Sel_A,Sel_B] = CS;0060 |0061 |"internal connections to all memory banks in the chip0062 |  DLio.oe = (MC.DO == 1); "enable chip lower address bus for data reading0063 |  DHio.oe = (MC.DO == 1); "enable chip upper address bus for data reading0064 |  DLio = DLob; " pass on buffered lower data lines output from memory array to the          external data bus0065 |  DHio = DHob; " pass on buffered higher data lines output from memory array to the          external data bus0066 |  DLib = DLio; " pass on external data to the buffer of internal lower data lines0067 |  DHib = DHio; " pass on external data to the buffer of internal higher data lines 0068 |  @const r = 1;0069 |  @repeat rows0070 |  { "connections for banks by row0071 |       @const c = 1;0072 |       @repeat columns 0073 |       {"connections for banks by column0074 |          @const x = (c-1) + (r-1)*columns;  "index used for processing bank cells in                  each vector0075 |0076 |       "Bank A connections0077 |          MBA(x).CK = MC.WR; 0078 |          MBA(x).EN = MC.Ba; 0079 |          MBA(x).[Di_7_,Di_6_,Di_5_,Di_4_,Di_3_,Di_2_,Di_1_,Di_0_] = DLib;0080 |          MBA(x).AD = ( (AR == (r-1)) & (AC == (c-1)) );  "address decoded line for each                  memory byte in bank A0081 |0082 |       "data ouput multiplexing based on the setting of input addresses and control                signals0083 |       when ( (AR == (r-1)) & (AC == (c-1)) & MC.RDa == 1) then 0084 |       DLob = MBA(x).[Do_7_,Do_6_,Do_5_,Do_4_,Do_3_,Do_2_,Do_1_,Do_0_]; 0085 |0086 |       "Bank B connections0087 |          MBB(x).CK = MC.WR; 0088 |          MBB(x).EN = MC.Bb; 0089 |          MBB(x).[Di_7_,Di_6_,Di_5_,Di_4_,Di_3_,Di_2_,Di_1_,Di_0_] = DHib;0090 |          MBB(x).AD = ( (AR == (r-1)) & (AC == (c-1)) );  "address decoded line for each                  memory byte in bank B0091 |0092 |       "data ouput multiplexing based on the setting of input addresses and control                signals0093 |       when ( (AR == (r-1)) & (AC == (c-1)) & MC.RDb == 1) then 0094 |       DHob = MBB(x).[Do_7_,Do_6_,Do_5_,Do_4_,Do_3_,Do_2_,Do_1_,Do_0_]; 0095 |0096 |       @const c = c + 1;};     "column iteration end0097 |0098 |  @const r = r + 1;};          "row iteration end0099 |0100 |END SRAM_Design; 

View the full-size image

The ABEL module starts with a declaration of the basic byte slice (statement 11) and the chip controller (statement 12), which point to their schematics. Output and Input data buses internal to the chip are declared in statements 7 and 8. Those buses will move data from the system data bus to the byte slices.The byte slices are replicated by declarations in statements 15 to 32. The macros in statements 15 to 16 and 19 to 20 declare two vectors of byte slices MBA(x) and MBB(x), which are populated by the iteration of statements 30 to 31. The iteration is driven by @Repeat directive in statement 29 that is set to the total number of bytes in each bank (specified by the constant expression in statement 25). The dimensions of the memory array are declared in statements 23 and 24, agreeing with Figure 1. By doing this, the ABEL compiler inserts the declarations of 524,288 byte slices (see Listing 2) into the source file for banks A and B during preprocessing. At that point, we start to see the power of hardware programming and the simplicity it offers the designer.

Listing 2 Declarations of 524,288 byte slices inserted into the source file by ABEL compiler

BAA(0) FUNCTIONAL_BLOCK sram_bank;BAA(1) FUNCTIONAL_BLOCK sram_bank_;..  

The input pins of the memory chip are declared in statements 36 through 41, while the bidirectional data bus pins are declared in statement 44. We shall do the proper mapping (wiring) of those pins to the signals of the memory array and the controller further down in the code.

Now I declare data buses internal and external to the chip in halves: the lower data bits 0:7 and the higher data bits 8:15. Statements 47 to 52 take care of this declaration. Splitting the data buses makes it simpler to pass data between each bank and the external data bus. So bank A is assigned the lower data bus and bank B takes the higher bus.

The tri-state bus controller from Figure 1 is implemented in statements 62 to 67–another design simplification over pure schematic design.

The internal connections to each byte slices in the array are declared in statements 77 through 84 for bank A, and statements 87 through 94 for bank B. Those statements are iterated in two nested loops: the outer one (statements 68, 69, 98) handles the rows of the array, while the inner one takes care of the columns (statements 71, 72, 96). The compiler executes both iterations and produces the relevant connections to each byte slice in both banks.

Statements 80 and 90 are responsible for address decoding that activates the AD signal used in the schematic of the byte slices of banks A and B. Here the beauty of programming is obvious. A massive decoding is achieved when iterating both statements in the nested loops, generating 1,024 lines for the storage array rows and 512 lines for its columns. The explanation of the decoding process goes like this: when the address lines of rows (AR) give the value of (r –1) in decimal and the address lines of columns (AC) give the value of (c –1) in decimal, then we are correctly pointing to element (x ) of byte slice vectors in bank A or B (calculated in statement 74).

Passing data to the external data bus requires a massive multiplexer that selects the data from the appropriate byte slice. Statements 83 and 84 handle byte slices of bank A, while statements 93 and 94 handle those of bank B. The conditional statements check if the row and column addresses lead to the byte slice being referenced in the vector of either bank. If the slice is addressed and the chip is enabled and the output data read is enabled, then the slice data is selected for transport to the external data bus. Thus, we've created two multiplexers of 524,288 x 8 lines each–a real achievement!

Processing the design hierarchy in ispLEVER Project Navigator generates a chip symbol, shown in Figure 5, having 24 input signals and 16 bidirectional signals in addition to power and ground pins (though not shown).

View the full-size image

Note that in ispLEVER Project Navigator, you can choose an FPGA device from a set of families to implement the chip. However, FPGAs offer only smaller array dimensions than those assumed in my project.

Hybrid approach
Logic circuit design can be so complicated that schematic design tools alone can't accomplish the task. Combining a suite of tools is a hybrid approach to tackling complex designs. In this article, I've demonstrated the marriage between schematic design and HDL programming to validate that approach. I chose memory design as an example because it involves lots of signal decoding and multiplexing, along with enormous replication of logic cells.

This hybrid approach requires a hierarchy of building blocks in which schematics are prepared for blocks that have fewer details and interconnections, while the HDL takes care of joining and replicating the schematically designed blocks. The design hierarchy is beneficial in its own right, as it enhances reusability and modularity. You can reuse any of the building blocks in another project and integrate them using a similar approach. You can also apply modifications to any of those blocks with a minimal effort, propagating changes to the rest of the project.

Gamal Ali Labib is an IT consultant in Cairo, Egypt. He specializes in IT security and IT turn-key projects management. He is also interested in parallel processing and VLSI. Dr. Labib has a B.Sc. and M.Sc. in computer engineering and electronics from Ain Shams University, Egypt, and a PhD in computer science from University of London, U.K. You can reach Dr. Labib at .

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.