I want to build on the design created in Workshop 4 of the Summer of FPGA series with the Ultra96-V2. That workshop created a design to use the UART click board and a PWM IP to drive the backlight on a LCD Mini Click board. The foundation of what follows is that design, and I'm assuming that if you are following along, you have that in place and you can fill in the simple blanks without me describing them in detail. I have additional useful posts associated with that workshop:
Summer of FPGA, Workshop 4: Elaborating on creating custom IP
EDIT 17th December 2021: I wrote my own driver code, but MikroElektronika have now released a C library to drive this click board: https://github.com/MikroElektronika/mikrosdk_click_v2/tree/master/clicks/lcdmini
Introduction
The LCD Mini Click is a 2 line LCD from Mikroe which plugs in to a Click Mezzanine which, in turn, clicks into the 40-pin low speed header on the Ultra96-V2 board. In this post, part one, I'm going to create a test build that will create a SPI output via the PL on the Ultra96-V2. The intention isn't to drive the LCD Mini Click but to ensure I have a working hardware build and test application that will generate an SPI signal.
In Part Two, to follow, I'm going to extend the workshop 4 design with the work accomplished here to actually drive the LCD through its SPI interface via the PL on the Ultra96-V2. The PL design created here will be incorporated and the test application extended to actually include the right commands to operate the LCD Mini Click and display information on it.
Schematics
It's worth checking out the schematics to determine how the Ultra96-V2, Click Mezzanine and LCD Mini Click will interface at the pin level.
LCD Click Mini
https://download.mikroe.com/documents/add-on-boards/click/lcd-mini/lcd-mini-click-schematic-v101.pdf PDF document
The Click Board uses SPI to interface so the MikroBus pins I'm interested in are CS, SCK and MOSI. The PWM pin is already covered by the existing design so I'm not exercising that here. The LCD Mini Click contains a digital potentiometer to control the LCD contrast and this can also be set through the SPI interface with its Chip Select is connected to the AN pin on the MikroBus, but see limitations below.
Click Mezzanine
It's a bit small when viewed as an image but the header J1 connects to the Low Speed Header on the Ultra96-V2 board. The pins on the header are all passed through a level shifter before they are used by the parts on the Mezzanine Board. I will be using the LCD Mini Click on MikroBus slot 1 so the pins I'm interested in are SPI_CS1, SPI_SCK, SPI_MOSI. Tracing these along the bus through the level shifter, these map to SPIO_CS, SPIO_SCLK and SPIO_DOUT which are pins 12, 8 and 14 on header J1. Note I haven't traced MB1_AN back due to limitations covered below.
Ultra96-V2
https://www.element14.com/community/docs/DOC-92273/l/ultra96-v2-rev1-schematic Available on the Element 14 site but you must download it to see all the information. The on-line viewer doesn't display all the pin identifiers for example. What I'm particularly interested in is the Low Speed Header and the Banks used to connect to the PL.
{gallery} Ultra96-V2 Pin Mappings |
---|
Low Speed Header, J5: This matches up to the header J1 on the Click Mezzanine. |
Bank 501: Contains the LS Header to PS/PL mapping. All the expected LS pins are on this bank but I'm going to find that this won't work! |
Bank 26: Contains additional LS Header PS/PL mappings. I'm going to find that I will need this! |
Bank 501/Bank 26 Power Rail: Identifies the power rail used, VCCO_PSIO1_501 -> +VCC_PSAUX and VCCO_26 -> +VCCAUX |
Power Rail VCCPSAUX: +1.8V |
Power Rail VCCAUX: +1.8V |
Limitations
The LCD Mini Click has the ability to control contrast through the on-board digi-pot with its chip select pin connected to AN on the MikroBus. The AN pins however are not accessible through the Click Mezzanine header, J1. It might be tempting to think that SPIO_CS2 is used, as CS2 is the label given on the LCD Mini Click schematic. However that is connected to the Click Mezzanine's on-board ADC (just below MikroBus slot 2 on the schematic) to select it, and the AN pins, MB1_AN and MB2_AN are the analog inputs for conversion. Thus, the Click Mezzanine board cannot be used to access the AN pins on the MikroBus slots for anything except Analog In from an attached Click Board. In fact, there is no means at all for the Click Mezzanine to pass through an analog input from a Click Board. Clearly an assumption in the design of the Click Mezzanine is that the AN pins will only be used for, e.g., Sensor Click boards, and no downstream board would want/need access to an analog input. In some respects that's ok as it can get converted on-board, however, it makes no sense to not provide access to the MB_AN pins for digital purposes, e.g. CS2 and CS3.
So no controlling the display contrast on the LCD.
Datasheets
Useful for understanding development.
The LCD is a Topway LMB162XFW module and a readable data sheet is available here.
The LCD is driven through a MCP23017 port expander, data sheet available here. This indicates the maximum clock frequency is 10MHz.
Block Design
For SPI, I want to access through PL, so I'm going to use an AXI Quad SPI IP block and to get going I will create a new, test solution. I can then incorporate it back into the original design when working. So, create a new Vivado project and create a new block design, drop a Zynq IP and run the board automation. Next, drop a AXI Quad SPI IP block on the design and double-click to customise it.
I will want to operate the LCD Mini Click as a slave, the only one, in standard mode, so this IP will be configured as a Master. The frequency ratio is a 'divider' for the input clock in order to generate a correct output clock (SCK) frequency. I'm setting it to 8 because I will add a clock wizard to generate an input clock as a ratio of the Zynq PL clock and this value coupled with the clock wizard clock will give me the frequency I need.
Drop a Clock Wizard onto the design and double click to customise it. The Clock frequency for the LCD Mini Click is a maximum of 10MHz but for my purposes I'm going to run it at half that so that I can avoid any 'edge' issues while I'm testing, at least that's my hope! I want to take the 100MHz Zynq bus frequency down to 40MHz thus when it is divided by the AXI QSPI Frequency Ratio of 8, I end up with 5MHz:
{gallery} AXI QUAD SPI Customisation |
---|
Clocking Options: Select PLL. The Product Guide has useful information about the IP. |
Output Clocks: Set to 40MHz. The reset pin needs to be active low to be driven from the Processor System Reset. |
Block Design
It's easier to connect the clk_out1 pin to the ext_spi_clk pin manually, then run connection automation which should hook up the rest of the pins.
This is what I ended up with. I also renamed the output pin to spi from spi_rtl.
Setting Constraints
Next, I need to map the SPI output pins to the header pins. First step is to create a constraints file: under the existing constraints set, create a constraints file called spi_test_constraints and in the Design Hierarchy, set it as a target
Then create the HDL Wrapper, run synthesis and open the Elaborated Design under the Flow Navigator. This will display a schematic with the name of the SPI pins that need constraining: these are described in the AXI Quad SPI Product Guide in table 2.2 on page 19.
Pin Mappings for Constraints
Using the information from the schematics linked above, I can derive the Pin and IO Standards for the constraints:
AXI QSPI Pin | Function | Click Mezzanine Header J5 Pin | Low Speed Header | Bank | PL Pin |
---|---|---|---|---|---|
spi_ssio[0:0] | CS | 12 | MIO41_SPIO_CS | Bank 501 | B10 |
spi_sck_io | SCK | 8 | MIO38_SPIO_SCLK | Bank 501 | C9 |
spi_io0_io | MOSI | 14 | MIO43_SPIO_MOSI | Bank 501 | E13 |
spi_i01_i0 | MISO | 10 | MIO42_SPIO_MISO | Bank 501 | D12 |
All these pins are on bank 501 so the IO Standard will be 1.8V. I don't need a MISO pin as I won't be reading anything from the LCD Mini Click but I still need to provide a constraint. On the menu bar, select IO Planning to add an I/O Ports tab to the bottom of the view and expand it to find the 4 ports to be constrained.
Now I have a problem! The defined pins on the LS Header for the SPI functions I need are already constrained to PS and I can't select them to connect the PL SPI pins. To get around this, I'm going for a bit of a bodge which I hope will ultimately work. I'll connect to alternate LS pins, not used by the Click Mezzanine, and cross-connect them with hook up wire to the pins used by the LCD Mini - fortunately the Click Mezzanine provides access to the LS Header from the top of the board. However, the pins are 0.2mm diameter so standard hook up wire won't fit; fortunately, 26AWG twisted pair does which I have to hand.
I can find suitable pins on the LS Header that are connected to Bank 26 and available to connect to the PL SPI pins:
AXI QSPI Pin | Function | Click Mezzanine Header J5 Pin | Cross-Connect to J5 Pin | Low Speed Header | Bank | PL Pin |
---|---|---|---|---|---|---|
spi_ssio[0:0] | CS | 16 | 12 | HD_GPIO_9 | Bank 26 | E6 |
spi_sck_io | SCK | 22 | 8 | HD_GPIO_10 | Bank 26 | D5 |
spi_io0_io | MOSI | 18 | 14 | HD_GPIO_11 | Bank 26 | E5 |
spi_io1_io | MISO | 20 | 10 | HD_GPIO_12 | Bank 26 | D6 |
All these pins are on Bank 26 so the IO Standard will be 1.8V. The I/O Ports constraints will look like this:
And the constraints file contains:
set_property PACKAGE_PIN E6 [get_ports {spi_ss_io[0]}] set_property PACKAGE_PIN E5 [get_ports spi_io0_io] set_property PACKAGE_PIN D6 [get_ports spi_io1_io] set_property PACKAGE_PIN D5 [get_ports spi_sck_io] set_property IOSTANDARD LVCMOS18 [get_ports {spi_ss_io[0]}] set_property IOSTANDARD LVCMOS18 [get_ports spi_io0_io] set_property IOSTANDARD LVCMOS18 [get_ports spi_io1_io] set_property IOSTANDARD LVCMOS18 [get_ports spi_sck_io]
Close the Elaborated Design.
Final Vivado Steps
That should be it. Generate the output products, generate the bitstream and export the Hardware.
Create the Application in Vitis
In Vitis, I'll create a new Platform Project, SPI_Test_App, and import the XSA exported from Vivado, targeting a standalone OS on the A53 PSU. In the Board Support Package, I always set the UARTs to psu_uart_1 for the FSBL, Standalone and PMU. I also build the Platform to save a bit of time later.
Sanity Test
I'm going to run a loopback test to perform a basic sanity test on the implementation. Xilinx provide an example application for this called xspi_low_level_example:
This example writes out a number of bytes and then reads them back again, performing a comparison to make sure they are equivalent. Adding this example, building it and then debugging it - I always create a debug configuration and launch that, breaking at main - gives me a positive result.
Not a lot of detail issued, but following the code through, it would only print that success message if the write/read comparison was equivalent.
Scope Test
Next, I want to run a test that transmits some data so that I can see it on a scope. For this, I'm going to use the xspi_polled_example, modified to suit my purposes. Out of the box, this example performs a self test to make sure that the hardware is built correctly, then performs a loopback test. I'm modifying it to perform the self test, delete the loopback test, then output some data to Slave 0 (as configured in hardware) 10 times. Having built this and run it under debug I see the following:
Success. The following images are what were captured on the scope:
{gallery} SPI Signals |
---|
Board Connections: a rather Heath-Robinson affair, but it seems to work. |
Captured Signals: channel 1, yellow, is CS; channel 2, magenta is MOSI; channel 3, cyan, is SCK |
Captured Signals: Dropped the CS channel, showing more of the data and clock signals. |
Captured Signals: Showing even more of the data and clock signals. |
This looks ok, a fair bit of ringing but I would expect that given the speed and the connection used. At least it shows my idea of using GPIO pins to cross connect to the SPI pins on the Click Mezzanine has legs (or at least can crawl!)
Test Code
It's well commented but if something isn't clear, please ask. The main steps are:
- Initialise and configure the SPI Driver (lines 141 - 157)
- Run a self test to check the hardware (lines 159 - 168) Essentially, this will reset the device to a known state and then run a loopback test. It's a driver provided function.
- Configure the SPI device as a Master and the Slave as Manual Select. This means that the CS line will not be dropped during transmission but will remain active throughout (lines 171 - 181)
- Select the Slave to transmit to (lines 183 - 193) The driver will drive the connected CS wire active.
- Transmit the data (lines 215 - 226) I do this 10 times but it's not really necessary for this test.
The SPI driver takes care of driving the wires based on the configured hardware so actually using it is very simple from a programming point of view. Must be, as I've never used SPI before and coupled with my beginner knowledge of FPGAs I'm amazed to get it running with little difficulty.
/****************************************************************************** * Copyright (C) 2008 - 2021 Xilinx, Inc. All rights reserved. * SPDX-License-Identifier: MIT ******************************************************************************/ /*****************************************************************************/ /** * @file xspi_polled_example.c * * * This file contains a design example using the Spi driver (XSpi) and the Spi * device using the polled mode. * * To put the driver in polled mode the Global Interrupt must be disabled after * the Spi is Initialized and Spi driver is started. * * This example works with a Cortex A53 processor on an Ultra96-V2 board. Altered * from the original example to output only with a test signal to prove the SPI * configuration is correct. * ******************************************************************************/ /***************************** Include Files *********************************/ #include "xparameters.h" /* XPAR parameters */ #include "xspi.h" /* SPI device driver */ #include "xspi_l.h" #include "xil_printf.h" #include "sleep.h" /************************** Constant Definitions *****************************/ /* * The following constants map to the XPAR parameters created in the * xparameters.h file. They are defined here such that a user can easily * change all the needed parameters in one place. */ #define SPI_DEVICE_ID XPAR_SPI_0_DEVICE_ID /* * This is the size of the buffer to be transmitted in this example. */ #define BUFFER_SIZE 12 /* * The following constant defines the slave select signal that is used to * to select the slave device on the SPI bus, this signal is connected * to the chip select of the device */ #define SLAVE_SPI_SELECT 0x01 /**************************** Type Definitions *******************************/ /* * The following data type is used to send data on the SPI * interface. */ typedef u8 DataBuffer[BUFFER_SIZE]; /***************** Macros (Inline Functions) Definitions *********************/ /************************** Function Prototypes ******************************/ int SpiPolledExample(XSpi *SpiInstancePtr, u16 SpiDeviceId); /************************** Variable Definitions *****************************/ /* * The instances to support the device drivers are global such that they * are initialized to zero each time the program runs. */ static XSpi SpiInstance; /* The instance of the SPI device */ /* * The following variables are used to read and write to the Spi device, they * are global to avoid having large buffers on the stack. */ u8 WriteBuffer[BUFFER_SIZE]; /*****************************************************************************/ /* * Main function to call the Spi Polled example. * * * @return XST_SUCCESS if successful, otherwise XST_FAILURE. * * @note None * ******************************************************************************/ int main(void) { int Status; xil_printf("Spi polled Example Start Test.\r\n"); /* * Run the Spi Polled example. */ Status = SpiPolledExample(&SpiInstance, SPI_DEVICE_ID); if (Status != XST_SUCCESS) { xil_printf("Spi polled Example Failed\r\n"); return XST_FAILURE; } xil_printf("Successfully ran Spi polled Example\r\n"); return XST_SUCCESS; } /*****************************************************************************/ /** * * This function does a minimal test on the Spi device and driver as a * design example. The purpose of this function is to illustrate how to use * the XSpi component using the polled mode. * * This function sends data only. * * * @param SpiInstancePtr is a pointer to the instance of Spi component. * @param SpiDeviceId is the Device ID of the Spi Device and is the * XPAR_<SPI_instance>_DEVICE_ID value from xparameters.h. * * @return XST_SUCCESS if successful, otherwise XST_FAILURE. * * @note * * This function contains an infinite loop such that if the Spi device is not * working it may never return. * ******************************************************************************/ int SpiPolledExample(XSpi *SpiInstancePtr, u16 SpiDeviceId) { int Status; u32 Count; u8 Test; XSpi_Config *ConfigPtr; /* Pointer to Configuration data */ /* * Initialize the SPI driver so that it is ready to use. */ xil_printf("Initialising the SPI Driver..."); ConfigPtr = XSpi_LookupConfig(SpiDeviceId); if (ConfigPtr == NULL) { xil_printf("...Failed, not found\r\n"); return XST_DEVICE_NOT_FOUND; } Status = XSpi_CfgInitialize(SpiInstancePtr, ConfigPtr, ConfigPtr->BaseAddress); if (Status != XST_SUCCESS) { xil_printf("...Failed, not initialised\r\n"); return XST_FAILURE; } xil_printf("...Initialised\r\n"); /* * Perform a self-test to ensure that the hardware was built correctly. */ xil_printf("Running self test..."); Status = XSpi_SelfTest(SpiInstancePtr); if (Status != XST_SUCCESS) { xil_printf("...Failed.\r\n"); return XST_FAILURE; } xil_printf("...Passed\r\n"); /* * Set the Spi device as a master and manual slave select. */ xil_printf("Setting SPI options for test..."); Status = XSpi_SetOptions(SpiInstancePtr, XSP_MASTER_OPTION | XSP_MANUAL_SSELECT_OPTION); if (Status != XST_SUCCESS) { xil_printf("...Failed to set options.\r\n"); return XST_FAILURE; } xil_printf("...Options set.\r\n"); /* * Select the slave on the SPI bus so that it can be * written using the SPI bus */ xil_printf("Selecting Slave on 0.."); Status = XSpi_SetSlaveSelect(SpiInstancePtr, SLAVE_SPI_SELECT); if (Status != XST_SUCCESS) { xil_printf("...Failed to select slave.\r\n"); return XST_FAILURE; } xil_printf("...Slave selected.\r\n"); /* * Start the SPI driver so that the device is enabled. */ XSpi_Start(SpiInstancePtr); /* * Disable Global interrupt to use polled mode operation */ XSpi_IntrGlobalDisable(SpiInstancePtr); /* * Initialize the write buffer with pattern to write. * Test value that is added to the unique value allows the value to be * changed in a debug environment. */ Test = 0x10; for (Count = 0; Count < BUFFER_SIZE; Count++) { WriteBuffer[Count] = (u8)(Count + Test); } /* * Transmit the data 10 times . */ xil_printf("Transmitting starts\r\n"); for (Count = 0; Count < 10; Count++) { xil_printf("Transmitting "); xil_printf("%d\r\n", Count); XSpi_Transfer(SpiInstancePtr, WriteBuffer, NULL, BUFFER_SIZE); usleep_A53(500000); // Sleep for 0.5 seconds } xil_printf("Transmitting ends\r\n"); /* * Deselect the Slave and stop the SPI driver so that the device is disabled. */ XSpi_SetSlaveSelect(SpiInstancePtr, 0x0); XSpi_Stop(SpiInstancePtr); return XST_SUCCESS; }
Next Steps
I appear to have a successful PL build and some test code that I can build on. So, my plan next is to incorporate it into the Workshop 4 design and actually start sending commands to the LCD.
Part Two extends on the work in this post and creates an application that actually drives the LCD.
Top Comments