Your SlideShare is downloading. ×
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Vhdl design flow
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×
Saving this for later? Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime – even offline.
Text the download link to your phone
Standard text messaging rates apply

Vhdl design flow

437

Published on

Vhdl Design Flow

Vhdl Design Flow

Published in: Education, Technology, Design
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total Views
437
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
36
Comments
0
Likes
0
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. 0 Lab CMPE 480 – ADVANCED DIGITAL LOGIC DESIGN Department of Electrical and Computer Engineering University of Alberta Winter 2009 VHDL Design Flow
  • 2. VHDL Design Flow T his tutorial is intended to provide you with an introduction to the tools that you will be using through the term. This tutorial will guide you through the design flow of a digital system, from its modeling in a hardware description language (VHDL) to its implementation in an FPGA. While this tutorial attempts to expose you to the most common features of the tools, it will not demonstrate all. Once completing the tutorial you are encouraged to explore the capabilities of the tools. The more familiar you are with your design environment, the more productive you will be. Lab Overview When you finish this lab you will know how to: Create a VHDL design and verify its syntax. Create a test bench waveform. Synthesize and implement your design. Perform behavioral and post-place and route simulations of your design. Create a configuration bitstream for an FPGA. Background – VHDL Design Flow STEP 1 - Design Entry The first step in the design process is to input your design into a machine-readable format. To do this you will use a Computer Aided Design (CAD) tool. In CMPE 480 we will use Xilinx Foundation. This is s typical CAD tool supports many design entry methods, such as a schematic capture, HDL entry (VHDL/Verilog) or a component netlist. In this course, all the designs are described using VHDL. STEP 2 – Functional Simulation Once a design has been described, the next step is to simulate it. This is done to verify that the design will meet the requirements of its specification. The first type of simulation that is performed is a Functional Simulation. This is also referred to as a Behavioral simulation in Xilinx Foundation.
  • 3. A behavioral simulation is used to verify that logical behavior of the circuit. It is very important to realize that simulations of this type DO NOT contain any physical implementation details, which means that they DO NOT include any component delay timing information. Designs that pass a functional simulation are on their way to being realizable in hardware, but will not necessarily work as expected. It is also very important to realize that some statements in VHDL are not synthesizable and are therefore included for simulation purposes only. Consider the following example: Z <= A and B after 2ns; The after qualifier is an arbitrary delay that cannot be realized in hardware, thus it is not synthesizable and would be ignored by most synthesis tools. It is included to model expected delays in a behavioral simulation. STEP 3 - Synthesis During synthesis, the CAD tool will interpret your VHDL design information and infer standard building blocks to implement your design (registers, multiplexers, lookup tables, adders, etc.). Subtle differences in your VHDL description can result in different hardware being synthesized at this stage. Different hardware inferences can result in large variations in system performance. STEP 4 – Implementation The Xilinx implementation process takes your design through the TRANSLATE, MAP, and PLACE AND ROUTE sub-processes. The TRANSLATE process convert the netlist generated from the synthesis process, into a form specific to the target device. The MAP process translates the standard building blocks into the specific resources available in the target hardware. The PLACE & ROUTE process picks up where the MAP process leaves off by allocating specific resources (placing) and interconnecting (routing) the placed design. At the end of this process you can perform a post-place and route simulation. This is the most accurate simulation available through the Xilinx toolset. It will give you an indication of what to expect of your design once it is actually implemented. STEP 5 – Device Configuration After the design has been verified, a binary hardware configuration file is generated (bitstream). This file is then downloaded into the FPGA via the JTAG interface. 2
  • 4. Part I - Getting Started In this tutorial you will create an 8-bit up/down counter using VHDL as the design description language. Your design will be synthesized and a bitstream file will be generated. The bitstream will be used to configure an FPGA in a Digilab board. You will be able to interact with your counter through switches and push-buttons to control its operation. The count will be shown in a set of 7-segment displays in a D2IO board. Starting Xilinx Foundation Double click on the Project Navigator shortcut icon on the desktop, or select: Start → Programs → Xilinx ISE 7.1i → Project Navigator Creating a New Project A project is a collection of all files necessary to create and to download a design to a selected FPGA. We will begin by creating a new project: 1. Select File → New Project to open the new project dialog box. 2. Select a meaningful name for the project. A directory of the same name will be created in the path shown in the Project Location. 3. Select HDL as the Top-Level Module Type, and click NEXT. 4. Fill in the properties as shown below: 3
  • 5. In CMPE 480 we use a Spartan II XC2S200-PQ208-6 FPGA. Our synthesis tool will be XST. We use ModelSim as our simulator and we will use VHDL as our hardware description language. Remember those settings, because every project you create in this class will have the same settings. 5. Click Next to proceed to the Create New dialog box in the next section of the tutorial. Source dialog box. We will fill this Creating a VHDL Source A block diagram of the up/down counter is illustrated below. It has five inputs: CLOCK, DIRECTION, LOAD, RESET and COUNT_IN. The direction of the up/down counter is indicated by the DIRECTION input (0 = down, 1 = up). The LOAD input sets the count to the value that appears in the 8-bit bus COUNT_IN. When the RESET input is ‘1’, the count is set to 0. There is one 8-bit bus output called COUNT_OUT, which shows the current count. Now we will generate a VHDL module where we will model the behaviour of the up/down counter. 1. Click on the New Source button in the Create New Source dialog box. 2. Select VHDL Module as the source type. 3. Enter counter as the file name. 4. Verify that the Add to project checkbox is selected, and then click NEXT. 5. Define the ports of your VHDL module in the Define VHDL Source dialog box. In the Port Name column, enter the port names: CLOCK, RESET, DIRECTION, LOAD, COUNT_IN, and COUNT_OUT. In the Direction column indicate whether each port is an input, output or inout. The only output is COUNT_OUT, all the other ports are inputs. Because COUNT_OUT and COUNT_IN are 8-bit buses, select 7 in the MSB (Most Significant Bit) column, and 0 in the LSB (Least Significant Bit) column of each of these ports. 4
  • 6. Your table should look similar to this: 6. Click NEXT in the dialog box and then click Finish in the next one to conclude the declaration of your module. 7. Click NEXT in the New Project Wizard dialog box. 8. The dialog box that you see now allows you to add existing files to the project. This is useful when some previously built modules are to be reused. Since this is not our case, just click on Next, verify your project specifications and if everything is correct click on Finish. If necessary, click on the Back button to correct some of the project specifications. Notice that Xilinx has created a new VHDL file using a standard template and the information entered in the New Source Wizard. Some standard libraries have been included, your entity has been defined and your architecture has been declared. By this time, the VHDL module should look like this: 5
  • 7. Defining the Architecture Now that we have a design file to work with, let’s define the architecture (behavior) of our module. Enter the following code exactly as it appears into the body of your architecture. Take a few minutes to examine the VHDL code and understand how the counter is modeled. Creating a Test Bench To perform a functional simulation, you will need a test bench. A test bench is another VHDL file that is used as a wrapper to instantiate one copy of the design, to provide inputs to your design and monitor the outputs. You can create this file manually, or you can use the test bench waveform editor to create a standard template based test bench which you can enhance later if required). 1. Select Project → New Source to open the new source dialog box. This should look familiar. 2. This time we will select Test Bench Waveform. Note that you could select VHDL Test Bench or Verilog Test Fixture if you wanted to create the test bench manually. In keeping with good naming, we will create our test benches’ with the same name as the design they are intended to test and add the suffix ‘_tb’. 3. Once you have provided a name for your test bench, click NEXT. You are now asked to associate your test bench with a source file. In this case we only have one source file so select it, click NEXT. Verify your test bench settings, click FINISH. 6
  • 8. 4. You will now specify the timing parameters used during simulation. The Clock high time and Clock low time together define the clock period for which the design must operate. The Input setup time defines when inputs must be valid. The Output valid delay defines the time after active clock edge when the outputs must be valid. Set the Initial Length of Test Bench to 5000ns and then Click OK. Defining the Simulation Inputs Now you should see the HDL Bencher tool in your workspace window. This tool is used to enter input stimulus and expected results graphically. Inputs are represented by blue squares. Outputs are represented with yellow squares. Clicking in any colored square will toggle its value (for single bit signals). The CLOCK signal has been initialized for you, but you still need to define the other inputs. 1. Let’s start with the RESET. Click on cycle 1 so the signal is set high. Click again on cycle 2 to set the signal low. We just defined a reset in the first cycle to initialize our counter. 2. Click on cycle 1 of DIRECTION to set the signal high, and then go to 1900ns and click again to set it back to low. We will count up for 8 cycles and then we will count down for the rest of the test bench. 7
  • 9. 3. Click on cycle 1 of COUNT_IN, and enter the number 64. Click on LOAD at 4300ns so the signal is set to high. Click on the next cycle to set LOAD back to low. We are setting the count to the value 64 after 4300ns. Once you save your test bench, notice that it is automatically added to your project. Also notice that it is ‘under’ the design module that the test bench was associated with. Select the test bench file in the source window and notice the processes window change. The processes available for a test bench are different from those available for a VHDL file. Xilinx automatically generates a corresponding VHDL behavioral model for the test bench waveform specified in the GUI. To view this file, select View Generated Test Bench As HDL from the processes window with the test bench selected in the source window. Test benches created in this fashion will have a *.vhw extension. We are now ready to simulate our design! Behavioural Simulation Now that we have created a test bench file, we are ready to launch the simulator. The Xilinx toolset does not include its own simulator. Instead, they choose to partner with third party tools. In CMPE 480, we will use Model Technology’s ModelSim XE (Xilinx Edition). ModelSim itself is a powerful tool and warrants some extra investigation. For now, we will cover only what we need to know to get going. With the test bench selected in the source window you should see something like the figure on the left. Expanding the ModelSim Simulator process we see each of the available types of simulation listed. They are listed in order of less to more accuracy from top to bottom respectively. To perform a functional (behavioral) simulation, 8
  • 10. simply double click Simulate Behavioral Model. This will launch ModelSim. ModelSim Simulator Let’s take a quick look at the ModelSim interface. ModelSim is a command line driven tool . Everything that can be done via the GUI can be done via the command line, or an automated script (macro). A command line reference is available on the course website for those who wish to take advantage of the more advanced features. By default, when ModelSim launches there are four windows displayed. Other windows can be accessed via the View menu selection in the workspace window. Workspace Window The main workspace window contains the Command Line Interface. In the LI, all of the commands invoked by the Xilinx macros are displayed, as well as the output from ModelSim itself, including any warnings or errors. The console also includes a prompt where single commands can be entered manually. Examining the commands that are echoed in this window will reveal more details about the simulators inner-workings. Double clicking any errors displayed will open the Source window and highlight the erroneous line in your VHDL description. Structure Window The structure window displays the hierarchy of your design. The top-level module will always be your test bench. Embedded within the test bench is the Unit Under Test (UUT). The UUT is your design file. If your design contains nested design modules, each component will be listed in the structure window. 9
  • 11. When you perform behavioral level simulations, modules are listed in the structure window with the name given to the component when it is instantiated. If you perform any of the back-annotated simulations, the names listed in the structure window are associated with the physical level resources. This can make ‘drilling down’ much more challenging. But more about that later. Signals Window The signal window will show all of the ports declared in the entity definition for the selected module. These signals can then be dragged into the wave window to be monitored. Wave Window The wave window displays the results of the actual simulation. You will spend a lot of time using this window, so it is a good idea to spend some time familiarizing yourself with its capabilities. For example, you can customize your display by aligning, color coding and selecting the radix for your signals and then File → Save Format to save the display setting as a macro. You can then File → Load Format to restore your settings the next time you run a simulation. This is very useful. Play here. 10
  • 12. Once you are satisfied with the settings, print the waveform. When printing your waveform, be sure that the area of interest in your simulation takes up the entire display area. Zoom in our out as necessary to make this true. It is in your best interest to make the display as readable as possible. Imagine that you are giving this to your technically challenged boss. It should be very readable without going into excruciating details. When annotating your waveform, make sufficient comments so that a technically proficient colleague would be able to discern the purpose, and the outcome of the simulation. NOTE: Once the design has been proven to work logically, we can turn our attention toward the implementation details. That is, taking the design from the realm of the theoretical to the actual. Close ModelSim. Synthesis Synthesis is a very straightforward step with the Xilinx toolset. Keep in mind that when you execute a process in Xilinx, you are executing that process on the source file currently selected in the sources window. To synthesize your design, select it in the sources window and double click Synthesize-XST. This will begin the synthesis process; monitor the console window for any errors. If the synthesis was successful, a green check mark will appear beside the synthesis process. Once a design has been synthesized, it is time to address the physical implementation details. For the most part, your CAD tools automatically handle these details. There are a number of user definable constraints that can be specified to refine system performance; most of which are related to timing constraints. We won’t cover those issues at this point. Implementation After all of the user constraints have been set, you are ready to implement your design. Once again we will put our trust in the tools to perform the low level details required for implementing our design into a specific hardware device. ASIDE: One major advantage of using an abstracted HDL language for digital design is that it allows designers to focus on design issues and leave implementation details to the CAD tools. This allows designers to test multiple architectures quickly. However, this can also been seen as a disadvantage as the designer is no longer in direct control of the hardware details required to implement their design. This can lead to uncertainty in the debugging phase and an overall lack of understanding of the final circuit. The implementation process is again very straightforward with the Xilinx toolset. Select your design module in the sources window and double click the Implement Design 11
  • 13. process. This can take some time if you have a large design or slow computer. Monitor the console window for warnings or errors. Confirm that the process completed successfully. Once a design has been implemented, a post place and route simulation can be performed. This is the most accurate timing simulation that our tool can provide because it knows all the details of our physical implementation. Select the test bench in the sources window and then choose Simulate Post-Place & Route VHDL Model in the processes window. This will launch ModelSim. Take a close look to the transitions in COUNT_OUT, and compare this results to those obtained with a behavioural simulation. FPGA Configuration Download the file Lab0_FPGA.zip and extract it in your computer. This file contains a project with a pre-built configuration that will abstract you from the hardware details of the Digilab board where you will download your design. A block diagram of this system is shown in the figure shown in the next page. The D2IO driver controls the output of data to the 7-segment displays, and LEDs. It also reads the inputs coming from the push-buttons and switches, and can provide them to another module, such as our up/down counter. The LOAD, RESET, and DIRECTION inputs are provided through the push-buttons. COUNT_IN is set through the switches and the COUNT_OUT is displayed in two 7-segment displays. 1. In the Project Navigator select File → Open Project. 2. Select the project Lab0_FPGA, from the directory you just extracted. Your Sources window should look like this: 12
  • 14. Block diagram of the components in project Lab0_FPGA. 13
  • 15. Adding your Up/Down Counter to the Project You may have noticed that there is a question mark on the counter module of the project. This shows that ISE cannot find a file with the VHDL module for the counter. You need to add your own module here. 1. Select Project → Add Source. 2. Select your counter module from your previous project. Click on OPEN. 3. Select VHDL Design File as the type of the source. 4. Click on the toplevel module in the sources window and synthesize your design. There should be no errors at this time. Pin Assignment Once the design is synthesized, the most basic user constraint that must be performed is the pin assignment. Pin assignment is the process of associating logical ports with physical pins. If you do not specify which pins your logical ports should be connected to, the tools will randomly assign pin locations. This will not suffice in cases where our target device is already located on a fixed Printed Circuit Board (as in our lab). Pin assignments and other user-defined constraints are contained in a User Constraint File (*.ucf). In this lab, we provide you with a pre-built UCF. In later labs you will learn how to build one by yourself. 1. To add this file to the project: 2. Select Project → Add source. 3. Select the file toplevel.ucf and click on OPEN. 4. A dialog box will appear, asking you to associate the constraints file with a source. Select toplevel from the list of options and then and click OK. Configuring the FPGA Once the pin numbers have been assigned to the logical ports, let’s create the toplevel.bit file (the bit file will always have the same filename as the entity you are implementing) and download it to the D2 board. Before you generate the bit file, select the toplevel source file and right click the Generate Programming File process. Select Properties. Select the Startup Options tab. For FPGA Start-Up Clock, use the pulldown box to select JTAG Clock. 14
  • 16. Be sure that your D2 board is powered and the parallel cable is properly connected. If you are at a station with a parallel port switch box, be sure that the switch is set to D2/D2IO Board. Now we should be set. Expand the Generate Programming File process and double click the Configure Device (iMPACT) process. If you have not implemented your design at this point it will automatically be synthesized and implemented. The iMPACT Device Configuration Utility should launch automatically. When iMPACT runs it will ask you a few questions about what you are trying to do. First select Boundary-Scan Mode. Click Next. Now select Automatically connect to cable and identify Boundary-Scan chain. Click FINISH. An information box may now pop-up. Read it. You are now asked to specify a programming file. This will be our toplevel.bit file. Use the dialog box to locate your programming file and click OPEN. At this point another information box may pop-up. For some reason Xilinx seems to detect our XC2S200 device as a XCV200 device, so it gets a little confused. Don’t worry about it; we actually do have a XC2S200 click YES. Now, simply right-click the device and select Program. 15
  • 17. Verification The counter should be working by this time. The 7-segment displays in the board should be showing the current count. Interact with the counter through the pushbuttons and switches and verify that the design is working as expected. IO is assigned as follows: LOAD : Push-button B0 RESET: Push-button B1 DIRECTION: Push-button B2 COUNT_IN: 8-switch bar COUNT_OUT: 7-segment displays Demonstration (8 marks) Show your working implementation to the Lab Instructor or a TA. Demonstrate the complete design functionality. This demonstration is worth 80% of Lab 0. 16
  • 18. Part II - Floorplanner and FPGA Editor (brief tutorial) This section is an introduction to two new design tools that we will use during this lab: the floorplanner and the FPGA Editor. In order to follow this tutorial you will first need to create a VHDL module with the half-adder description that we discuss below. Synthesize the sample design and verify its functionality with a behavioural simulation. Sample Design – Half-Adder Our sample design is a Half-Adder (HA). The HA performs bit-wise addition between two input bits. We will examine a half-adder in more detail in Lab 1, but for now you can use the code to learn how the floor planner works. The VHDL equivalent of the external interface description is known as an declaration. The HA entity declaration would appear as follows: entity ha is Port ( X Y Sum Carry end ha; : : : : in std_logic; in std_logic; out std_logic; out std_logic); architecture Behavioral of ha is begin Sum <= X xor Y; Carry <= X and Y; end Behavioral; 17 entity
  • 19. Floorplanner Floorplanning is particularly useful on structured designs and data path logic. With the floorplanner, designers can see where to place logic for optimal results, placing data paths exactly at the desired location on the die. The floorplanner enables designers to plan a design prior to or after using Place-andRoute (PAR) software. Invoking floorplanner after a design has been placed and routed allows designers to view and possibly improve the results of the automatic implementation. In an iterative floorplan design flow, designers floorplan and place and route interactively. The logic placement in the floorplan window can be modified as often as necessary to achieve design goals. Iterations of a floorplanned design can be saved for use later as a constraints file during PAR. In this lab we will use the floorplanner to define a user constraint file (UCF) containing our pin assignments. 1. Select your design file in the sources window and expand the toolbox in the processes window. User Constraints 2. Double-click Assign Package Pins in the processes window. 3. The following message provides information about the UCF file creation process. Information messages are denoted by the blue question mark on the left of the dialog box. It is highly recommended that you read them. Click Yes, it will launch the PACE editor. 4. The Pace editor allows you to graphically associate your logical ports to the physical pins of your target package. Expanding the I/O Pins folder in the Design Browser window will reveal the logical ports in your design. The ports are also listed in the Design Object List. You can then drag and drop the logical ports onto the pins you wish to assign them to (you will have to zoom in quite a bit to be accurate at this). Alternately, you can type the desired pin number into the LOC field in the Design Browser List. All pin numbers MUST be prefixed by a P. So if you waned to assign logical port A to physical pin 7 you would type P7 in the LOC field. Perform the pin assignment and save your work. 18
  • 20. 5. Once you save the settings, all of the information contained in the GUI interface is written to a *.ucf file (the UCF file will have the same name as the design entity that you are configuring) in your project folder. You can view the contents of the file in a text editor. The pin assignment section will look similar to this: #PACE: Start of PACE I/O Pin Assignments NET "Carry" LOC = "P6" ; NET "Sum" LOC = "P5" ; NET "X" LOC = "P3" ; NET "Y" LOC = "P4" ; You can edit the text file directly if you wish to avoid use of the slower GUI. 6. Implement your design. FPGA Editor FPGA Editor is a graphical application used for displaying and configuring FPGAs. This tool allows you to perform a variety of functions on your designs including: Place and route critical components before running the automatic place and route tools. Finish placement and routing if the routing program does not completely route your design. 19
  • 21. Add probes to your design to examine the signal states of the targeted device. Create an entire design by hand (advanced users). To view the final implementation of your design, ensure your design module is still selected in the sources window. 1. Expand the Implement Design process. 2. Now expand the Place & Route process. 3. Double click View/Edit Routed Design (FPGA Editor). This will launch the FPGA Editor. The FPGA Editor workspace includes three windows: window, and World window. Array window, List The Array window displays a graphical representation of the FPGA device. The device components and the interconnections (both logical and routed) between these components are displayed in this window. When you zoom out at maximum you should be able to see all of the 1176 configurable logic blocks (CLB) in the Spartan II FPGA. The List window displays a list of the components, nets, layers, paths, macros, and constraints in your design. Use the pull-down list box at the top of the window to specify the items you want displayed in the List window. 20
  • 22. The World window shows the area of the device that is currently displayed in the Array window. As you pan and zoom the Array window, notice the corresponding changes in the size and position of the white rectangle within the World window. Also, any objects selected in the Array window appear in the World window. You can drag the white rectangle with the mouse button to pan the display to the desired position. 1. Zoom in into the Array and take a closer look to the CLB’s. When you zoom in, the white rectangle in the world window will indicate your position in the FPGA. 2. In the List window, select the component Sum_OBUF of type SLICE. This is the slice of the CLB in which the HA has been implemented. Each CLB is made up of two identical slices. Our HA is so small that it fits in a single slice. 3. Notice that a small red square appears on the World window when you click on Sum_OBUF. This small square shows the exact location of the slice. Drag the white rectangle until it covers the slice. A magnified view of the slice will be available in the Array window. Zoom in until you can see the slice and its connections to the FPGA pins. 21
  • 23. 4. Double click on the red CLB Slice in the Array window. A new window will appear showing the internal architecture of the CLB slice and the resources that were used to implement your design will be highlighted. 22
  • 24. Any design that you implement in this lab must be realizable in these standard blocks. Although not all FPGA architectures are identical, they are all based upon some regular array of basic building blocks. So any design you ever build using an FPGA will eventually be implemented into standard blocks. Understanding the physical architecture you are targeting will allow you to maximize your design efficiency. The following is an excerpt from the Spartan-II 2.5V Family Functional Description (posted in the lab website): The basic building block of the Spartan II CLB is the Logic Cell. Each LC includes a 4-input function generator, carry logic, and a storage element. Output from the function generator in each LC drives the CLB output and the D input of the flip-flop. Each Spartan II CLB contains four LCs, organized in two similar slices. Spartan II function generators are implemented as 4-input Look-Up Tables. In addition to operating as a function generator, each LUT can provide 16x1-bit synchronous RAM. Furthermore, the two LUTs within a slice can be combined to create 16x2-bit or 32x1-bit synchronous RAM, or a 16x1-bit dual-port synchronous RAM. Storage elements in the Spartan II slice can be configured either as edge-triggered D-type flip-flops or as level sensitive latches. The D inputs can be driven either by function generators in within the slice of directly from slice inputs, bypassing the function generators. Demonstration (2 marks) For Lab 0, show that you are familiar with the FPGA Editor to the Lab Instructor or a TA. This demonstration is worth 20% of Lab 0. 23
  • 25. 24

×