11. SPI
11.1 What is SPI
SPI (Serial Peripheral Interface) is a synchronous serial communication protocol used for data transmission between a microcontroller and external devices. It consists of one master device (usually a microcontroller MCU) and one or more slave devices, that is, a one-master-multi-slave mode. It is usually used for short-distance, high-speed, full-duplex communication and is widely used in many embedded systems and electronic devices.
11.2 SPI Hardware Interface
SPI mainly uses 4 lines: the clock line (SCLK), the Master Output Slave Input line (MOSI), the Master Input Slave Output line (MISO), and the Chip Select line (CS).
- The master device sends data to the slave device through the MOSI line. In each clock cycle, the master device sends one bit to the MOSI line, and the slave device reads that bit in the next clock cycle.
- The slave device sends data to the master device through the MISO line. In each clock cycle, the slave device sends one bit to the MISO line, and the master device reads that bit in the next clock cycle.
- Data transmission can be full-duplex, that is, the master device and slave device can send and receive data at the same time.
- The length of data transmission can be variable, usually in bytes.
- Data transmission can be unidirectional, that is, the master device only sends data or only receives data.
- Data transmission can be multi-master, that is, multiple master devices can communicate with multiple slave devices.
The master device selects the slave device to communicate with through the chip select line. Each slave device has a chip select line. When the chip select line is low, it indicates that the slave device is selected. (Some devices are active high; please refer to their datasheet to determine.) The master device synchronizes data transmission by controlling the level of the clock line. The rising and falling edges of the clock line are used to control data transmission and sampling.
The master-slave wiring of SPI is similar to that of a serial port, requiring the transmit and receive lines to be cross-connected.
11.3 SPI Mode Selection
The SPI protocol defines multiple transmission modes, also known as SPI modes or timing modes, used to control the order in which data is transmitted under the clock signal and the way data is sampled. The transmission mode of SPI is mainly determined by two parameters: Clock Polarity (CKPL) and Clock Phase (CKPH).
- Clock Polarity (CKPL): Clock polarity defines the level of the clock signal in the idle state.
CKPL = 0: The clock signal is low in the idle state.
CKPL = 1: The clock signal is high in the idle state.
- Clock Phase (CKPH): The phase defines on which edge of the clock signal data sampling and updating occur.
CKPH = 0: Data sampling occurs on the first edge of the clock, and data updating occurs on the second edge.
CKPH = 1: Data sampling occurs on the second edge of the clock, and data updating occurs on the first edge.
The following are common SPI modes:
Mode 0 (CKPL=0, CKPH=0):- Clock Polarity is 0, meaning the clock idle state is low.
- Clock Phase is 0, meaning data is sampled and stable on the first edge of the clock signal (clock rising edge).
Mode 1 (CKPL=0, CKPH=1):- Clock Polarity is 0, and the clock idle state is low.
- Clock Phase is 1, and data is sampled and stable on the second edge of the clock signal (clock falling edge).
Mode 2 (CKPL=1, CKPH=0):- Clock Polarity is 1, and the clock idle state is high.
- Clock Phase is 0, and data is sampled and stable on the first edge of the clock signal (clock falling edge).
Mode 3 (CKPL=1, CKPH=1):- Clock Polarity is 1, and the clock idle state is high.
- Clock Phase is 1, and data is sampled and stable on the second edge of the clock signal (clock rising edge).
The decision to select an SPI mode usually depends on the specification requirements and communication protocol of the slave device. Different devices may use different modes, so before communicating with a specific slave device, you must understand the SPI mode required by the slave device. If the SPI mode is not explicitly specified, you can usually try the most common Mode 0 or Mode 3 based on the slave device's specification manual or communication protocol. In addition, you need to pay attention to the clock frequency limit of the SPI mode to ensure that the clock frequencies between the master and slave devices match.
11.4 Basic SPI Parameters
The SPI protocol defines a set of parameters that are very important for correctly setting communication parameters and implementing SPI communication.
Basic SPI parameters include:
Clock Polarity (CPOL): Specifies the level of the signal line in the clock idle state. There are two states: high when idle (CPOL=1) or low when idle (CPOL=0);Clock Phase (CPHA): Specifies the moment of data sampling. There are two states: sampling data after the clock rising edge (CPHA=0) or sampling data before the clock falling edge (CPHA=1);Data bits: Specifies the number of bits contained in each SPI data packet (usually 8 bits), and can also be set to smaller or larger values;Transmission mode: Determines how data is transmitted on the SPI bus (e.g., full-duplex, half-duplex, or simplex mode);Clock rate: Specifies the clock rate of the SPI bus, in bits per second (bps);Master/Slave mode: Determines whether the device is a master or a slave on the SPI bus;Transmission order: Specifies the bit transmission order of the data, MSB (most significant bit) first or LSB (least significant bit) first.
These parameters can be set by configuring the SPI control register to ensure correct communication between SPI devices. When determining these parameters, the actual hardware setup and communication requirements should be considered. If you need to use SPI for communication, make sure these parameters are set correctly.
11.5 Software SPI and Hardware SPI
Like IIC, SPI is also divided into software SPI and hardware SPI. The software SPI part will not be discussed here. This chapter focuses on the hardware SPI part.
The ESP32-S3 chip integrates four SPI controllers:
- SPI0
- SPI1
- General-purpose SPI2, i.e., GP-SPI2
- and General-purpose SPI3, i.e., GP-SPI3
The SPI0 and SPI1 controllers are mainly used internally to access external flash and PSRAM. We can only use SPI2 and SPI3.
Hardware SPI supports the following features:
11.6 SPI Usage Process
The process of using the ESP32-S3's SPI (Serial Peripheral Interface) function in MicroPython is as follows:
- Import the relevant modules and libraries:
import machine- Initialize the SPI object:
spi = machine.SPI(sck=machine.Pin(sck_pin_number), mosi=machine.Pin(mosi_pin_number), miso=machine.Pin(miso_pin_number))Where sck_pin_number, mosi_pin_number, and miso_pin_number are the pin numbers of the clock line, transmission line, and receive line connected to the ESP32-S3, respectively. If you do not need to use the MISO (master receives data) function, you can leave the miso pin unspecified.
- Set the SPI clock polarity and phase:c
spi.init(polarity=0, phase=0)1
Use the init() method to set the SPI clock polarity and phase. The default polarity and phase are both 0.
4. Set the SPI mode and word length (optional step):
spi.init(mode=machine.SPI.MODE_MASTER, bits=8)
Use the init() method to set the SPI operating mode and word length. The default operating mode is master mode, and the word length is 8 bits. You can set the appropriate operating mode and word length according to your needs.
5. Send SPI commands (write and read):
```
spi.write(data)
spi.read(nbytes)2
3
4
5
6
7
8
9
10
11
Use the write() method to send data to the slave device. Use the read() method to read data from the slave device and return a bytes object containing the read bytes. If you only want to send data without receiving data, you can use the write_readinto() method.
Note the following points:
- Before using the write() and read() methods, make sure the slave device is correctly connected to the SPI bus.
- If you need to use slave device mode instead of master device mode, you can use the machine.SPI.MODE_SLAVE constant to set the operating mode.
- If you need to use the hardware SPI bus instead of the software SPI bus, you can use the machine.SPI class to initialize a hardware SPI object. This object can directly use the hardware SPI bus without using software simulation.
11.7 SPI Verification
Use a 0.96-inch SPI screen as the case;
Download ssd1306.py to the chip.
import time
import framebuf
# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xa4)
SET_NORM_INV = const(0xa6)
SET_DISP = const(0xae)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xa0)
SET_MUX_RATIO = const(0xa8)
SET_COM_OUT_DIR = const(0xc0)
SET_DISP_OFFSET = const(0xd3)
SET_COM_PIN_CFG = const(0xda)
SET_DISP_CLK_DIV = const(0xd5)
SET_PRECHARGE = const(0xd9)
SET_VCOM_DESEL = const(0xdb)
SET_CHARGE_PUMP = const(0x8d)
class SSD1306:
def __init__(self, width, height, external_vcc):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
# Note the subclass must initialize self.framebuf to a framebuffer.
# This is necessary because the underlying data buffer is different
# between I2C and SPI implementations (I2C needs an extra byte).
self.poweron()
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00, # off
# address setting
SET_MEM_ADDR, 0x00, # horizontal
# resolution and layout
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0
SET_MUX_RATIO, self.height - 1,
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
SET_DISP_OFFSET, 0x00,
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
# timing and driving scheme
SET_DISP_CLK_DIV, 0x80,
SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1,
SET_VCOM_DESEL, 0x30, # 0.83*Vcc
# display
SET_CONTRAST, 0xff, # maximum
SET_ENTIRE_ON, # output follows RAM contents
SET_NORM_INV, # not inverted
# charge pump
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01): # on
self.write_cmd(cmd)
self.fill(0)
self.show()
def poweroff(self):
self.write_cmd(SET_DISP | 0x00)
def contrast(self, contrast):
self.write_cmd(SET_CONTRAST)
self.write_cmd(contrast)
def invert(self, invert):
self.write_cmd(SET_NORM_INV | (invert & 1))
def show(self):
x0 = 0
x1 = self.width - 1
if self.width == 64:
# displays with width of 64 pixels are shifted by 32
x0 += 32
x1 += 32
self.write_cmd(SET_COL_ADDR)
self.write_cmd(x0)
self.write_cmd(x1)
self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0)
self.write_cmd(self.pages - 1)
self.write_framebuf()
def fill(self, col):
self.framebuf.fill(col)
def pixel(self, x, y, col):
self.framebuf.pixel(x, y, col)
def scroll(self, dx, dy):
self.framebuf.scroll(dx, dy)
def text(self, string, x, y, col=1):
self.framebuf.text(string, x, y, col)
class SSD1306_I2C(SSD1306):
def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
self.i2c = i2c
self.addr = addr
self.temp = bytearray(2)
# Add an extra byte to the data buffer to hold an I2C data/command byte
# to use hardware-compatible I2C transactions. A memoryview of the
# buffer is used to mask this byte from the framebuffer operations
# (without a major memory hit as memoryview doesn't copy to a separate
# buffer).
self.buffer = bytearray(((height // 8) * width) + 1)
self.buffer[0] = 0x40 # Set first byte of data buffer to Co=0, D/C=1
self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height)
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.temp[0] = 0x80 # Co=1, D/C#=0
self.temp[1] = cmd
self.i2c.writeto(self.addr, self.temp)
def write_framebuf(self):
# Blast out the frame buffer using a single I2C transaction to support
# hardware I2C interfaces.
self.i2c.writeto(self.addr, self.buffer)
def poweron(self):
pass
class SSD1306_SPI(SSD1306):
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
self.rate = 10 * 1024 * 1024
dc.init(dc.OUT, value=0)
res.init(res.OUT, value=0)
cs.init(cs.OUT, value=1)
self.spi = spi
self.dc = dc
self.res = res
self.cs = cs
self.buffer = bytearray((height // 8) * width)
self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height)
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
self.cs.on()
self.dc.off()
self.cs.off()
self.spi.write(bytearray([cmd]))
self.cs.on()
def write_framebuf(self):
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
self.cs.on()
self.dc.on()
self.cs.off()
self.spi.write(self.buffer)
self.cs.on()
def poweron(self):
self.res.on()
time.sleep_ms(1)
self.res.off()
time.sleep_ms(10)
self.res.on()2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166