基于STM32F103C8T6标准库驱动2.4寸SPI TFT触摸屏:从零搭建嵌入式GUI交互系统

张开发
2026/5/14 22:10:44 15 分钟阅读

分享文章

基于STM32F103C8T6标准库驱动2.4寸SPI TFT触摸屏:从零搭建嵌入式GUI交互系统
1. 项目概述与硬件准备最近在做一个智能家居控制面板的项目选用了STM32F103C8T6这块性价比超高的开发板作为主控搭配2.4寸SPI接口的TFT触摸屏作为人机交互界面。说实话刚开始接触这个组合时踩了不少坑特别是用标准库开发的时候网上的资料远不如HAL库丰富。不过经过几周的摸索总算把整套系统跑通了现在就把完整经验分享给大家。先说说为什么选择这个硬件组合。STM32F103C8T6虽然是个老将了但72MHz主频、丰富的外设接口和超低的价格让它依然是入门嵌入式开发的绝佳选择。而2.4寸SPI TFT屏更是性价比之王240x320的分辨率足够显示丰富信息SPI接口又节省IO资源。两者结合完全可以做出一个功能完善的嵌入式GUI系统。必备硬件清单STM32F103C8T6最小系统板注意要带BOOT0跳线帽2.4寸SPI TFT触摸屏型号通常是ILI9341驱动ST-Link V2下载器杜邦线若干建议用彩色线区分功能5V/3.3V电源开发板USB供电也行这里特别提醒一下市面上TFT屏的接口定义可能不同我用的这款引脚顺序是1. GND 2. VCC 3. SCK 4. SDA 5. RES 6. DC 7. CS 8. BLK接线时一定要对照屏幕背面标注接错可能烧毁屏幕。我第一次就差点把5V接到GND上幸好及时发现。2. 开发环境搭建2.1 工具链安装我习惯用Keil MDK开发STM32项目虽然现在STM32CubeIDE也很流行但标准库项目还是Keil更顺手。安装时要注意下载Keil MDK-ARM建议5.25以上版本安装STM32F1系列器件支持包安装ST-Link驱动配置工程时选择STM32F103C8设备注意不是STM32F103C8T6器件库里没有T6后缀有个坑我踩过新建工程时默认用的ARM Compiler 6但标准库用这个编译器会报错。解决方法是右键工程→Options for Target→Target→ARM Compiler选择V5。2.2 标准库获取与配置ST官方已经不再维护标准库了但GitHub上还能找到完整包。我整理了一个稳定版本// 标准库核心文件 - Libraries/CMSIS/ // 内核相关 - Libraries/STM32F10x_StdPeriph_Driver/ // 外设驱动 - Project/STM32F10x_StdPeriph_Template/ // 工程模板把这些文件放到工程目录后需要修改几个关键配置在Options for Target→C/C→Define中添加USE_STDPERIPH_DRIVER,STM32F10X_MD在Include Paths中添加\Libraries\CMSIS\CM3\CoreSupport \Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x \Libraries\STM32F10x_StdPeriph_Driver\inc3. SPI驱动TFT屏幕3.1 硬件连接SPI接口接线要特别注意电平匹配。STM32F103C8T6是3.3V器件而有些屏幕是5V tolerant的。我的接法如下STM32引脚TFT屏引脚功能说明PA4CS片选PA5SCK时钟PA7MOSI数据输出PB0DC数据/命令选择NRSTRES复位3.3VVCC电源GNDGND地PB1BLK背光控制这里PB0和PB1我用来控制DC和背光你也可以换成其他GPIO。注意CS引脚如果不接硬件SPI的NSS就需要软件控制。3.2 SPI初始化代码标准库的SPI初始化比HAL库要繁琐些但理解后其实更灵活。以下是完整配置void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // 配置SCK/MOSI为复用推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // SPI参数配置 SPI_InitStructure.SPI_Direction SPI_Direction_1Line_Tx; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_High; // 根据屏幕手册调整 SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; // 根据屏幕手册调整 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 18MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }实测发现SPI时钟相位(CPHA)和极性(CPOL)设置不对会导致花屏建议先试SPI_MODE0CPOLLow, CPHA1Edge不行再换其他模式。4. TFT屏幕驱动实现4.1 底层通信函数驱动TFT屏需要实现几个基本函数// 写命令 void TFT_WriteCmd(uint8_t cmd) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); // DC0 GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS0 SPI_I2S_SendData(SPI1, cmd); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS1 } // 写数据 void TFT_WriteData(uint8_t data) { GPIO_SetBits(GPIOB, GPIO_Pin_0); // DC1 GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS0 SPI_I2S_SendData(SPI1, data); while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); GPIO_SetBits(GPIOA, GPIO_Pin_4); // CS1 }注意标准库的SPI发送函数不会自动等待传输完成必须手动检查TXE标志。我一开始没加这个等待导致数据错乱。4.2 屏幕初始化序列不同厂商的ILI9341初始化参数可能不同这里分享我调试成功的配置void TFT_Init(void) { // 硬件复位 GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 背光关闭 GPIO_ResetBits(GPIOA, GPIO_Pin_4); // CS0 GPIO_ResetBits(GPIOA, GPIO_Pin_2); // RES0 Delay_ms(100); GPIO_SetBits(GPIOA, GPIO_Pin_2); // RES1 Delay_ms(100); // 发送初始化命令 TFT_WriteCmd(0xCF); TFT_WriteData(0x00); TFT_WriteData(0xC1); TFT_WriteData(0X30); TFT_WriteCmd(0xED); TFT_WriteData(0x64); TFT_WriteData(0x03); TFT_WriteData(0X12); TFT_WriteCmd(0xE8); TFT_WriteData(0x85); TFT_WriteData(0x00); TFT_WriteData(0x78); // ... 更多初始化命令完整代码见文末链接 TFT_WriteCmd(0x29); // 开启显示 GPIO_SetBits(GPIOB, GPIO_Pin_1); // 背光开启 }有个小技巧初始化失败时可以先用逻辑分析仪抓SPI波形对照屏幕手册检查命令序列是否正确。我遇到过屏幕只亮背光不显示内容的问题最后发现是漏发了一个关键命令。5. 触摸功能实现5.1 触摸芯片驱动这款屏幕通常搭载XPT2046触摸控制器也是SPI接口。因为STM32的SPI外设有限我采用软件模拟SPI来实现uint16_t TP_Read_AD(uint8_t cmd) { uint16_t data 0; TP_CS_LOW(); TP_SPI_Write(cmd); Delay_us(6); data TP_SPI_Read() 8; data | TP_SPI_Read(); data 3; TP_CS_HIGH(); return data 0xFFF; }注意触摸芯片的SPI时序和TFT屏可能不同需要单独实现一套GPIO模拟的SPI函数。读取坐标时要多次采样取平均消除抖动void TP_Read_XY(uint16_t *x, uint16_t *y) { uint16_t xbuf[5], ybuf[5]; for(uint8_t i0; i5; i) { xbuf[i] TP_Read_AD(0xD0); // 读X ybuf[i] TP_Read_AD(0x90); // 读Y } // 排序后取中值 *x TP_Get_Median(xbuf, 5); *y TP_Get_Median(ybuf, 5); }5.2 触摸校准触摸屏必须校准才能准确反映坐标。我采用四点校准法在屏幕四个角依次显示校准点记录触摸原始值和理论值计算校准系数void TP_Calibrate(void) { uint16_t x[4], y[4]; // 触摸原始值 uint16_t xt[4] {50, 50, 230, 230}; // 理论X uint16_t yt[4] {50, 310, 50, 310}; // 理论Y // 采集四个点的触摸数据 for(uint8_t i0; i4; i) { Show_Calib_Point(xt[i], yt[i]); while(!TP_Scan()); x[i] TP_Read_X(); y[i] TP_Read_Y(); } // 计算校准参数 tp_dev.xfac (float)(xt[2]-xt[0])/(x[2]-x[0]); tp_dev.xoff xt[0] - tp_dev.xfac*x[0]; // Y轴同理... }校准参数需要保存到Flash否则每次上电都要重新校准。我选择存在STM32的最后一页Flashvoid TP_Save_Param(void) { FLASH_Unlock(); FLASH_ErasePage(0x0800FC00); FLASH_ProgramHalfWord(0x0800FC00, *(uint16_t*)tp_dev.xfac); // 保存其他参数... FLASH_Lock(); }6. 简易GUI框架搭建6.1 页面管理系统我设计了一个简单的页面管理系统通过链表管理多个页面typedef struct { void (*Load)(void); // 页面加载函数 void (*Draw)(void); // 页面绘制函数 void (*Handler)(void); // 事件处理函数 } PageType; PageType *current_page; void GUI_Run(void) { while(1) { current_page-Draw(); TP_Scan(); if(tp_dev.sta TP_PRES_DOWN) { current_page-Handler(); } } }每个页面实现自己的三个函数。比如主页面void MainPage_Load(void) { // 创建按钮等控件 Create_Button(10, 10, 100, 40, 设置, SETTING_BTN_ID); } void MainPage_Draw(void) { TFT_Clear(BLUE); Draw_All_Controls(); } void MainPage_Handler(void) { uint8_t btn_id Get_Touched_Button(); if(btn_id SETTING_BTN_ID) { current_page SettingPage; } }6.2 控件实现按钮是最基础的控件实现如下typedef struct { uint16_t x, y, width, height; char *text; uint8_t id; } ButtonType; ButtonType buttons[MAX_BUTTONS]; uint8_t btn_count 0; uint8_t Create_Button(uint16_t x, uint16_t y, uint16_t w, uint16_t h, char *text, uint8_t id) { if(btn_count MAX_BUTTONS) return 0; buttons[btn_count].x x; buttons[btn_count].y y; buttons[btn_count].width w; buttons[btn_count].height h; buttons[btn_count].text text; buttons[btn_count].id id; // 绘制按钮 TFT_DrawRectangle(x, y, xw, yh, WHITE); TFT_ShowString(x5, yh/2-8, text, WHITE, BLACK, 16); return btn_count; } uint8_t Get_Touched_Button(void) { if(!(tp_dev.sta TP_PRES_DOWN)) return 0; uint16_t x tp_dev.x[0]; uint16_t y tp_dev.y[0]; for(uint8_t i0; ibtn_count; i) { if(x buttons[i].x x buttons[i].x buttons[i].width y buttons[i].y y buttons[i].y buttons[i].height) { return buttons[i].id; } } return 0; }7. 性能优化技巧7.1 提高刷新速度SPI全速运行在18MHz时刷新整个屏幕(240x320x2153600字节)需要约85ms。通过以下优化可以提升体验局部刷新只更新变化区域void GUI_Update(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { TFT_SetWindow(x1, y1, x2, y2); // 只发送该区域数据... }使用DMA传输解放CPU资源void TFT_DMA_Send(uint8_t *data, uint16_t len) { DMA_DeInit(DMA1_Channel3); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)data; DMA_InitStructure.DMA_BufferSize len; DMA_Init(DMA1_Channel3, DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); }双缓冲机制在内存中完成绘制再一次性更新7.2 内存管理STM32F103C8T6只有20KB RAM需要精打细算使用__attribute__((section(.ccmram)))将帧缓冲区放到CCM RAM这块内存专供内核使用速度更快启用压缩字体存储比如只存ASCII 32-127的字符点阵使用位域存储控件状态节省空间typedef struct { uint16_t x : 9; // 0-319 uint16_t y : 9; // 0-239 uint16_t visible : 1; uint16_t enabled : 1; } ControlAttr;8. 项目进阶与扩展完成基础框架后可以添加更多实用功能多语言支持通过外部Flash存储不同语言的字符串资源void GUI_SetLanguage(LANG_TYPE lang) { current_lang lang; SPI_Flash_Read(lang_str_addr[lang], (uint8_t*)strings, STRING_COUNT*MAX_STR_LEN); }动画效果利用定时器实现过渡动画void GUI_Animate(uint16_t start, uint16_t end, uint16_t duration) { uint16_t step (end - start) / (duration / 10); for(uint16_t istart; i!end; istep) { // 更新位置 Delay_ms(10); } }文件系统通过SPI Flash实现图片资源存储void GUI_Show_Image(uint32_t addr) { SPI_Flash_Read(addr, buf, IMG_SIZE); TFT_DrawPicture(0, 0, 240, 320, buf); }这套系统我已经应用在几个实际项目中包括智能家居控制面板、工业HMI终端等。虽然功能比不上商用GUI库但胜在完全掌控、资源占用小特别适合对成本敏感的项目。

更多文章