1. STM32 Reference Implementation Notes
The default reference project is based on the STM32F103, with the four interfaces implemented using the STM32's SPI and FSMC peripherals. The sections below describe each implementation and the rationale behind it, to serve as a reference when porting to other platforms. Download the code
1.1 SPI Interface (spi.c)
Implementation: Uses the STM32 hardware SPI2 peripheral; SPI2_Init configures the baud rate, CPOL/CPHA, data bits, and so on. The three control lines CS / RS / BUSY are initialized as ordinary GPIOs by GPIO_INIT. Data transmission and reception call SPI2_WriteByte and SPI2_ReadDyte respectively, sending or reading byte by byte.
Key code:
GPIO and SPI peripheral initialization (user_change region):
static void spi_gpio_init(void)
{
GPIO_INIT();
SPI2_Init();
}CS / RS control line level setting:
static void spi_rs_set_value(u8 value)
{
if(value == 0)RS0
else RS1
}
static void spi_cs_set_value(u8 value)
{
if(value == 0)CS_L();
else CS_H();
}Byte-by-byte send / read:
static void spi_writebytes(u8* byte, u32 bytelen)
{
u32 i;
for(i=0;i<bytelen;i++)
{
SPI2_WriteByte(*byte);
byte++;
}
}
static void spi_readbytes(u8 register_address, u8* data, u32 datalen)
{
u32 i;
spi_wait_busy_sign();
for(i=0; i<datalen; i++)
{
spi_rs_set_value(0);
spi_cs_set_value(0);
spi_writebytes(®ister_address, 1);
spi_rs_set_value(1);
*data = SPI2_ReadDyte();
data++;
}
}Rationale:
- 4-wire SPI is a serial protocol whose timing is sensitive to the CPOL/CPHA phase; hardware SPI samples precisely on the clock edge, avoiding the timing jitter introduced by software bit-banging.
- Hardware SPI can reach a maximum rate of fPCLK/2 (SPI2 on the F103 is on APB1, up to about 18 MHz) and supports DMA, giving a much higher refresh throughput than GPIO emulation.
- The STM32 SPI peripheral has a built-in transmit buffer and the RXNE flag, ensuring precise single-byte send/receive timing; spi_wait_busy_sign polls the LCD's BUSY pin signal, preventing display anomalies caused by lost data.
1.2 QSPI Interface (qspi.c)
Implementation: The STM32F1 series has no native QSPI controller, so the reference project emulates QSPI with the FSMC. FSMC_GPIO_Init and FSMC_Config initialize all 16 FSMC data lines (D0~D15) and configure NOR/PSRAM mode; the QSPI protocol uses 4 IO lines (IO0~IO3), which are physically carried on 4 FSMC data lines (e.g. D0~D3), while the code splits the data at the bit level to match the single-line / 4-line transmission phases of QSPI.
- qspi_send_one_line_data: splits 1 byte into 8 bits, writing 1 bit at a time to the FSMC (single-line mode), used for the command/address phase where only 1 IO line is active.
- qspi_send_four_line_data: splits each byte into a high 4 bits and a low 4 bits written in two passes (4-line mode), used for pixel data, where all 4 IO lines transfer in parallel.
- qspi_read_bytes: reads bit by bit in single-line mode 8 times and assembles them into 1 byte.
Key code:
FSMC initialization (emulating the QSPI bus with FSMC):
static void qspi_gpio_init(void)
{
GPIO_INIT();
FSMC_GPIO_Init();
FSMC_Config();
}Single-line mode transmission (command/address phase, split into 8 bit writes):
static void qspi_send_one_line_data(u8 senddata)
{
int ii = 0;
u8 data[8] ={0};
for(ii = 0;ii<8;ii++)
{
data[ii] = (senddata >> (7 - ii))&0x01;
}
for(ii = 0;ii<8;ii++)
{
FSMC_Write_Data(data[ii]);
}
}4-line mode transmission (pixel data, high/low 4 bits written in two passes):
static void qspi_send_four_line_data(u8* data, u32 datalen)
{
u32 i;
for(i=0;i<datalen;i++)
{
FSMC_Write_Data((*data >> 4) & 0x0f);
FSMC_Write_Data(*data & 0x0f);
data++;
}
}Single-line bit-by-bit read, assembled into bytes:
static void qspi_read_bytes(u8* data, u32 datalen)
{
u8 dat;
u32 i ;
int j;
for(i=0; i<datalen; i++)
{
dat = 0;
for (j = 7; j >= 0; j--) {
dat |= (FSMC_Read_Bit() << j);
}
*data =dat;
data++;
}
}Command-sending sequence (wrapping single-line transmission):
static void qspi_writecmd(u8 cmd)
{
qspi_wait_busy_sign();
qspi_set_cs_value(0);
qspi_send_one_line_data(0x02);
qspi_send_one_line_data(0x00);
qspi_send_one_line_data(cmd);
qspi_send_one_line_data(0x00);
}Rationale:
- The STM32F1 has no dedicated QSPI peripheral, while the QSPI protocol demands precise clock edges and switching between 1/4-line modes; pure GPIO-toggle timing is difficult to keep stable.
- The FSMC provides an address/data-separated bus timing; the 4 IO lines can be operated as "1-bit memory data lines", and its hardware timing engine keeps every bit-toggle interval consistent, staying closer to the QSPI specification than software GPIO toggling.
- Thanks to the FSMC's address-mapping nature, the implementation can later be smoothly migrated to an STM32F4/F7/L4/H7 with native QSPI by replacing the bit-splitting calls inside user_change with APIs such as HAL_QSPI_Transmit.
1.3 8080 8-bit Parallel (8080_8b.c)
Implementation: Uses the FSMC's NOR/PSRAM mode. FSMC_GPIO_Init and FSMC_Config likewise initialize all 16 data lines (D0~D15) and complete the FSMC timing parameter configuration (address setup / data setup / hold time), mapping the command address and data address to different FSMC Bank offsets. In 8080 8-bit mode, each transfer uses only the 8 lines D0~D7 to carry one byte, with D8~D15 left idle.
- Command write: first pull D/C low via GPIO (_8080_8b_set_dc_value(0)) together with CS, then call FSMC_Write_Cmd(cmd) to write one byte to the command address; the FSMC drives the D0~D7 data bus and the /WR write timing.
- Data write: first pull D/C high via GPIO (_8080_8b_set_dc_value(1)), then call FSMC_Write_Data(data) to write to the data address; the FSMC drives the data bus and the /WR write timing.
- Data read: pull RD low via GPIO (_8080_8b_set_rd_value(0)) to trigger LCD output, then call FSMC_Read_Data to read one byte, and pull RD high afterwards; the /RD pull-low/pull-high is manually controlled by GPIO.
- The three control lines CS, D/C, RD are each controlled by an independent GPIO (CS_L/CS_H, DC_L/DC_H, RD_L/RD_H); the FSMC only handles the read/write timing of the data bus.
Key code:
FSMC and control-line initialization:
static void _8080_8b_gpio_Init(void)
{
GPIO_INIT();
FSMC_GPIO_Init();
FSMC_Config();
}CS / DC / RD control line level setting:
static void _8080_8b_set_cs_value(u8 value)
{
if(value==0)CS_L();
else {CS_H();}
}
static void _8080_8b_set_dc_value(u8 value)
{
if(value ==0)DC_L();
else DC_H();
}
static void _8080_8b_set_rd_value(u8 value)
{
if(value ==0){RD_L()}
else RD_H()
}Command / data write (FSMC drives the D0~D7 data bus and /WR write timing; D/C is preset via GPIO):
static void _8080_8b_write_cmd(u8 cmd)
{
FSMC_Write_Cmd(cmd);
}
static void _8080_8b_write_data(u8* data, u32 datalen)
{
u32 i;
for(i=0;i<datalen;i++)
{
FSMC_Write_Data(*data);
data++;
}
}Data read (/RD manually pulled low via GPIO to trigger LCD output; FSMC reads one byte from the data bus):
static void _8080_8b_read_bytes(u8* data, u32 datalen)
{
u32 i;
for(i=0; i<datalen; i++)
{
_8080_8b_set_rd_value(0);
*data = FSMC_Read_Data();
_8080_8b_set_rd_value(1);
data++;
}
}Command-sending sequence (D/C=0, CS=0):
static void _8080_8b_writecmd(u8 cmd)
{
_8080_8b_wait_busy_sign();
_8080_8b_set_dc_value(0);
_8080_8b_set_cs_value(0);
_8080_8b_write_cmd(cmd);
}Rationale:
- The 8080 parallel data-bus timing is isomorphic to the FSMC's NOR/PSRAM mode; the FSMC generates the D0~D7 data bus and /WR write timing directly in hardware, avoiding the CPU toggling 8 data lines beat by beat; the /CS, /RD, D/C control lines are driven by independent GPIOs and only need level toggling before and after each transfer.
- Data throughput scales linearly with the parallel bus width; an 8-bit parallel port transfers 1 byte per beat — 8× faster than single-line SPI — well suited to stable refresh of medium-resolution panels.
- The FSMC maps the LCD command/data addresses into a linear address space, so you can write to addresses just like memory, keeping the code concise.
1.4 8080 16-bit Parallel (8080_16b.c)
Implementation: Shares the same FSMC configuration as the 8-bit version (likewise initializing all 16 data lines D0~D15, same NOR/PSRAM mode and timing parameters); the difference is that in 8080 16-bit mode each transfer uses all 16 data lines to carry one half-word (2 bytes).
- _8080_16b_write_data: packs consecutive bytes two at a time into 16-bit half-words, high byte first, writing 2 bytes at a time via FSMC_Write_Data(datas); odd bytes are automatically padded into a half-word.
- _8080_16b_read_bytes: reads at single-byte granularity, pulling RD low each beat before calling FSMC_Read_Data.
- The command/data/CS/RD control sequences are the same as in the 8-bit version.
Key code:
FSMC and control-line initialization (shared with the 8-bit version):
static void _8080_16b_gpio_init(void)
{
GPIO_INIT();
FSMC_GPIO_Init();
FSMC_Config();
}16-bit data write (bytes packed pairwise into half-words, odd bytes padded):
static void _8080_16b_write_data(u8* data, u32 datalen)
{
u32 i;
u16 datas;
for(i = 0; i < datalen - 1; i += 2)
{
datas = ((u16)data[i] << 8) | data[i+1];
FSMC_Write_Data(datas);
}
if(datalen % 2)
{
datas = ((u16)data[datalen-1] << 8);
FSMC_Write_Data(datas);
}
}Single-byte-granularity read (pull RD low each beat, then call FSMC_Read_Data):
static void _8080_16b_read_bytes(u8* data, u32 datalen)
{
u32 i;
for(i=0; i<datalen; i++)
{
_8080_16b_set_rd_value(0);
*data = FSMC_Read_Data();
_8080_16b_set_rd_value(1);
data++;
}
}Pixel data write (goes directly through the 16-bit packing path):
static void _8080_16b_writepix(u8* colorbuff, u32 len)
{
_8080_16b_write_data(colorbuff, len);
}Rationale:
- The 16-bit parallel port transfers 2 bytes per beat, theoretically doubling the throughput of 8-bit again; it is the highest-bandwidth form of the 8080 interface, suited to high-resolution / high-frame-rate panels (e.g. 800×480 and above).
- In FSMC 16-bit mode the hardware manages the D0~D15 data bus and /WR write timing, driving all 16 data lines in a single beat and avoiding the inter-bit skew caused by bit-by-bit GPIO toggling; /RD is still controlled by GPIO (RD_L/RD_H), and the pairwise packing of bytes into half-words is done in software inside _8080_16b_write_data.
- The command and parameter phases are still sent in 8-bit (the command byte has only 8 significant bits), while only the pixel-data phase is sent as 16-bit half-words; this both complies with the controller's command specification and maximizes refresh bandwidth — it is the recommended TR2S configuration on the STM32 platform.
1.5 STM32 Implementation and Peripheral Mapping Overview
| Interface source file | STM32 peripheral | Key APIs | Peripheral selection rationale |
|---|---|---|---|
| interface/spi.c | Hardware SPI2 | SPI2_Init / SPI2_WriteByte / SPI2_ReadDyte | Hardware SPI offers precise timing, high speed, and DMA support |
| interface/qspi.c | FSMC (16 data lines, 4 of them mapped to QSPI IO0~IO3) | FSMC_GPIO_Init / FSMC_Config / FSMC_Write_Data / FSMC_Read_Bit | F1 has no native QSPI; the FSMC provides stable bit-by-bit timing |
| interface/8080_8b.c | FSMC (16 data lines, D0~D7 used per transfer for 1 byte) | FSMC_Write_Cmd / FSMC_Write_Data / FSMC_Read_Data | FSMC generates the D0~D7 data bus and /WR write timing; /CS /RD /DC are GPIO-controlled |
| interface/8080_16b.c | FSMC (16 data lines, D0~D15 used per transfer for 2 bytes) | FSMC_Write_Cmd / FSMC_Write_Data(u16) / FSMC_Read_Data | 16-bit transfers 2 bytes per beat — highest bandwidth, suited to high-resolution panels |
Note: The implementations above serve only as a reference for the STM32 platform and are not the only viable approach. When porting to other MCUs, they can be replaced with: a native QSPI controller, software-emulated GPIO, or a hardware SPI peripheral — as long as the function signatures and mounting logic inside the user_change region stay unchanged, the upper-layer APIs require no modification.