The goal for this project is to get the display, touch screen, and sd card working on the Pi Pico by calibrating the touchscreen and printing the contents of the sdcard to serial console.
Learning how to share multiple SPI peripherals reduces the amount of pins you need which can increase the amount of available pins for other uses. A SPI bus is very similar to an I2C bus except the SPI peripheral has a unique chip select pin assigned to it. I find it easier to think of the chip select pin as a SPI address pin. You can only have 1 address per device with I2C and the same holds true with SPI devices except the address is a physical pin.
Because SPI peripherals require a physical pin you will be limited to how many you can have based on how many free pins your microcontroller has. What SPI lacks in chain ability it makes up for with speed.
- I2C devices are half-duplex
- SPI devices are full duplex
Because SPI communication is twice as fast as I2C it makes far more sense to use SPI for displays and sd cards! Don't get me wrong, I2C definitely has its place for uses such as:
- temp sensors
- optical sensors
- 7-Segment displays
- 14-Segment displays
- small 16x9 matrix LED modules
- Neopixel strips
- and chaining a ton of devices on 1 bus without the limitation of physical pins per device.
When it comes to needing faster communication for a display that has 480x320 (153,600 total) pixels or sd card reading & writing for a single device that's where the SPI protocol outperforms I2C.
I recently worked on a project with a Raspberry Pi Pico that needed to have a Touchscreen TFT & SD Card. That's actually 3 peripherals because the touchscreen controller chip requires pins too. In the majority of scenarios when you have a SPI touchscreen you actually have 2 SPI devices, the display and the touchscreen.
In the Raspberry Pi Pico pinout diagram there are 2 separate SPI buses. SPI0 and SPI1. SPI0 peripherals cannot communicate with peripherals on the SPI1 bus and vice versa. A peripheral would be anything on the SPI protocol such as a display, sdcard, temp sensor, etc... They are highlighted below with magenta labels. Please notice that each SPI bus is prepended with SPI0 or SPI1.
Here is the wiring setup using an Adafruit Proto Picow Doubler. The doubler offers a maximum of 3 input headers per physical pin. That means you can share up to 3 SPI devices per pin with their doubler. This is a very handy feature as I didn't have to use a breadboard to prototype this project, very cool. The reset button also came in handy during prototyping. If you have a Pi Pico I highly recommend getting one of these. They're like Feather doublers but for the Pico.
It's almost impossible to follow wire routing from a picture so here is a diagram of the pinout I'm using.
I'm using a Chinese 3.5" ST7796 TFT with built-in SDCard. You can use an Adafruit 3.5" TFT Featherwing with minimal code changes.
Here is the back of the Chinese TFT. This is typical of TFT's with touchscreens and built-in SD Cards. There are a lot of pins. If you're a beginner it might seem daunting at first and that's ok, learning this stuff takes time.
They mix and match their silkscreen labeling which can be easily confusing. For example T_DIN and T_DO are the same thing as MOSI and MISO. All you need to know is that T_DIN = MOSI & T_DO = MISO. The reason for this is because MOSI & MISO naming conventions are being phased out in favor of DIN & DOUT, mixing the silkscreen naming conventions on 1 PCB is abnormal.
You'll often see touchscreen silkscreen labels use a naming convention of T, TS, or Touch. You can use the IRQ for touch like the Adafruit 3.5" TFT Featherwing does, but I'm not doing that here, I'm not using the IRQ pin at all.
You can optionally choose to wire the LED/LITE pin to a PWM pin on the Pi Pico for brightness control. To always have the display at full brightness I'm connecting the LED pin directly to 3.3V OUT on the Pi Pico.
The goal with SCK, MOSI, and MISO for SPI bus sharing is to splice all SCK pins together, all MOSI pins together, and all MISO pins together. Since I'm using a doubler there is no literal splicing of wires, just using multiple headers to the same pin, the same as you would do with a breadboard.
- GP2 (SCK)
- GP3 (MOSI)
- GP4 (MISO)
Each peripheral is then given its own unique physical pin for chip select addressing.
- GP21 (TFT CS)
- GP17 (SD CS)
- GP14 (TS CS)
You might notice that TS_CS is on SPI bus 1 not 0 with the rest of the pins. You can put CS pins anywhere but they must have a unique pin that cannot be shared. Since I'm using SD_CS on GP17 which is SPI0CS then I cannot use any other magenta pin with the same SPI0CS label. So I chose a random pin on SPI1.
Data Command (DC) can be on any free pin and TFT Reset is optional. The TFT screen can still reset via software command using Data Command (DC), so it's not completely necessary to use a hardware reset pin.
You might be tempted to hookup VCC or LITE to the 5V VBUS pin. It's a better idea in a 3.3V logic system to use the 3.3V OUT pin especially with a display that has no datasheet or schematics. Most Circuit Python microcontrollers are 3.3V logic. The 3.3V OUT pin is power only, there is no data going through a power pin.
Watch out for the 3.3V EN pin right above 3.3V pin. Many beginners mistakenly use 3.3V EN instead of the 3.3V OUT pin. If you want 3.3V power then use the 3.3V OUT pin.
The RUN pin is nice for soldering a toggle switch for a reset button. In fact that's how the reset pin works on the Adafruit Picow Bell Doubler... and any other add-on board with a reset button for the Pi Pico. You can test it by taking a piece of wire, touching one end to the RUN pin, and the other end to any GND pin. It will reset the Pico... which is always a good thing to know.
Now that we've got all the learning steps out of the way we can play with some code!
# SPDX-FileCopyrightText: 2024 DJDevon3 # SPDX-License-Identifier: MIT # Coded for Circuit Python 9.x """Raspberry Pi Pico with Touchscreen TFT & SDCard Example""" import time import os import board import busio import terminalio import displayio from adafruit_display_text import label import adafruit_imageload import sdcardio import storage from circuitpython_st7796s import ST7796S from circuitpython_xpt2046 import Touch displayio.release_displays() spi = busio.SPI(board.GP2, MOSI=board.GP3, MISO=board.GP4) tft_cs = board.GP21 tft_dc = board.GP22 tft_rst = board.GP20 ts_cs = board.GP14 sd_cs = board.GP17 # Initialize SPI SDCard prior to other SPI peripherals! try: print("Attempting to mount sd card") sdcard = sdcardio.SDCard(spi, sd_cs) vfs = storage.VfsFat(sdcard) virtual_root = "/sd" storage.mount(vfs, virtual_root) print(os.listdir("/sd")) except Exception as e: print("no sd card:", e) print("continuing") # 4.0" ST7796S Display DISPLAY_WIDTH = 480 DISPLAY_HEIGHT = 320 DISPLAY_ROTATION = 180 # Touch calibration TOUCH_X_MIN = 100 TOUCH_X_MAX = 1996 TOUCH_Y_MIN = 100 TOUCH_Y_MAX = 1996 display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_rst) display = ST7796S(display_bus, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, rotation=DISPLAY_ROTATION) # Instantiate the touchpad touch = Touch( spi=spi, cs=ts_cs, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, rotation=DISPLAY_ROTATION, x_min=TOUCH_X_MIN, x_max=TOUCH_X_MAX, y_min=TOUCH_Y_MIN, y_max=TOUCH_Y_MAX, ) # Quick Colors for Labels TEXT_BLACK = 0x000000 TEXT_BLUE = 0x0000FF TEXT_CYAN = 0x00FFFF TEXT_GRAY = 0x8B8B8B TEXT_GREEN = 0x00FF00 TEXT_LIGHTBLUE = 0x90C7FF TEXT_MAGENTA = 0xFF00FF TEXT_ORANGE = 0xFFA500 TEXT_PURPLE = 0x800080 TEXT_RED = 0xFF0000 TEXT_WHITE = 0xFFFFFF TEXT_YELLOW = 0xFFFF00 def make_my_label(font, anchor_point, anchored_position, scale, color): func_label = label.Label(font) func_label.anchor_point = anchor_point func_label.anchored_position = anchored_position func_label.scale = scale func_label.color = color return func_label instructions_label = make_my_label( terminalio.FONT, (0.5, 0.5), (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 4), 2, TEXT_WHITE ) x_min_label = make_my_label( terminalio.FONT, (0.0, 0.0), (10, DISPLAY_HEIGHT/2), 2, TEXT_WHITE ) x_max_label = make_my_label( terminalio.FONT, (1.0, 0.0), (DISPLAY_WIDTH - 10, DISPLAY_HEIGHT/2), 2, TEXT_WHITE ) y_min_label = make_my_label( terminalio.FONT, (0.5, 0.0), (DISPLAY_WIDTH / 2, 10), 2, TEXT_WHITE ) y_max_label = make_my_label( terminalio.FONT, (0.5, 1.0), (DISPLAY_WIDTH / 2, DISPLAY_HEIGHT - 10), 2, TEXT_WHITE ) sprite_sheet, palette = adafruit_imageload.load( "icons/rocket.bmp", bitmap=displayio.Bitmap, palette=displayio.Palette, ) palette.make_transparent(0) rocket_icon = displayio.TileGrid( sprite_sheet, pixel_shader=palette, x=DISPLAY_WIDTH - 80, y=2, width=1, height=1, tile_width=80, tile_height=82, default_tile=0, ) # Create Display Groups main_group = displayio.Group() main_group.append(rocket_icon) main_group.append(x_min_label) main_group.append(x_max_label) main_group.append(y_min_label) main_group.append(y_max_label) main_group.append(instructions_label) display.root_group = main_group print("Go Ahead - Touch the Screen - Make My Day!") x = y = 0 x_min = y_min = x_max = y_max = min(DISPLAY_WIDTH, DISPLAY_HEIGHT) // 2 x_min_label.text = f"X-Min:{x_min}" x_max_label.text = f"X-Max:{x_max}" y_min_label.text = f"Y-Min:{y_min}" y_max_label.text = f"Y-Max:{y_max}" instructions_label.text = f"draw swirlies on corners to calibrate" while True: x = touch.raw_touch() if x is not None: x_min = min(x_min, x[0]) x_max = max(x_max, x[0]) y_min = min(y_min, x[1]) y_max = max(y_max, x[1]) print(f"(({x_min}, {x_max}), ({y_min}, {y_max}))") x_min_label.text = f"X-Min:{x_min}" x_max_label.text = f"X-Max:{x_max}" y_min_label.text = f"Y-Min:{y_min}" y_max_label.text = f"Y-Max:{y_max}" time.sleep(0.05)
After wiring and code it should present a test calibration screen. If you scroll a stylus (or fingernail) across the screen it will display calibration values for you to plug in at the top of the script for more accurate touch results.
Here's a serial output example
Attempting to mount sd card ['System Volume Information', 'Github_Avatars'] # <-- Showing SD Card contents Go Ahead - Touch the Screen - Make My Day! ((160, 1883), (160, 240)) ((160, 1907), (160, 240)) ((160, 1949), (160, 240)) ((160, 1949), (160, 240)) ((160, 1956), (160, 240)) ((160, 1956), (160, 240)) ((160, 1956), (160, 240)) ((160, 1956), (160, 240)) ((160, 1956), (160, 240))
We now have the SD Card contents printed to serial and touchscreen calibration. Everything works!
Why is knowing how to share a SPI bus with peripherals such a big deal?
You will eventually come upon a situation where a microcontroller doesn't have enough pins for your project, you will run out of pins someday, and when that happens I hope you remember that you can share SCK, MOSI, and MISO lines for SPI devices.
What is the least amount of pins needed for a SPI display, SPI Touchscreen, and SPI SD Card?
9 pins
- 1 physical pin for 3V3 power
- 1 physical pin for common ground
- 1 physical pin for TFT Data Command
- 1 physical pin for all MOSI's
- 1 physical pin for all MISO's
- 1 physical pin for all SCK's
- 3 physical pins for chip select for the TFT, Touchscreen, SD Card
You can accomplish this same project with an Adafruit QT Py and have 4 GPIO pins left over including I2C for Stemma capable sensors and devices.
Now you can see why it's important to learn this. Yes, you can run all of this on a QT Py with creative use of SPI bus sharing. :)
Now that the display, touchscreen, and sd card are working you're ready to embark upon bigger and better projects!
If you have any questions please seek support in the Adafruit Forums or Adafruit Discord.
Github Repository: DJDevon3's Rasberry Pi Pico Touchscreen & SDCard Example