参考资料
STM32
- STM32F103参考手册:rm0008-stm32f101xx-stm32f102xx-stm32f103xx-stm32f105xx-and-stm32f107xx
- STM32103ZE数据手册:STM32F103xC, STM32F103xD, STM32F103xE
FSMC扩展SRAM
FSMC扩展LCD模组
- LCD驱动模组:中景园ZJY350IT002
- LCD驱动芯片:ILI9486
字模取模工具
图片取模工具
MDK参考手册
- ARM Product Manuals
- [µVision User’s Guide]https://www.keil.com/support/man/docs/uv4/)
- Arm® Compiler for Embedded User Guide 6.23
FSMC概述
MCU自带的FLASH和SRAM资源是十分有限的,相比于PC机的存储空间而言要小的可怜。一般情况对于嵌入式应用来说这点存储空间一般也就够用了,但避免不了一些大量消耗内存的应用, 比如说图像处理。对于这类对内存要求较高的应用,我们往往需要扩展一个FLASH或者SRAM。STM32提供的FSMC就是用来完成这项功能的。
FSMC(Flexible static memory controller,灵活的静态存储器控制器),STM32可以通过FSMC与SRAM、ROM、PSRAM、Nor Flash和NandFlash存储器的引脚相连,从而进行数据的交换。要注意的是,FSMC 只能扩展静态的内存(S:static),不能是动态的内存,比如 SDRAM 就不能扩展。
FSMC把AHB总线上的数据转换为对应外设的通信协议,控制外设的访问时序,以至于我们可以直接在程序中寻址访问。
FSMC组成
功能框图
FSMC主要由4部分组成:AHB总线接口(包括FSMC的配置寄存器)、NOR闪存/SRAM控制器、NAND闪存/PC卡控制器、外设接口四个部分构成。
本次实验需要用到的引脚如下:
- FSMC_A[25:0]:地址总线
- FSMC_D[15:0]:双向数据总线
- FSMC_NE[4:1]:片选引脚,低电平有效
- FSMC_NOE:读使能,低电平有效
- FSMC_NWE:写使能,低电平有效
AHB总线接口
AHB总线接口是CPU、DMA等AHB总线主设备访问FSMC的通道,它负责把AHB总线事务转换成为外设通信的协议。
AHB总线事务的请求可以是8、16或者32位的,但外设器件的数据线位宽是恒定的。如果两者宽度相同就不存在什么问题,如果总线事务的位宽大于外设的位宽,那么总线接口将把总线事务拆分为多个连续的8位或16位形式访问外设。我们应当尽量避免总线事务宽度小于外设宽度的情况出现,因为这将可能导致数据的不一致,具体与外设类型有关系。
配置寄存器则描述了扩展外设的具体形式、通信协议和接口形式。用于总线接口将AHB总线事务转换为外设通信协议, 驱动NOR闪存/SRAM控制器和NAND闪存/PC卡控制器,进而控制外设。
NOR闪存/PSRAM控制器
NOR/PSRAM内存控制器支持各种同步和异步的内存。所谓同步内存就是在读写内存的时候需要一个同步时钟来指导数据的发送和接收, 与我们在串口通信中提到的同步/异步通信是一个道理。对于同步内存,FSMC只会在读写操作的时候产生驱动时钟,而且其频率是系统总线时钟HCLK的分频。
NOR/PSRAM控制器用于生成适当的时序,以驱动8位、16位、32位的异步SRAM和ROM、异步或者突发模式的PSRAM和NOR闪存。我们通过配置寄存器描述外设的特征和时序后,控制器就可以为我们生成对应的驱动时序。
NAND闪存/PC卡控制器
NAND/PC卡控制器用于驱动8位或者16位的NAND闪存以及16位的PC卡兼容设备。
外设接口
用于与要扩展外设联通用的。在接线时必须根据每个外设的特点,来进行合适的接线。
外部设备地址映射
从FSMC的角度看,可以把外部存储器划分为固定大小为4个256M字节的存储块。
存储块1用于访问最多4个NOR闪存或PSRAM存储设备。这个存储区被划分为4个NOR/PSRAM区并有4个专用的片选。存储块2和3用于访问NAND闪存设备,每个存储块连接一个NAND闪存。存储块4用于访问PC卡设备。
每一个存储块上的存储器类型是由用户在配置寄存器中定义的。
FSMC控制NOR闪存或PSRAM的时序
FSMC 外设支持输出多种不同的时序以便于控制不同的存储器,它具有6种模式:1,A,2/B,C,D,复用模式。
所有信号由内部时钟HCLK保持同步,但该时钟不会输出到外部扩展的存储器。FSMC始终在片选信号NE失效前对数据线采样,这样能够保证符合存储器的数据保持时序。
所有的控制器输出信号在内部时钟(HCLK)的上升沿变化,在同步写模式(PSRAM)下,读写的数据在存储器时钟(CLK)的下降沿变化。
我们以读写SRAM的模式A为例来介绍。
当内核发出访问某个指向外部存储器的地址时,FSMC外设会根据配置控制信号线产生时序访问存储器(硬件自动生成对应的时序),上图中的是访问外部 SRAM 时 FSMC 外设的读写时序。
在读时序中,一个存储器操作周期由1个地址建立周期(ADDSET),1个数据建立周期(DATASET)和2个HCLK周期组成。
- 在地址建立周期中,地址线发出要访问的地址,数据掩码信号线指示出要读取地址的高、低字节部分,片选信号使能存储器芯片
- 地址建立周期结束后读使能信号线发出读使能信号,接着存储器通过数据信号线把目标数据传输给 FSMC,FSMC 把它交给内核。
写时序类似,区别是它的一个存储器操作周期仅由1个地址建立周期(ADDSET)和1个数据建立周期(DATAST)组成,且在数据建立周期期间写使能信号线发出写信号,接着 FSMC 把数据通过数据线传输到存储器中。
FSMC应用案例:扩展外部SRAM
需求描述
使用FSMC扩展外部SRAM。然后把内存数据存储到外部SRAM中。
STM32F1 系列的芯片不支持扩展SDRAM(STM32F429 系列支持),它仅支持使用 FSMC 外设扩展 SRAM。由于引脚数量的限制,只有 STM32F103ZE 或以上型号的芯片才可以扩展外部 SRAM。
SRAM芯片IS62WV51216
引脚分析
SRAM 的控制比较简单,只要控制信号线使能了访问,从地址线输入要访问的地址,即可从 I/O 数据线写入或读出数据。
读操作时序分析
写操作时序分析
模式1扩展SRAM实践
硬件连接
FSMC片选引脚 & 地址映射
RCC时钟使能
void FSMC_RCC_Config(void) {
// 开启FSMC和GPIO时钟-DEFG
RCC->AHBENR |= RCC_AHBENR_FSMCEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPDEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
}
GPIO配置
如上,所有涉及到的引脚配置为复用推挽即可。
void FSMC_GPIO_Config(void) {
/* 1 配置 A0-A18 地址端口的输出模式 复用推挽输出CNF:10 50MHz速度 MODE:11*/
/* =============MODE=============== */
GPIOF->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1 |
GPIO_CRL_MODE2 |
GPIO_CRL_MODE3 |
GPIO_CRL_MODE4 |
GPIO_CRL_MODE5);
GPIOF->CRH |= (GPIO_CRH_MODE12 |
GPIO_CRH_MODE13 |
GPIO_CRH_MODE14 |
GPIO_CRH_MODE15);
GPIOG->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1 |
GPIO_CRL_MODE2 |
GPIO_CRL_MODE3 |
GPIO_CRL_MODE4 |
GPIO_CRL_MODE5);
GPIOD->CRH |= (GPIO_CRH_MODE11 |
GPIO_CRH_MODE12 |
GPIO_CRH_MODE13);
/* =============CNF=============== */
GPIOF->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1 |
GPIO_CRL_CNF2_1 |
GPIO_CRL_CNF3_1 |
GPIO_CRL_CNF4_1 |
GPIO_CRL_CNF5_1);
GPIOF->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0 |
GPIO_CRL_CNF2_0 |
GPIO_CRL_CNF3_0 |
GPIO_CRL_CNF4_0 |
GPIO_CRL_CNF5_0);
GPIOF->CRH |= (GPIO_CRH_CNF12_1 |
GPIO_CRH_CNF13_1 |
GPIO_CRH_CNF14_1 |
GPIO_CRH_CNF15_1);
GPIOF->CRH &= ~(GPIO_CRH_CNF12_0 |
GPIO_CRH_CNF13_0 |
GPIO_CRH_CNF14_0 |
GPIO_CRH_CNF15_0);
GPIOG->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1 |
GPIO_CRL_CNF2_1 |
GPIO_CRL_CNF3_1 |
GPIO_CRL_CNF4_1 |
GPIO_CRL_CNF5_1);
GPIOG->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0 |
GPIO_CRL_CNF2_0 |
GPIO_CRL_CNF3_0 |
GPIO_CRL_CNF4_0 |
GPIO_CRL_CNF5_0);
GPIOD->CRH |= (GPIO_CRH_CNF11_1 |
GPIO_CRH_CNF12_1 |
GPIO_CRH_CNF13_1);
GPIOD->CRH &= ~(GPIO_CRH_CNF11_0 |
GPIO_CRH_CNF12_0 |
GPIO_CRH_CNF13_0);
/*
2 数据端口 复用推挽输出
在实际应用中,即使数据线被配置为输出模式,FSMC控制器仍然能够管理数据线的方向,使其在需要时成为输入线。
这种自动切换是由FSMC控制器硬件管理的,不需要软件干预。
因此,即使GPIO配置为复用推挽输出,FSMC依然可以实现读取操作。
*/
/* =============MODE=============== */
GPIOD->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1);
GPIOD->CRH |= (GPIO_CRH_MODE8 |
GPIO_CRH_MODE9 |
GPIO_CRH_MODE10 |
GPIO_CRH_MODE14 |
GPIO_CRH_MODE15);
GPIOE->CRL |= (GPIO_CRL_MODE7);
GPIOE->CRH |= (GPIO_CRH_MODE8 |
GPIO_CRH_MODE9 |
GPIO_CRH_MODE10 |
GPIO_CRH_MODE11 |
GPIO_CRH_MODE12 |
GPIO_CRH_MODE13 |
GPIO_CRH_MODE14 |
GPIO_CRH_MODE15);
/* =============CNF=============== */
GPIOD->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0);
GPIOD->CRH |= (GPIO_CRH_CNF8_1 |
GPIO_CRH_CNF9_1 |
GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF14_1 |
GPIO_CRH_CNF15_1);
GPIOD->CRH &= ~(GPIO_CRH_CNF8_0 |
GPIO_CRH_CNF9_0 |
GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF14_0 |
GPIO_CRH_CNF15_0);
GPIOE->CRL |= (GPIO_CRL_CNF7_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF7_0);
GPIOE->CRH |= (GPIO_CRH_CNF8_1 |
GPIO_CRH_CNF9_1 |
GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF11_1 |
GPIO_CRH_CNF12_1 |
GPIO_CRH_CNF13_1 |
GPIO_CRH_CNF14_1 |
GPIO_CRH_CNF15_1);
GPIOE->CRH &= ~(GPIO_CRH_CNF8_0 |
GPIO_CRH_CNF9_0 |
GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF11_0 |
GPIO_CRH_CNF12_0 |
GPIO_CRH_CNF13_0 |
GPIO_CRH_CNF14_0 |
GPIO_CRH_CNF15_0);
/* 3 其他控制端口 复用推挽输出 */
GPIOD->CRL |= (GPIO_CRL_MODE4 |
GPIO_CRL_MODE5);
GPIOD->CRL |= (GPIO_CRL_CNF4_1 |
GPIO_CRL_CNF5_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF4_0 |
GPIO_CRL_CNF5_0);
GPIOE->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1);
GPIOE->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0);
GPIOG->CRH |= (GPIO_CRH_MODE10);
GPIOG->CRH |= (GPIO_CRH_CNF10_1);
GPIOG->CRH &= ~(GPIO_CRH_CNF10_0);
}
AFIO配置
FSMC对引脚的复用只有一套,没有备用的重映射引脚,因此AFIO无需额外配置。
FSMC Bank结构体
在访问FSMC的Bank寄存器的时stm32f10x.h并没有给所有的寄存器起名字,而是用了一个数组存储了所有的寄存器。每个数组长度为8,表示一共存储了8个寄存器。
typedef struct
{
__IO uint32_t BTCR[8];
} FSMC_Bank1_TypeDef;
void FSMC_BCR_Config(void) {
// 配置 BCR3 => BTCR[4]
// 开启写使能,允许向SRAM写入数据
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_WREN;
// 配置存储器数据总线宽度为16
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MWID;
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MWID_0;
// 配置存储器类型为SRAM
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MTYP;
// 地址-数据复用使能:关闭
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MUXEN;
// 使能存储器对应的Bank
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MBKEN;
}
控制寄存器BCR(分区3-BCK3)
时序生成寄存器BTR(分区3-BTR3)
结合这里的STM32参考手册以及之前对SRAM读/写操作的时序分析,我们来看下ADDSET和DATASET该如何配置。
地址访问阶段
- SRAM读/写操作:对于地址访问时间/地址建立时间,最小值可以为0,因此ADDSET我们可以配置为0.
数据访问阶段
- SRAM读操作:SRAM输出使能访问时间最小可以为0,对应DATASET可以配置为0
- SRAM写操作:写使能有效电平时间时间最小为40ns(55ns版本SRAM),根据STM32F103的HCLK频率72MHz(周期约13.9ns),对应DATASET我们可以配置为4,即4个HCLK周期满足最小40ns的时序要求。因为模式1时序生成寄存器只有一个BTR,因此DATASET可以取最大值4。(其他模式需要分开配置读、写时序参数时,可以通过BCR中的EXTMOD位和BWTR来实现)。
[!NOTE]
如下以55ns访问时间版本的SRAM为例(IS62WV51216BLL-55TLI )
void FSMC_BTR_Config(void) {
// 配置 BTR3 => BTCR[5]
// 地址访问/建立时间(HCLK数量)配置为0,实际为(0+1)HCLK
FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_ADDSET;
// 数据访问/建立时间配置为4(4*HCLK),实际为(4+1)HCLK
FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_DATAST;
// 4 => 0100
FSMC_Bank1->BTCR[5] |= FSMC_BTR3_DATAST_2;
}
完整代码
fsmc.h
#ifndef __FSMC_H__
#define __FSMC_H__
#include "stm32f10x.h"
void FSMC_Init(void);
#endif /* __FSMC_H__ */
fsmc.c
#include "fsmc.h"
static void FSMC_RCC_Config(void);
static void FSMC_GPIO_Config(void);
static void FSMC_BCR_Config(void);
static void FSMC_BTR_Config(void);
void FSMC_Init(void) {
FSMC_RCC_Config();
FSMC_GPIO_Config();
FSMC_BTR_Config();
FSMC_BCR_Config();
}
void FSMC_RCC_Config(void) {
// 开启FSMC和GPIO时钟-DEFG
RCC->AHBENR |= RCC_AHBENR_FSMCEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPDEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPFEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
}
void FSMC_GPIO_Config(void) {
/* 1 配置 A0-A18 地址端口的输出模式 复用推挽输出CNF:10 50MHz速度 MODE:11*/
/* =============MODE=============== */
GPIOF->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1 |
GPIO_CRL_MODE2 |
GPIO_CRL_MODE3 |
GPIO_CRL_MODE4 |
GPIO_CRL_MODE5);
GPIOF->CRH |= (GPIO_CRH_MODE12 |
GPIO_CRH_MODE13 |
GPIO_CRH_MODE14 |
GPIO_CRH_MODE15);
GPIOG->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1 |
GPIO_CRL_MODE2 |
GPIO_CRL_MODE3 |
GPIO_CRL_MODE4 |
GPIO_CRL_MODE5);
GPIOD->CRH |= (GPIO_CRH_MODE11 |
GPIO_CRH_MODE12 |
GPIO_CRH_MODE13);
/* =============CNF=============== */
GPIOF->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1 |
GPIO_CRL_CNF2_1 |
GPIO_CRL_CNF3_1 |
GPIO_CRL_CNF4_1 |
GPIO_CRL_CNF5_1);
GPIOF->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0 |
GPIO_CRL_CNF2_0 |
GPIO_CRL_CNF3_0 |
GPIO_CRL_CNF4_0 |
GPIO_CRL_CNF5_0);
GPIOF->CRH |= (GPIO_CRH_CNF12_1 |
GPIO_CRH_CNF13_1 |
GPIO_CRH_CNF14_1 |
GPIO_CRH_CNF15_1);
GPIOF->CRH &= ~(GPIO_CRH_CNF12_0 |
GPIO_CRH_CNF13_0 |
GPIO_CRH_CNF14_0 |
GPIO_CRH_CNF15_0);
GPIOG->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1 |
GPIO_CRL_CNF2_1 |
GPIO_CRL_CNF3_1 |
GPIO_CRL_CNF4_1 |
GPIO_CRL_CNF5_1);
GPIOG->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0 |
GPIO_CRL_CNF2_0 |
GPIO_CRL_CNF3_0 |
GPIO_CRL_CNF4_0 |
GPIO_CRL_CNF5_0);
GPIOD->CRH |= (GPIO_CRH_CNF11_1 |
GPIO_CRH_CNF12_1 |
GPIO_CRH_CNF13_1);
GPIOD->CRH &= ~(GPIO_CRH_CNF11_0 |
GPIO_CRH_CNF12_0 |
GPIO_CRH_CNF13_0);
/*
2 数据端口 复用推挽输出
在实际应用中,即使数据线被配置为输出模式,FSMC控制器仍然能够管理数据线的方向,使其在需要时成为输入线。
这种自动切换是由FSMC控制器硬件管理的,不需要软件干预。
因此,即使GPIO配置为复用推挽输出,FSMC依然可以实现读取操作。
*/
/* =============MODE=============== */
GPIOD->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1);
GPIOD->CRH |= (GPIO_CRH_MODE8 |
GPIO_CRH_MODE9 |
GPIO_CRH_MODE10 |
GPIO_CRH_MODE14 |
GPIO_CRH_MODE15);
GPIOE->CRL |= (GPIO_CRL_MODE7);
GPIOE->CRH |= (GPIO_CRH_MODE8 |
GPIO_CRH_MODE9 |
GPIO_CRH_MODE10 |
GPIO_CRH_MODE11 |
GPIO_CRH_MODE12 |
GPIO_CRH_MODE13 |
GPIO_CRH_MODE14 |
GPIO_CRH_MODE15);
/* =============CNF=============== */
GPIOD->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0);
GPIOD->CRH |= (GPIO_CRH_CNF8_1 |
GPIO_CRH_CNF9_1 |
GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF14_1 |
GPIO_CRH_CNF15_1);
GPIOD->CRH &= ~(GPIO_CRH_CNF8_0 |
GPIO_CRH_CNF9_0 |
GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF14_0 |
GPIO_CRH_CNF15_0);
GPIOE->CRL |= (GPIO_CRL_CNF7_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF7_0);
GPIOE->CRH |= (GPIO_CRH_CNF8_1 |
GPIO_CRH_CNF9_1 |
GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF11_1 |
GPIO_CRH_CNF12_1 |
GPIO_CRH_CNF13_1 |
GPIO_CRH_CNF14_1 |
GPIO_CRH_CNF15_1);
GPIOE->CRH &= ~(GPIO_CRH_CNF8_0 |
GPIO_CRH_CNF9_0 |
GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF11_0 |
GPIO_CRH_CNF12_0 |
GPIO_CRH_CNF13_0 |
GPIO_CRH_CNF14_0 |
GPIO_CRH_CNF15_0);
/* 3 其他控制端口 复用推挽输出 */
GPIOD->CRL |= (GPIO_CRL_MODE4 |
GPIO_CRL_MODE5);
GPIOD->CRL |= (GPIO_CRL_CNF4_1 |
GPIO_CRL_CNF5_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF4_0 |
GPIO_CRL_CNF5_0);
GPIOE->CRL |= (GPIO_CRL_MODE0 |
GPIO_CRL_MODE1);
GPIOE->CRL |= (GPIO_CRL_CNF0_1 |
GPIO_CRL_CNF1_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF0_0 |
GPIO_CRL_CNF1_0);
GPIOG->CRH |= (GPIO_CRH_MODE10);
GPIOG->CRH |= (GPIO_CRH_CNF10_1);
GPIOG->CRH &= ~(GPIO_CRH_CNF10_0);
}
void FSMC_BCR_Config(void) {
// 配置 BCR3 => BTCR[4]
// 开启写使能,允许向SRAM写入数据
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_WREN;
// 配置存储器数据总线宽度为16
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MWID;
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MWID_0;
// 配置存储器类型为SRAM
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MTYP;
// 地址-数据复用使能:关闭
FSMC_Bank1->BTCR[4] &= ~FSMC_BCR3_MUXEN;
// 使能存储器对应的Bank
FSMC_Bank1->BTCR[4] |= FSMC_BCR3_MBKEN;
}
void FSMC_BTR_Config(void) {
// 配置 BTR3 => BTCR[5]
// 地址访问/建立时间(HCLK数量)配置为0,实际为(0+1)HCLK
FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_ADDSET;
// 数据访问/建立时间配置为4(4*HCLK),实际为(4+1)HCLK
FSMC_Bank1->BTCR[5] &= ~FSMC_BTR3_DATAST;
// 4 => 0100
FSMC_Bank1->BTCR[5] |= FSMC_BTR3_DATAST_2;
}
main.c
#include "fsmc.h"
#include "logger.h"
#include "stm32f10x.h"
#include "uart.h"
void uart1_received_callback(uint8_t buf[], uint8_t size) {}
// 编译器关键字:__attribute__用于设置变量的属性,at用于指定存放地址
uint16_t var1 __attribute__((at(0x68000000)));
uint32_t var2 __attribute__((at(0x68000004))); // 地址必须4字节对齐
uint16_t var3;
int main() {
uart_init();
FSMC_Init();
LOG_DEBUG("main start")
// 局部变量在栈上分配,其地址与函数执行时的栈指针SP有关
uint16_t var4 __attribute__((at(0x68000000)));
uint32_t *p = (uint32_t *)0x68000020;
var1 = 10;
var2 = 20;
var3 = 30;
var4 = 40;
*p = 50;
LOG_DEBUG("var1 = %d, @%p", var1, &var1)
LOG_DEBUG("var2 = %d, @%p", var2, &var2)
LOG_DEBUG("var3 = %d, @%p", var3, &var3)
LOG_DEBUG("var4 = %d, @%p", var4, &var4)
LOG_DEBUG("p = %d, @%p", *p, p)
while (1) {
}
}
HAL库实现
再看内存映射
SRAM地址线接口[25:0]
STM32将32bit-4G地址空间划分为了8个block块,因此32bit的高3位可以作为块号。
而在block3内,又可以使用一个bit来表示bank(内存组号)。高三位固定为011,0110表示bank1,0111表示bank2。
在bank1中,又可以使用两位来表示区号,对应NOR/PSRAM1~4。
这也解释了为什么FSMC框图中,地址线引脚只有26个,因为高6位(block-3bit,bank-1bit,分区-2bit)用于定位分区,低26bit才用于定位存储器内部单元
数据宽度 & 高/低字节掩码
FSMC扩展LCD显存
LCD接口类型
LCD的接口有多种,分类很细。主要看LCD的驱动方式和控制方式,目前彩色LCD的连接方式一般有这么几种:MCU模式,RGB模式,SPI模式,VSYNC模式,MDDI模式,DSI模式。
但应用比较多的就是MCU模式和RGB模式。
MCU模式
因为主要针对单片机的领域在使用,因此得名,其主要特点是价格便宜。MCU-LCD接口的标准术语是Intel提出的8080总线标准,因此在很多文档中用 I80 来指MCU-LCD屏。
对于MCU接口的LCM(LCD Module),其内部的芯片就叫LCD驱动器,都带GRAM(显存)。主要功能是对主机发过的数据/命令,进行变换,变成每个像素的RGB数据,使之在屏上显示出来。
RGB模式
RGB模式是大屏采用较多的模式,比如我们电脑显示器。
对于RGB接口的LCM,主机输出的直接是每个像素的RGB数据,不需要进行变换(GAMMA校正等除外),对于这种接口,需要在主机部分有个LCD控制器(平常所说的显卡),以产生RGB数据和点、行、帧同步信号。
液晶模块Z350IT002
这里使用的液晶显示模块是Z350IT002。这是一款 TFT LCD(薄膜晶体管液晶显示器)模块。TFT LCD 以其优秀的显示性能和低功耗特性而广泛应用于各种电子设备。
(1)分辨率
模块的分辨率为 320RGB × 480 点阵。这意味着屏幕横向有 320 个像素点,每个像素点由红、绿、蓝三种颜色组成,纵向有 480 个像素点。这种分辨率适合于显示清晰的图像和文字。
(2)构造
它由 960 个源(source)和 480 个门(gate)构成。源用于横向的像素点驱动,门用于纵向的像素点驱动。这种结构有助于精确地控制每个像素点,从而提供清晰的图像显示。
(3)微控制器接口
Z350IT002 设计了易于通过微控制器访问和控制的接口。这使得它非常适合于嵌入式系统或其他需要直接由微控制器控制显示屏的应用。
(4)应用领域
考虑到其尺寸和分辨率,Z350IT002 特别适用于中小尺寸的显示需求,如便携式设备、工业控制面板、小型嵌入式系统等。
(5)显示效果
作为一款 TFT LCD,Z350IT002 可以提供良好的颜色对比度和亮度,适合于需要中等分辨率和高色彩质量的应用。
总体而言,Z350IT002 是一款适用于多种中小型电子设备的TFT LCD显示模块,其易于微控制器集成的特点使其成为许多嵌入式应用和工业产品的理想选择。
控制器芯片ILI9486
液晶模块Z350IT002内部使用的控制芯片是:ILI9486。
ILI9486 是一款流行的 LCD 控制器芯片,由 Ilitek 公司生产,通常用于驱动中小尺寸的 TFT(薄膜晶体管)LCD 显示屏。
具有320RGBx480点的分辨率。它包括960通道的源驱动器和480通道的门驱动器,以及用于320RGBx480点图形数据的345600字节GRAM和电源供应电路。
另外ILI9486支持多种接口类型,包括:
(1)并行CPU数据总线接口,支持8位、9位、16位和18位。
(2)3线和4线串行外设接口(SPI)。
(3)符合RGB(16位/18位)数据总线,用于视频图像显示。
(4)高速串行接口,提供一个数据和时钟通道,支持最高达500Mbps的MIPI DSI链路。
(5)支持MDDI接口。
[!IMPORTANT]
本案例使用的芯片接口固定为16位的并行8080接口。
使用STM32的FSMC来实现8080时序
8080 通讯接口时序可以由 STM32 使用普通 I/O 接口进行模拟,但这样效率太低,STM32 提供了一种特别的控制方法——使用 FSMC 接口实现 8080 时序。我们在前面使用CubeMX扩展SRAM时候已经可以看到了这点。
时序的控制FSMC可以完成,但是如何向GRAM写数据,写什么格式的数据呢?
GRAM,作用可以理解为显存, GRAM 中每个存储单元都对应着液晶面板的一个像素点。使像素点呈现特定的颜色,而多个像素点组合起来就成为一个你想表达的东西,一段文字或者一副图。
[!IMPORTANT]
按照标准的格式,16 位的像素点的三原色描述的位数为R:G:B=5:6:5, 描述绿色的位数比较多是因为人眼对绿色更为敏感。
实验-使用FSMC控制LCD显示
硬件电路设计
(1)D0-D15是16位数据总线接口。分别接FSMC的D0-D15。
(2)RST是LCD复位引脚,低电平复位。接LCD-RST(PG15)。
(3)RD是读控制引脚,上升沿时读数据。接FSMC-NOE(PD4)。
(4)WR是写控制引脚,上升沿时写数据。接FSMC-NWE(PD5)。
(5)RS是数据或命令选择引脚RS=1写数据,RS=0写命令。接FSMC-A10(PG0)。
(6)CS是片选引脚,低电平有效。接FSMC-NE4(PG12)。
(7)LEDA是背光电源(3.0V-3.4V)引脚。
(8)LEDK是背光亮度控制引脚。通过LCD-BG(PB0)来驱动MOS管Q5的导通电流。可以通过给LCD-BG输出PWM波来控制背光的亮度。占空比越大,背光就会越亮。
(9)YD,XL,YU,XR是触摸屏控制引脚。
FSMC寻址
扩展LCD的时候,使用的是块1的地址,一共4*64MB = 256MB,每部分的地址如下:
我们选择的是NE4, 所以地址范围是:0X6C00 0000 ~ 0X6FFF FFFF
,寄存器的基地址是6C000000
。
[!NOTE]
注意:当使用16位宽的外部存储器时,用HADDR[25:1]表示外部的FSMC_A[24:0],内部地址相当于左移了1位,所以计算地址的时候要注意。
LCD我们选择的是16位宽度的,选择地址线时,我们选择的是A10接LCD的D/CX(数据/命令引脚)。
当A10=0时,表示写命令,所以地址是:
0x6C00 0000
。当A10=1时,表示写数据,所以地址是:
0x6C00 0000 + 1<<11 = 0x6C00 0800
FSMC初始化
fsmc.h
#ifndef __FSMC_H__
#define __FSMC_H__
#include "stm32f10x.h"
void FSMC_Init(void);
#endif /* __FSMC_H__ */
fsmc.c
#include "fsmc.h"
static void FSMC_RCC_Config(void);
static void FSMC_GPIO_Config(void);
static void FSMC_BCR_Config(void);
static void FSMC_BTR_Config(void);
void FSMC_Init(void) {
FSMC_RCC_Config();
FSMC_GPIO_Config();
FSMC_BTR_Config();
FSMC_BCR_Config();
}
void FSMC_RCC_Config(void) {
// 开启FSMC和GPIO时钟-DEFG
RCC->AHBENR |= RCC_AHBENR_FSMCEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPDEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
}
void FSMC_GPIO_Config(void) {
/* 1 配置 A10 地址端口的输出模式 复用推挽输出CNF:10 50MHz速度 MODE:11*/
GPIOG->CRL |= GPIO_CRL_MODE0;
GPIOG->CRL |= GPIO_CRL_CNF0_1;
GPIOG->CRL &= ~GPIO_CRL_CNF0_0;
/*
2 数据端口 复用推挽输出
在实际应用中,即使数据线被配置为输出模式,FSMC控制器仍然能够管理数据线的方向,使其在需要时成为输入线。
这种自动切换是由FSMC控制器硬件管理的,不需要软件干预。
因此,即使GPIO配置为复用推挽输出,FSMC依然可以实现读取操作。
*/
/* =============MODE=============== */
GPIOD->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
GPIOD->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
GPIOE->CRL |= (GPIO_CRL_MODE7);
GPIOE->CRH |=
(GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 | GPIO_CRH_MODE11 |
GPIO_CRH_MODE12 | GPIO_CRH_MODE13 | GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
/* =============CNF=============== */
GPIOD->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
GPIOD->CRH |= (GPIO_CRH_CNF8_1 | GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF14_1 | GPIO_CRH_CNF15_1);
GPIOD->CRH &= ~(GPIO_CRH_CNF8_0 | GPIO_CRH_CNF9_0 | GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_0);
GPIOE->CRL |= (GPIO_CRL_CNF7_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF7_0);
GPIOE->CRH |= (GPIO_CRH_CNF8_1 | GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF11_1 | GPIO_CRH_CNF12_1 | GPIO_CRH_CNF13_1 |
GPIO_CRH_CNF14_1 | GPIO_CRH_CNF15_1);
GPIOE->CRH &= ~(GPIO_CRH_CNF8_0 | GPIO_CRH_CNF9_0 | GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF11_0 | GPIO_CRH_CNF12_0 | GPIO_CRH_CNF13_0 |
GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_0);
/* 3 其他控制端口 复用推挽输出 */
// NOE NWE PD4 PD5
GPIOD->CRL |= (GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
GPIOD->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0);
// NE4 PG12
GPIOG->CRH |= (GPIO_CRH_MODE12);
GPIOG->CRH |= (GPIO_CRH_CNF12_1);
GPIOG->CRH &= ~(GPIO_CRH_CNF12_0);
}
void FSMC_BCR_Config(void) {
// 配置 BCR4 => BTCR[6]
// 开启写使能,允许向SRAM写入数据
FSMC_Bank1->BTCR[6] |= FSMC_BCR4_WREN;
// 配置存储器数据总线宽度为16
FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MWID;
FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MWID_0;
// 配置存储器类型为SRAM
FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MTYP;
// 地址-数据复用使能:关闭
FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MUXEN;
// 使能存储器对应的Bank
FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MBKEN;
}
void FSMC_BTR_Config(void) {
// 配置 BTR4 => BTCR[7]
// 地址访问/建立时间(HCLK数量)配置为0,实际为(0+1)HCLK
FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_ADDSET;
// 数据访问/建立时间配置为3 => 0011,实际为(3+1)HCLK
FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_DATAST;
FSMC_Bank1->BTCR[7] |= FSMC_BTR4_DATAST_0;
FSMC_Bank1->BTCR[7] |= FSMC_BTR4_DATAST_1;
// 总线读-写切换空闲时间配置为0 => (0+1)HCLK
FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_BUSTURN;
}
LCD初始化
lcd.h
#ifndef __LCD_H__
#define __LCD_H__
#include "fsmc.h"
#include "delay.h"
#define SRAM_BANK4 0x6C000000
// PA10为0:发送命令
#define LCD_ADDR_CMD ((volatile uint16_t *)SRAM_BANK4)
// PA10为1:发送数据(SRAM数据总线宽度为16时,FSMC会将AHB地址线[25:1]>>1输入到SRAM,因此这里的bit12对应FSMC的PA10-bit11)
#define LCD_ADDR_DATA ((volatile uint16_t *)(SRAM_BANK4 | (0x01 << 11)))
#define DISPLAY_W 320
#define DISPLAY_H 480
//画笔颜色
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define BRED 0XF81F
#define GRED 0XFFE0
#define GBLUE 0X07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define GREEN 0x07E0
#define CYAN 0x7FFF
#define YELLOW 0xFFE0
#define BROWN 0XBC40 //棕色
#define BRRED 0XFC07 //棕红色
#define GRAY 0X8430 //灰色
void LCD_Init(void);
void LCD_Reset(void);
// 背光开关
void LCD_BGOn(void);
void LCD_BGOff(void);
// 基础参数配置
void LCD_RegConfig(void);
// 写命令、字节
void LCD_WriteCmd(uint16_t cmd);
void LCD_WriteData(uint16_t data);
// 读数据
uint16_t LCD_ReadData(void);
// 读LCD设备ID
uint32_t LCD_ReadId(void);
// 设置显示区域
void LCD_SetArea(uint16_t x, uint16_t y, uint16_t width, uint16_t height);
// 清屏
void LCD_ClearAll(uint16_t color);
#endif /* __LCD_H__ */
lcd.c
#include "lcd.h"
static void LCD_GPIO_Config(void) {
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
/* LCD 复位和背光 通用推挽输出 MODE=11 CNF=00 */
// PG15 PB0
GPIOG->CRH |= GPIO_CRH_MODE15;
GPIOG->CRH &= ~GPIO_CRH_CNF15;
GPIOB->CRL |= GPIO_CRL_MODE0;
GPIOB->CRL &= ~GPIO_CRL_CNF0;
}
void LCD_Init(void) {
// 初始化FSMC,将LCD显存接入FMSC Bank1 NOR/PSRAM-4
FSMC_Init();
// 初始化LCD特定引脚
LCD_GPIO_Config();
// LCD硬件复位初始化(上电后需要初始化LCD)
LCD_Reset();
// 开启LCD背光
LCD_BGOn();
// 配置一些LCD基本参数
LCD_RegConfig();
}
void LCD_Reset(void) {
// 拉低RST,初始化LCD
GPIOG->ODR &= ~GPIO_ODR_ODR15;
delay_ms(100);
GPIOG->ODR |= GPIO_ODR_ODR15;
delay_ms(100);
}
void LCD_BGOn(void) { GPIOB->ODR |= GPIO_ODR_ODR0; }
void LCD_BGOff(void) { GPIOB->ODR &= ~GPIO_ODR_ODR0; }
void LCD_RegConfig(void) {
/* 1. 设置灰阶电压以调整TFT面板的伽马特性, 正校准。一般出厂就设置好了 */
LCD_WriteCmd(0xE0);
LCD_WriteData(0x00);
LCD_WriteData(0x07);
LCD_WriteData(0x10);
LCD_WriteData(0x09);
LCD_WriteData(0x17);
LCD_WriteData(0x0B);
LCD_WriteData(0x41);
LCD_WriteData(0x89);
LCD_WriteData(0x4B);
LCD_WriteData(0x0A);
LCD_WriteData(0x0C);
LCD_WriteData(0x0E);
LCD_WriteData(0x18);
LCD_WriteData(0x1B);
LCD_WriteData(0x0F);
/* 2. 设置灰阶电压以调整TFT面板的伽马特性,负校准 */
LCD_WriteCmd(0XE1);
LCD_WriteData(0x00);
LCD_WriteData(0x17);
LCD_WriteData(0x1A);
LCD_WriteData(0x04);
LCD_WriteData(0x0E);
LCD_WriteData(0x06);
LCD_WriteData(0x2F);
LCD_WriteData(0x45);
LCD_WriteData(0x43);
LCD_WriteData(0x02);
LCD_WriteData(0x0A);
LCD_WriteData(0x09);
LCD_WriteData(0x32);
LCD_WriteData(0x36);
LCD_WriteData(0x0F);
/* 3. Adjust Control 3 (F7h) */
/*LCD_WriteCmd(0XF7);
LCD_WriteData(0xA9);
LCD_WriteData(0x51);
LCD_WriteData(0x2C);
LCD_WriteData(0x82);*/
/* DSI write DCS command, use loose packet RGB 666 */
/* 4. 电源控制1*/
LCD_WriteCmd(0xC0);
LCD_WriteData(0x11); /* 正伽马电压 */
LCD_WriteData(0x09); /* 负伽马电压 */
/* 5. 电源控制2 */
LCD_WriteCmd(0xC1);
LCD_WriteData(0x02);
LCD_WriteData(0x03);
/* 6. VCOM控制 */
LCD_WriteCmd(0XC5);
LCD_WriteData(0x00);
LCD_WriteData(0x0A);
LCD_WriteData(0x80);
/* 7. Frame Rate Control (In Normal Mode/Full Colors) (B1h) */
LCD_WriteCmd(0xB1);
LCD_WriteData(0xB0);
LCD_WriteData(0x11);
/* 8. Display Inversion Control (B4h) (正负电压反转,减少电磁干扰)*/
LCD_WriteCmd(0xB4);
LCD_WriteData(0x02);
/* 9. Display Function Control (B6h) */
LCD_WriteCmd(0xB6);
LCD_WriteData(0x0A);
LCD_WriteData(0xA2);
/* 10. Entry Mode Set (B7h) */
LCD_WriteCmd(0xB7);
LCD_WriteData(0xc6);
/* 11. HS Lanes Control (BEh) */
LCD_WriteCmd(0xBE);
LCD_WriteData(0x00);
LCD_WriteData(0x04);
/* 12. Interface Pixel Format (3Ah) */
LCD_WriteCmd(0x3A);
LCD_WriteData(0x55); /* 0x55 : 16 bits/pixel */
/* 13. Sleep Out (11h) 关闭休眠模式 */
LCD_WriteCmd(0x11);
/* 14. 设置屏幕方向和RGB */
LCD_WriteCmd(0x36);
LCD_WriteData(0x08);
delay_ms(120);
/* 14. display on */
LCD_WriteCmd(0x29);
}
void LCD_WriteCmd(uint16_t cmd) { *LCD_ADDR_CMD = cmd; }
void LCD_WriteData(uint16_t data) { *LCD_ADDR_DATA = data; }
uint16_t LCD_ReadData(void) { return *LCD_ADDR_DATA; }
uint32_t LCD_ReadId(void) {
LCD_WriteCmd(0x04);
LCD_ReadData(); // dummy read
uint32_t lcdId = 0;
lcdId |= (LCD_ReadData() & 0xFF) << 16;
lcdId |= (LCD_ReadData() & 0xFF) << 8;
lcdId |= (LCD_ReadData() & 0xFF);
return lcdId;
}
void LCD_SetArea(uint16_t x, uint16_t y, uint16_t width, uint16_t height) {
// 设置列范围
LCD_WriteCmd(0x2A);
// Start Column 的高位字节、低位字节
LCD_WriteData((x >> 8) & 0xFF);
LCD_WriteData(x & 0xFF);
// End Column 的高位字节、低位字节
LCD_WriteData(((x + width - 1) >> 8) & 0xFF);
LCD_WriteData((x + width - 1) & 0xFF);
// 设置行范围
LCD_WriteCmd(0x2B);
// Start Page 的高位字节、低位字节
LCD_WriteData((y >> 8) & 0xFF);
LCD_WriteData(y & 0xFF);
// End Page 的高位字节、低位字节
LCD_WriteData(((y + height - 1) >> 8) & 0xFF);
LCD_WriteData((y + height - 1) & 0xFF);
}
void LCD_ClearAll(uint16_t color) {
LCD_SetArea(0, 0, DISPLAY_W, DISPLAY_H);
// 写内存
LCD_WriteCmd(0x2C);
for (uint32_t i = 0, count = DISPLAY_W * DISPLAY_H; i < count; i++) {
LCD_WriteData(color);
}
}
测试——读取LCD ID & 指定颜色清屏
#include "lcd.h"
#include "logger.h"
#include "stm32f10x.h"
#include "uart.h"
void uart1_received_callback(uint8_t buf[], uint8_t size) {}
int main() {
uart_init();
LCD_Init();
LOG_DEBUG("main start")
uint32_t lcdId = LCD_ReadId();
LOG_DEBUG("lcdId = %#x", lcdId)
LCD_ClearAll(MAGENTA);
while (1) {
}
}
绘制Ascii字符
绘制单个字符
[!NOTE]
取模说明:从第一行开始向右每取8个点作为一个字节,如果最后不足8个点就补满8位。 取模顺序是从低到高 ,即第一个点作为最低位。如
*-------
取为00000001
。代码中使用的字模见附录。
void LCD_DisplayAsciiChar(uint16_t x, uint16_t y, uint16_t height, uint8_t ch,
uint16_t textColor, uint16_t bgColor) {
LCD_SetArea(x, y, height / 2, height);
// 写显存
LCD_WriteCmd(0x2C);
uint8_t index = ch - ' '; // 字符在码标中的索引
if (height == 12 || height == 16) {
uint8_t *dotMatrix =
(uint8_t *)(height == 12 ? ascii_1206[index] : ascii_1608[index]);
for (uint8_t i = 0; i < height; i++) {
// 生成的码表中,列编码采用逆序,因此逐次判断字节的低位
uint8_t colByte = dotMatrix[i];
for (uint8_t j = 0; j < height / 2; j++) {
if (colByte & 0x01) {
LCD_WriteData(textColor);
} else {
LCD_WriteData(bgColor);
}
colByte >>= 1;
}
}
} else if (height == 24 || height == 32) {
uint8_t *dotMatrix =
(uint8_t *)(height == 12 ? ascii_2412[index] : ascii_3216[index]);
// 列为12或16,对应两个字节,因此要循环的字节数为 (height * 2)
for (uint8_t i = 0; i < height * 2; i++) {
// 生成的码表中,列编码采用逆序,因此逐次判断字节的低位
uint8_t colByte = dotMatrix[i];
// 如果宽度为24/2=12,则奇数字节只取低4位
uint8_t colCount = 8;
if (height == 24 && (i % 2) == 1) {
colCount = 4;
}
for (uint8_t j = 0; j < colCount; j++) {
if (colByte & 0x01) {
LCD_WriteData(textColor);
} else {
LCD_WriteData(bgColor);
}
colByte >>= 1;
}
}
}
}
int main() {
uart_init();
LCD_Init();
LOG_DEBUG("main start")
uint32_t lcdId = LCD_ReadId();
LOG_DEBUG("lcdId = %#x", lcdId)
LCD_ClearAll(WHITE);
LCD_DisplayAsciiChar(10, 10, 12, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 22, 16, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 38, 24, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 62, 32, 'A', MAGENTA, WHITE);
LOG_DEBUG("display done")
while (1) {
}
}
绘制字符串
void LCD_DisplayAsciiStr(uint16_t x, uint16_t y, uint16_t height, char *str,
uint16_t textColor, uint16_t bgColor) {
uint16_t width = height / 2;
while (*str) {
if (*str == '\n' || x + width > DISPLAY_W) {
x = 0;
y += height;
if (*str == '\n') {
str++;
continue;
}
}
LCD_DisplayAsciiChar(x, y, height, *str, textColor, bgColor);
x += width;
str++;
}
}
LCD_ClearAll(WHITE);
LCD_DisplayAsciiChar(10, 10, 12, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 22, 16, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 38, 24, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 62, 32, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiStr(200, 200, 32,
"Hello World! All is well!\nEnjoy yourself!", MAGENTA,
WHITE);
绘制中文
[!NOTE]
代码中引用的字模请见附录。
void LCD_DisplayChinese(uint16_t x, uint16_t y, uint16_t height, uint8_t index,
uint16_t textColor, uint16_t bgColor) {
LCD_SetArea(x, y, height, height);
// 写显存
LCD_WriteCmd(0x2C);
const uint8_t *bytes = chineses[index];
for (uint8_t i = 0; i < 128; i++) {
uint8_t colByte = bytes[i];
for (uint8_t j = 0; j < 8; j++) {
if (colByte & 0x01) {
LCD_WriteData(textColor);
} else {
LCD_WriteData(bgColor);
}
colByte >>= 1;
}
}
}
for (uint8_t i = 0; i < 7; i++) {
LCD_DisplayChinese(i * 32, 300, 32, i, RED, WHITE);
}
绘制图片
void LCD_DisplayImg(uint16_t x, uint16_t y) {
LCD_SetArea(x, y, 240, 240);
// 写显存
LCD_WriteCmd(0x2C);
// 像素240*240,每个像素使用16bit真彩格式
for (uint32_t i = 0; i < 240 * 240 * 2; i += 2) {
// 将相邻的两个字节组装为16bit真彩格式
LCD_WriteData((gImage_img[i] << 8) | gImage_img[i + 1]);
}
}
LCD_DisplayImg(50, 50);
绘制几何图形——点/线/矩形/圆
lcd.c
void LCD_DrawPoint(uint16_t x, uint16_t y, uint8_t w, uint16_t textColor) {
LCD_SetArea(x, y, w, w);
// 写显存
LCD_WriteCmd(0x2C);
for (uint32_t i = 0; i < w * w; i++) {
LCD_WriteData(textColor);
}
}
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint8_t w,
uint16_t textColor) {
if (x1 == x2) {
// 斜率不存在
for (uint16_t i = y1; i <= y2; i++) {
LCD_DrawPoint(x1, i, w, textColor);
}
return;
}
double k = (y1 - y2) / (x1 - x2);
double b = y1 - k * x1;
for (uint16_t x = x1; x <= x2; x++) {
uint16_t y = (uint16_t)(k * x + b);
LCD_DrawPoint(x, y, w, textColor);
}
}
void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
uint8_t w, uint16_t textColor) {
LCD_DrawLine(x1, y1, x2, y1, w, textColor);
LCD_DrawLine(x1, y2, x2, y2, w, textColor);
LCD_DrawLine(x1, y1, x1, y2, w, textColor);
LCD_DrawLine(x2, y1, x2, y2, w, textColor);
}
void LCD_DrawHollowCircle(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor) {
// 极坐标方程 x = x0 + rcosθ, y = y0 + rsinθ
// θ => [0, π/2], α => [0, 90°]
for (uint8_t a = 0; a <= 90; a++) {
uint16_t deltaX = cos(3.14 * a / 180) * r;
uint16_t deltaY = sin(3.14 * a / 180) * r;
// 确定四个象限中对应的点
LCD_DrawPoint(x + deltaX, y + deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x + deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y + deltaY, w, textColor);
}
}
void LCD_DrawSolidCircle(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor, uint16_t bgColor) {
LCD_DrawPoint(x, y, w, bgColor);
for (uint16_t i = 1; i < r; i++) {
LCD_DrawHollowCircle(x, y, i, w, bgColor);
}
LCD_DrawHollowCircle(x, y, r, w, textColor);
}
void LCD_DrawSolidCirclePlus(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor, uint16_t bgColor) {
for (uint8_t a = 0; a <= 90; a++) {
uint16_t deltaX = cos(3.14 * a / 180) * r;
uint16_t deltaY = sin(3.14 * a / 180) * r;
// 通过直线来画圆
// 一二象限的点连线
LCD_DrawLine(x - deltaX, y + deltaY, x + deltaX, y + deltaY, w, bgColor);
// 三四象限的点连线
LCD_DrawLine(x - deltaX, y - deltaY, x + deltaX, y - deltaY, w, bgColor);
// 线段两端端点颜色(圆周颜色)
LCD_DrawPoint(x + deltaX, y + deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x + deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y + deltaY, w, textColor);
}
}
main.c
LCD_DrawPoint(100, 100, 5, BLUE);
// 斜线
LCD_DrawLine(20, 20, 200, 200, 5, GREEN);
// 垂线
LCD_DrawLine(20, 20, 20, 200, 5, BLUE);
// 矩形
LCD_DrawRectangle(50, 50, 250, 250, 5, RED);
// 空心圆
LCD_DrawHollowCircle(100, 100, 50, 2, RED);
// 实心圆
// LCD_DrawSolidCircle(100, 350, 50, 2, BLUE, RED);
LCD_DrawSolidCirclePlus(100, 350, 50, 2, BLUE, RED);
完整示例代码
fsmc.h
#ifndef __FSMC_H__
#define __FSMC_H__
#include "stm32f10x.h"
void FSMC_Init(void);
#endif /* __FSMC_H__ */
fsmc.c
#include "fsmc.h"
static void FSMC_RCC_Config(void);
static void FSMC_GPIO_Config(void);
static void FSMC_BCR_Config(void);
static void FSMC_BTR_Config(void);
void FSMC_Init(void) {
FSMC_RCC_Config();
FSMC_GPIO_Config();
FSMC_BTR_Config();
FSMC_BCR_Config();
}
void FSMC_RCC_Config(void) {
// 开启FSMC和GPIO时钟-DEFG
RCC->AHBENR |= RCC_AHBENR_FSMCEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPDEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPEEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
}
void FSMC_GPIO_Config(void) {
/* 1 配置 A10 地址端口的输出模式 复用推挽输出CNF:10 50MHz速度 MODE:11*/
GPIOG->CRL |= GPIO_CRL_MODE0;
GPIOG->CRL |= GPIO_CRL_CNF0_1;
GPIOG->CRL &= ~GPIO_CRL_CNF0_0;
/*
2 数据端口 复用推挽输出
在实际应用中,即使数据线被配置为输出模式,FSMC控制器仍然能够管理数据线的方向,使其在需要时成为输入线。
这种自动切换是由FSMC控制器硬件管理的,不需要软件干预。
因此,即使GPIO配置为复用推挽输出,FSMC依然可以实现读取操作。
*/
/* =============MODE=============== */
GPIOD->CRL |= (GPIO_CRL_MODE0 | GPIO_CRL_MODE1);
GPIOD->CRH |= (GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 |
GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
GPIOE->CRL |= (GPIO_CRL_MODE7);
GPIOE->CRH |=
(GPIO_CRH_MODE8 | GPIO_CRH_MODE9 | GPIO_CRH_MODE10 | GPIO_CRH_MODE11 |
GPIO_CRH_MODE12 | GPIO_CRH_MODE13 | GPIO_CRH_MODE14 | GPIO_CRH_MODE15);
/* =============CNF=============== */
GPIOD->CRL |= (GPIO_CRL_CNF0_1 | GPIO_CRL_CNF1_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF0_0 | GPIO_CRL_CNF1_0);
GPIOD->CRH |= (GPIO_CRH_CNF8_1 | GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF14_1 | GPIO_CRH_CNF15_1);
GPIOD->CRH &= ~(GPIO_CRH_CNF8_0 | GPIO_CRH_CNF9_0 | GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_0);
GPIOE->CRL |= (GPIO_CRL_CNF7_1);
GPIOE->CRL &= ~(GPIO_CRL_CNF7_0);
GPIOE->CRH |= (GPIO_CRH_CNF8_1 | GPIO_CRH_CNF9_1 | GPIO_CRH_CNF10_1 |
GPIO_CRH_CNF11_1 | GPIO_CRH_CNF12_1 | GPIO_CRH_CNF13_1 |
GPIO_CRH_CNF14_1 | GPIO_CRH_CNF15_1);
GPIOE->CRH &= ~(GPIO_CRH_CNF8_0 | GPIO_CRH_CNF9_0 | GPIO_CRH_CNF10_0 |
GPIO_CRH_CNF11_0 | GPIO_CRH_CNF12_0 | GPIO_CRH_CNF13_0 |
GPIO_CRH_CNF14_0 | GPIO_CRH_CNF15_0);
/* 3 其他控制端口 复用推挽输出 */
// NOE NWE PD4 PD5
GPIOD->CRL |= (GPIO_CRL_MODE4 | GPIO_CRL_MODE5);
GPIOD->CRL |= (GPIO_CRL_CNF4_1 | GPIO_CRL_CNF5_1);
GPIOD->CRL &= ~(GPIO_CRL_CNF4_0 | GPIO_CRL_CNF5_0);
// NE4 PG12
GPIOG->CRH |= (GPIO_CRH_MODE12);
GPIOG->CRH |= (GPIO_CRH_CNF12_1);
GPIOG->CRH &= ~(GPIO_CRH_CNF12_0);
}
void FSMC_BCR_Config(void) {
// 配置 BCR4 => BTCR[6]
// 开启写使能,允许向SRAM写入数据
FSMC_Bank1->BTCR[6] |= FSMC_BCR4_WREN;
// 配置存储器数据总线宽度为16
FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MWID;
FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MWID_0;
// 配置存储器类型为SRAM
FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MTYP;
// 地址-数据复用使能:关闭
FSMC_Bank1->BTCR[6] &= ~FSMC_BCR4_MUXEN;
// 使能存储器对应的Bank
FSMC_Bank1->BTCR[6] |= FSMC_BCR4_MBKEN;
}
void FSMC_BTR_Config(void) {
// 配置 BTR4 => BTCR[7]
// 地址访问/建立时间(HCLK数量)配置为0,实际为(0+1)HCLK
FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_ADDSET;
// 数据访问/建立时间配置为3 => 0011,实际为(3+1)HCLK
FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_DATAST;
FSMC_Bank1->BTCR[7] |= FSMC_BTR4_DATAST_0;
FSMC_Bank1->BTCR[7] |= FSMC_BTR4_DATAST_1;
// 总线读-写切换空闲时间配置为0 => (0+1)HCLK
FSMC_Bank1->BTCR[7] &= ~FSMC_BTR4_BUSTURN;
}
lcd.h
#ifndef __LCD_H__
#define __LCD_H__
#include "delay.h"
#include "fsmc.h"
#include <math.h>
#define SRAM_BANK4 0x6C000000
// PA10为0:发送命令
#define LCD_ADDR_CMD ((volatile uint16_t *)SRAM_BANK4)
// PA10为1:发送数据(SRAM数据总线宽度为16时,FSMC会将AHB地址线[25:1]>>1输入到SRAM,因此这里的bit12对应FSMC的PA10-bit11)
#define LCD_ADDR_DATA ((volatile uint16_t *)(SRAM_BANK4 | (0x01 << 11)))
#define DISPLAY_W 320
#define DISPLAY_H 480
// 画笔颜色
#define WHITE 0xFFFF
#define BLACK 0x0000
#define BLUE 0x001F
#define BRED 0XF81F
#define GRED 0XFFE0
#define GBLUE 0X07FF
#define RED 0xF800
#define MAGENTA 0xF81F
#define GREEN 0x07E0
#define CYAN 0x7FFF
#define YELLOW 0xFFE0
#define BROWN 0XBC40 // 棕色
#define BRRED 0XFC07 // 棕红色
#define GRAY 0X8430 // 灰色
void LCD_Init(void);
void LCD_Reset(void);
void LCD_BGOn(void);
void LCD_BGOff(void);
void LCD_RegConfig(void);
void LCD_WriteCmd(uint16_t cmd);
void LCD_WriteData(uint16_t data);
uint16_t LCD_ReadData(void);
uint32_t LCD_ReadId(void);
void LCD_SetArea(uint16_t x, uint16_t y, uint16_t width, uint16_t height);
void LCD_ClearAll(uint16_t color);
void LCD_DisplayAsciiChar(uint16_t x, uint16_t y, uint16_t height, uint8_t ch,
uint16_t textColor, uint16_t bgColor);
void LCD_DisplayAsciiStr(uint16_t x, uint16_t y, uint16_t height, char *str,
uint16_t textColor, uint16_t bgColor);
void LCD_DisplayChinese(uint16_t x, uint16_t y, uint16_t height, uint8_t index,
uint16_t textColor, uint16_t bgColor);
void LCD_DisplayImg(uint16_t x, uint16_t y);
void LCD_DrawPoint(uint16_t x, uint16_t y, uint8_t w, uint16_t textColor);
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint8_t w,
uint16_t textColor);
void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
uint8_t w, uint16_t textColor);
// 空心圆
void LCD_DrawHollowCircle(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor);
// 实心圆
void LCD_DrawSolidCircle(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor, uint16_t bgColor);
void LCD_DrawSolidCirclePlus(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor, uint16_t bgColor);
#endif /* __LCD_H__ */
lcd.c
#include "lcd.h"
// #include "img.h"
#include "lcd_font.h"
#include "logger.h"
static void LCD_GPIO_Config(void) {
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
/* LCD 复位和背光 通用推挽输出 MODE=11 CNF=00 */
// PG15 PB0
GPIOG->CRH |= GPIO_CRH_MODE15;
GPIOG->CRH &= ~GPIO_CRH_CNF15;
GPIOB->CRL |= GPIO_CRL_MODE0;
GPIOB->CRL &= ~GPIO_CRL_CNF0;
}
void LCD_Init(void) {
// 初始化FSMC,将LCD显存接入FMSC Bank1 NOR/PSRAM-4
FSMC_Init();
// 初始化LCD特定引脚
LCD_GPIO_Config();
// LCD硬件复位初始化(上电后需要初始化LCD)
LCD_Reset();
// 开启LCD背光
LCD_BGOn();
// 配置一些LCD基本参数
LCD_RegConfig();
}
void LCD_Reset(void) {
// 拉低RST,初始化LCD
GPIOG->ODR &= ~GPIO_ODR_ODR15;
delay_ms(100);
GPIOG->ODR |= GPIO_ODR_ODR15;
delay_ms(100);
}
void LCD_BGOn(void) { GPIOB->ODR |= GPIO_ODR_ODR0; }
void LCD_BGOff(void) { GPIOB->ODR &= ~GPIO_ODR_ODR0; }
void LCD_RegConfig(void) {
/* 1. 设置灰阶电压以调整TFT面板的伽马特性, 正校准。一般出厂就设置好了 */
LCD_WriteCmd(0xE0);
LCD_WriteData(0x00);
LCD_WriteData(0x07);
LCD_WriteData(0x10);
LCD_WriteData(0x09);
LCD_WriteData(0x17);
LCD_WriteData(0x0B);
LCD_WriteData(0x41);
LCD_WriteData(0x89);
LCD_WriteData(0x4B);
LCD_WriteData(0x0A);
LCD_WriteData(0x0C);
LCD_WriteData(0x0E);
LCD_WriteData(0x18);
LCD_WriteData(0x1B);
LCD_WriteData(0x0F);
/* 2. 设置灰阶电压以调整TFT面板的伽马特性,负校准 */
LCD_WriteCmd(0XE1);
LCD_WriteData(0x00);
LCD_WriteData(0x17);
LCD_WriteData(0x1A);
LCD_WriteData(0x04);
LCD_WriteData(0x0E);
LCD_WriteData(0x06);
LCD_WriteData(0x2F);
LCD_WriteData(0x45);
LCD_WriteData(0x43);
LCD_WriteData(0x02);
LCD_WriteData(0x0A);
LCD_WriteData(0x09);
LCD_WriteData(0x32);
LCD_WriteData(0x36);
LCD_WriteData(0x0F);
/* 3. Adjust Control 3 (F7h) */
/*LCD_WriteCmd(0XF7);
LCD_WriteData(0xA9);
LCD_WriteData(0x51);
LCD_WriteData(0x2C);
LCD_WriteData(0x82);*/
/* DSI write DCS command, use loose packet RGB 666 */
/* 4. 电源控制1*/
LCD_WriteCmd(0xC0);
LCD_WriteData(0x11); /* 正伽马电压 */
LCD_WriteData(0x09); /* 负伽马电压 */
/* 5. 电源控制2 */
LCD_WriteCmd(0xC1);
LCD_WriteData(0x02);
LCD_WriteData(0x03);
/* 6. VCOM控制 */
LCD_WriteCmd(0XC5);
LCD_WriteData(0x00);
LCD_WriteData(0x0A);
LCD_WriteData(0x80);
/* 7. Frame Rate Control (In Normal Mode/Full Colors) (B1h) */
LCD_WriteCmd(0xB1);
LCD_WriteData(0xB0);
LCD_WriteData(0x11);
/* 8. Display Inversion Control (B4h) (正负电压反转,减少电磁干扰)*/
LCD_WriteCmd(0xB4);
LCD_WriteData(0x02);
/* 9. Display Function Control (B6h) */
LCD_WriteCmd(0xB6);
LCD_WriteData(0x0A);
LCD_WriteData(0xA2);
/* 10. Entry Mode Set (B7h) */
LCD_WriteCmd(0xB7);
LCD_WriteData(0xc6);
/* 11. HS Lanes Control (BEh) */
LCD_WriteCmd(0xBE);
LCD_WriteData(0x00);
LCD_WriteData(0x04);
/* 12. Interface Pixel Format (3Ah) */
LCD_WriteCmd(0x3A);
LCD_WriteData(0x55); /* 0x55 : 16 bits/pixel */
/* 13. Sleep Out (11h) 关闭休眠模式 */
LCD_WriteCmd(0x11);
/* 14. 设置屏幕方向和RGB */
LCD_WriteCmd(0x36);
LCD_WriteData(0x08);
delay_ms(120);
/* 14. display on */
LCD_WriteCmd(0x29);
}
void LCD_WriteCmd(uint16_t cmd) { *LCD_ADDR_CMD = cmd; }
void LCD_WriteData(uint16_t data) { *LCD_ADDR_DATA = data; }
uint16_t LCD_ReadData(void) { return *LCD_ADDR_DATA; }
uint32_t LCD_ReadId(void) {
LCD_WriteCmd(0x04);
LCD_ReadData(); // dummy read
uint32_t lcdId = 0;
lcdId |= (LCD_ReadData() & 0xFF) << 16;
lcdId |= (LCD_ReadData() & 0xFF) << 8;
lcdId |= (LCD_ReadData() & 0xFF);
return lcdId;
}
void LCD_SetArea(uint16_t x, uint16_t y, uint16_t width, uint16_t height) {
// 设置列范围
LCD_WriteCmd(0x2A);
// Start Column 的高位字节、低位字节
LCD_WriteData((x >> 8) & 0xFF);
LCD_WriteData(x & 0xFF);
// End Column 的高位字节、低位字节
LCD_WriteData(((x + width - 1) >> 8) & 0xFF);
LCD_WriteData((x + width - 1) & 0xFF);
// 设置行范围
LCD_WriteCmd(0x2B);
// Start Page 的高位字节、低位字节
LCD_WriteData((y >> 8) & 0xFF);
LCD_WriteData(y & 0xFF);
// End Page 的高位字节、低位字节
LCD_WriteData(((y + height - 1) >> 8) & 0xFF);
LCD_WriteData((y + height - 1) & 0xFF);
}
void LCD_ClearAll(uint16_t color) {
LCD_SetArea(0, 0, DISPLAY_W, DISPLAY_H);
// 写显存
LCD_WriteCmd(0x2C);
for (uint32_t i = 0, count = DISPLAY_W * DISPLAY_H; i < count; i++) {
LCD_WriteData(color);
}
}
void LCD_DisplayAsciiChar(uint16_t x, uint16_t y, uint16_t height, uint8_t ch,
uint16_t textColor, uint16_t bgColor) {
LCD_SetArea(x, y, height / 2, height);
// 写显存
LCD_WriteCmd(0x2C);
uint8_t index = ch - ' '; // 字符在码标中的索引
if (height == 12 || height == 16) {
uint8_t *dotMatrix =
(uint8_t *)(height == 12 ? ascii_1206[index] : ascii_1608[index]);
for (uint8_t i = 0; i < height; i++) {
// 生成的码表中,列编码采用逆序,因此逐次判断字节的低位
uint8_t colByte = dotMatrix[i];
for (uint8_t j = 0; j < height / 2; j++) {
if (colByte & 0x01) {
LCD_WriteData(textColor);
} else {
LCD_WriteData(bgColor);
}
colByte >>= 1;
}
}
} else if (height == 24 || height == 32) {
uint8_t *dotMatrix =
(uint8_t *)(height == 12 ? ascii_2412[index] : ascii_3216[index]);
// 列为12或16,对应两个字节,因此要循环的字节数为 (height * 2)
for (uint8_t i = 0; i < height * 2; i++) {
// 生成的码表中,列编码采用逆序,因此逐次判断字节的低位
uint8_t colByte = dotMatrix[i];
// 如果宽度为24/2=12,则奇数字节只取低4位
uint8_t colCount = 8;
if (height == 24 && (i % 2) == 1) {
colCount = 4;
}
for (uint8_t j = 0; j < colCount; j++) {
if (colByte & 0x01) {
LCD_WriteData(textColor);
} else {
LCD_WriteData(bgColor);
}
colByte >>= 1;
}
}
}
}
void LCD_DisplayAsciiStr(uint16_t x, uint16_t y, uint16_t height, char *str,
uint16_t textColor, uint16_t bgColor) {
uint16_t width = height / 2;
while (*str) {
if (*str == '\n' || x + width > DISPLAY_W) {
x = 0;
y += height;
if (*str == '\n') {
str++;
continue;
}
}
LCD_DisplayAsciiChar(x, y, height, *str, textColor, bgColor);
x += width;
str++;
}
}
void LCD_DisplayChinese(uint16_t x, uint16_t y, uint16_t height, uint8_t index,
uint16_t textColor, uint16_t bgColor) {
LCD_SetArea(x, y, height, height);
// 写显存
LCD_WriteCmd(0x2C);
const uint8_t *bytes = chineses[index];
for (uint8_t i = 0; i < 128; i++) {
uint8_t colByte = bytes[i];
for (uint8_t j = 0; j < 8; j++) {
if (colByte & 0x01) {
LCD_WriteData(textColor);
} else {
LCD_WriteData(bgColor);
}
colByte >>= 1;
}
}
}
void LCD_DisplayImg(uint16_t x, uint16_t y) {
LCD_SetArea(x, y, 240, 240);
// 写显存
LCD_WriteCmd(0x2C);
// 像素240*240,每个像素使用16bit真彩格式
// for (uint32_t i = 0; i < 240 * 240 * 2; i += 2) {
// // 将相邻的两个字节组装为16bit真彩格式
// LCD_WriteData((gImage_img[i] << 8) | gImage_img[i + 1]);
// }
}
void LCD_DrawPoint(uint16_t x, uint16_t y, uint8_t w, uint16_t textColor) {
LCD_SetArea(x, y, w, w);
// 写显存
LCD_WriteCmd(0x2C);
for (uint32_t i = 0; i < w * w; i++) {
LCD_WriteData(textColor);
}
}
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint8_t w,
uint16_t textColor) {
if (x1 == x2) {
// 斜率不存在
for (uint16_t i = y1; i <= y2; i++) {
LCD_DrawPoint(x1, i, w, textColor);
}
return;
}
double k = (y1 - y2) / (x1 - x2);
double b = y1 - k * x1;
for (uint16_t x = x1; x <= x2; x++) {
uint16_t y = (uint16_t)(k * x + b);
LCD_DrawPoint(x, y, w, textColor);
}
}
void LCD_DrawRectangle(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2,
uint8_t w, uint16_t textColor) {
LCD_DrawLine(x1, y1, x2, y1, w, textColor);
LCD_DrawLine(x1, y2, x2, y2, w, textColor);
LCD_DrawLine(x1, y1, x1, y2, w, textColor);
LCD_DrawLine(x2, y1, x2, y2, w, textColor);
}
void LCD_DrawHollowCircle(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor) {
// 极坐标方程 x = x0 + rcosθ, y = y0 + rsinθ
// θ => [0, π/2], α => [0, 90°]
for (uint8_t a = 0; a <= 90; a++) {
uint16_t deltaX = cos(3.14 * a / 180) * r;
uint16_t deltaY = sin(3.14 * a / 180) * r;
// 确定四个象限中对应的点
LCD_DrawPoint(x + deltaX, y + deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x + deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y + deltaY, w, textColor);
}
}
void LCD_DrawSolidCircle(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor, uint16_t bgColor) {
LCD_DrawPoint(x, y, w, bgColor);
for (uint16_t i = 1; i < r; i++) {
LCD_DrawHollowCircle(x, y, i, w, bgColor);
}
LCD_DrawHollowCircle(x, y, r, w, textColor);
}
void LCD_DrawSolidCirclePlus(uint16_t x, uint16_t y, uint16_t r, uint8_t w,
uint16_t textColor, uint16_t bgColor) {
for (uint8_t a = 0; a <= 90; a++) {
uint16_t deltaX = cos(3.14 * a / 180) * r;
uint16_t deltaY = sin(3.14 * a / 180) * r;
// 通过直线来画圆
// 一二象限的点连线
LCD_DrawLine(x - deltaX, y + deltaY, x + deltaX, y + deltaY, w, bgColor);
// 三四象限的点连线
LCD_DrawLine(x - deltaX, y - deltaY, x + deltaX, y - deltaY, w, bgColor);
// 线段两端端点颜色(圆周颜色)
LCD_DrawPoint(x + deltaX, y + deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x + deltaX, y - deltaY, w, textColor);
LCD_DrawPoint(x - deltaX, y + deltaY, w, textColor);
}
}
main.c
#include "lcd.h"
#include "logger.h"
#include "stm32f10x.h"
#include "uart.h"
void uart1_received_callback(uint8_t buf[], uint8_t size) {}
int main() {
uart_init();
LCD_Init();
LOG_DEBUG("main start")
uint32_t lcdId = LCD_ReadId();
LOG_DEBUG("lcdId = %#x", lcdId)
LCD_ClearAll(WHITE);
LCD_DisplayAsciiChar(10, 10, 12, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 22, 16, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 38, 24, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 62, 32, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiStr(200, 200, 32,
"Hello World! All is well!\nEnjoy yourself!", MAGENTA,
WHITE);
for (uint8_t i = 0; i < 7; i++) {
LCD_DisplayChinese(i * 32, 300, 32, i, RED, WHITE);
}
// LCD_DisplayImg(50, 50);
LCD_DrawPoint(100, 100, 5, BLUE);
// 斜线
LCD_DrawLine(20, 20, 200, 200, 5, GREEN);
// 垂线
LCD_DrawLine(20, 20, 20, 200, 5, BLUE);
// 矩形
LCD_DrawRectangle(50, 50, 250, 250, 5, RED);
// 空心圆
LCD_DrawHollowCircle(100, 100, 50, 2, RED);
// 实心圆
// LCD_DrawSolidCircle(100, 350, 50, 2, BLUE, RED);
LCD_DrawSolidCirclePlus(100, 350, 50, 2, BLUE, RED);
LOG_DEBUG("display done")
while (1) {
}
}
字模
-
字符字模:https://github.com/zanwen/Miscellaneous/blob/main/HexoBlog/lcd_font.h
-
图片字模:https://github.com/zanwen/Miscellaneous/blob/main/HexoBlog/lcd_img.h
HAL库实现
FSMC配置
LCD特定引脚配置
示例代码
void LCD_Init(void) {
// 初始化FSMC,将LCD显存接入FMSC Bank1 NOR/PSRAM-4
FSMC_Init();
// 初始化LCD特定引脚
LCD_GPIO_Config();
// LCD硬件复位初始化(上电后需要初始化LCD)
LCD_Reset();
// 开启LCD背光
LCD_BGOn();
// 配置一些LCD基本参数
LCD_RegConfig();
}
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_FSMC_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
LCD_Init();
uint32_t lcdId = LCD_ReadId();
printf("lcdId = %#x\n", lcdId);
LCD_ClearAll(WHITE);
LCD_DisplayAsciiChar(10, 10, 12, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 22, 16, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 38, 24, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiChar(10, 62, 32, 'A', MAGENTA, WHITE);
LCD_DisplayAsciiStr(200, 200, 32,
"Hello World! All is well!\nEnjoy yourself!", MAGENTA,
WHITE);
for (uint8_t i = 0; i < 7; i++) {
LCD_DisplayChinese(i * 32, 300, 32, i, RED, WHITE);
}
// LCD_DisplayImg(50, 50);
LCD_DrawPoint(100, 100, 5, BLUE);
// 斜线
LCD_DrawLine(20, 20, 200, 200, 5, GREEN);
// 垂线
LCD_DrawLine(20, 20, 20, 200, 5, BLUE);
// 矩形
LCD_DrawRectangle(50, 50, 250, 250, 5, RED);
// 空心圆
LCD_DrawHollowCircle(100, 100, 50, 2, RED);
// 实心圆
// LCD_DrawSolidCircle(100, 350, 50, 2, BLUE, RED);
LCD_DrawSolidCirclePlus(100, 350, 50, 2, BLUE, RED);
printf("display done\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
编译优化级别导致的bug
[!WARNING]
CubeMX生成的MDK项目,编译器优化选项为
-O3
(最大程度优化),可能会导致程序被过度优化,可以调整为默认优化级别-O2
或不优化。可以配合Keil的调试功能,单步调试确认FSMC相关的寄存器是否如期配置了,以及LCD初始化时
LCD_RegConfig
中的程序是否因编译器优化而没有执行。
[!NOTE]
如上,有时编译器优化会认为重复调用同一函数是无效代码,从而将前面n-1的调用直接优化为不执行。
遗留问题
FSMC BUSTURN参数
1. IS62WV51216BLL 的关键参数
查看 IS62WV51216BLL-55TLI 数据手册可知:
- 55ns 表示其最短的访问时间(Access Time)为 55ns。
- 这是片选有效到数据有效的时间。
- 对于 FSMC 配置,关键参数包括:
- Address Setup Time (tAS):地址稳定到写/读使能的时间。
- Data Setup Time (tDS):数据必须保持稳定的时间。
- Access Time (tACC):从片选有效到数据有效的时间。
2. FSMC 的相关时序参数
FSMC 提供一系列寄存器用于配置与外部存储器的时序:
- ADDSET(Address Setup Time):地址信号建立到第一次时钟边沿的时间。
- DATAST(Data Setup Time):数据稳定有效的时间。
- BUSRUN(Bus Turnaround Duration):用于总线方向切换的时间。
- CLKDIV(Clock Divide Ratio):外部存储器访问的时钟分频。
- ACCMOD(Access Mode):访问模式,包括同步/异步。
其中,BUSRUN 的作用是确保在读/写方向切换时,有足够的等待时间,避免总线争用。
3. BUSRUN 的配置思路
使用场景
- BUSRUN 参数适用于以下场景:
- 读/写切换:CPU 从写操作切换到读操作时,需要总线方向切换时间。
- 多设备共享总线:如果多个设备共用 FSMC 的数据总线,切换时可能需要加入时间缓冲。
典型情况
对于 IS62WV51216BLL-55TLI:
- 方向切换时间通常在 10ns 以下。
- STM32F103 的 HCLK 假设为 72MHz(周期约为 13.88ns)。
- 因此,BUSRUN 通常设置为 1(对应 1 个 HCLK 周期)。
CubeMX生成的Keil编译优化级别导致程序没有被执行
[!NOTE]
附录
LCD字模
-
字符字模:https://github.com/zanwen/Miscellaneous/blob/main/HexoBlog/lcd_font.h
-
图片字模:https://github.com/zanwen/Miscellaneous/blob/main/HexoBlog/lcd_img.h