作为一名嵌入式开发工程师,我经常使用STM32系列单片机进行项目开发。今天我将分享一个完整的STM32F103RCT6开发流程,从工程创建到外设配置,再到1.8寸SPI液晶屏的驱动实现。这个方案已经在多个实际项目中验证过稳定性,特别适合初学者入门和中级开发者参考。
STM32F103RCT6属于STM32F1系列的中容量产品,具有256KB Flash和48KB RAM,主频可达72MHz。选择这款芯片主要基于以下几点考虑:
开发板需要准备:
使用STM32CubeMX创建工程是STM32开发的标准化流程,它能极大简化外设配置过程。以下是具体步骤:
打开CubeMX,点击"New Project",在芯片选择框中输入"STM32F103RCT6"并选择对应型号
在Pinout视图中配置关键系统外设:
时钟树配置:
工程生成设置:
注意:建议为每个外设生成独立的.c/.h文件,这样代码结构更清晰,后续维护更方便。
生成工程后,用Keil打开并进行以下关键配置:
魔术棒(Options for Target)配置:
调试器配置:
编译与下载:
在CubeMX中配置USART1(或其他可用串口):
生成代码后,HAL库会自动完成USART的初始化。我们需要验证串口是否正常工作:
c复制HAL_UART_Transmit(&huart1, (uint8_t*)"Hello\r\n", 7, 1000);
为了方便调试,我们需要重定向printf到串口输出。在main.c文件中添加以下代码:
c复制#include <stdio.h>
extern UART_HandleTypeDef huart1;
int __io_putchar(int ch) {
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
return ch;
}
int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
return len;
}
关键注意事项:
在实际开发中,我总结了几个串口调试的经验:
c复制printf("[%lu] Debug message\r\n", HAL_GetTick());
c复制printf("Value: 0x%02X\r\n", variable);
根据液晶屏的接口定义,我们需要连接以下信号线:
在CubeMX中将这6个引脚配置为GPIO Output:
将GPIO速度设置为High,以提高SPI通信速率。生成代码后,HAL库会自动初始化这些GPIO。
软件SPI通过GPIO模拟SPI时序,相比硬件SPI更灵活但速度较慢。以下是关键函数实现:
c复制void LCD_Writ_Bus(u8 dat) {
u8 i;
LCD_CS_Clr(); // 片选使能
for(i=0; i<8; i++) {
LCD_SCLK_Clr(); // 时钟拉低
if(dat & 0x80) LCD_MOSI_Set();
else LCD_MOSI_Clr();
LCD_SCLK_Set(); // 时钟上升沿,数据采样
dat <<= 1;
}
LCD_CS_Set(); // 片选禁用
}
这个函数实现了SPI的8位数据传输,注意:
不同型号的LCD需要不同的初始化序列。以下是典型的ST7735初始化代码:
c复制void LCD_Init(void) {
LCD_RES_Clr(); // 复位LCD
HAL_Delay(100);
LCD_RES_Set();
HAL_Delay(100);
LCD_BLK_Set(); // 开启背光
// 发送初始化命令序列
LCD_WR_REG(0x11); // Sleep out
HAL_Delay(120);
LCD_WR_REG(0xB1); // FRMCTR1
LCD_WR_DATA8(0x05);
LCD_WR_DATA8(0x3C);
LCD_WR_DATA8(0x3C);
// 更多初始化命令...
LCD_WR_REG(0x29); // Display on
}
实现基本的图形绘制功能是GUI开发的基础。以下是几个关键函数:
c复制void LCD_DrawPoint(u16 x,u16 y,u16 color) {
LCD_Address_Set(x,y,x,y);
LCD_WR_DATA(color);
}
c复制void LCD_DrawLine(u16 x1,u16 y1,u16 x2,u16 y2,u16 color) {
u16 t;
int xerr=0,yerr=0,delta_x,delta_y,distance;
int incx,incy,uRow,uCol;
delta_x=x2-x1;
delta_y=y2-y1;
uRow=x1;
uCol=y1;
if(delta_x>0) incx=1;
else if(delta_x==0) incx=0;
else {incx=-1;delta_x=-delta_x;}
if(delta_y>0) incy=1;
else if(delta_y==0) incy=0;
else {incy=-1;delta_y=-delta_y;}
if(delta_x>delta_y) distance=delta_x;
else distance=delta_y;
for(t=0;t<distance+1;t++) {
LCD_DrawPoint(uRow,uCol,color);
xerr+=delta_x;
yerr+=delta_y;
if(xerr>distance) {
xerr-=distance;
uRow+=incx;
}
if(yerr>distance) {
yerr-=distance;
uCol+=incy;
}
}
}
c复制void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color) {
u16 i,j;
LCD_Address_Set(xsta,ysta,xend-1,yend-1);
for(i=ysta;i<yend;i++) {
for(j=xsta;j<xend;j++) {
LCD_WR_DATA(color);
}
}
}
显示字符需要字库支持。我们通常使用两种字库:
以下是字符显示函数示例:
c复制void LCD_ShowChar(u16 x,u16 y,u8 num,u16 fc,u16 bc,u8 sizey,u8 mode) {
u8 temp,sizex,t,m=0;
u16 i,TypefaceNum;
u16 x0=x;
sizex=sizey/2;
TypefaceNum=(sizex/8+((sizex%8)?1:0))*sizey;
num=num-' '; // 计算在字库中的偏移量
LCD_Address_Set(x,y,x+sizex-1,y+sizey-1);
for(i=0;i<TypefaceNum;i++) {
if(sizey==12) temp=ascii_1206[num][i];
else if(sizey==16) temp=ascii_1608[num][i];
else if(sizey==24) temp=ascii_2412[num][i];
else if(sizey==32) temp=ascii_3216[num][i];
else return;
for(t=0;t<8;t++) {
if(!mode) {
if(temp&(0x01<<t)) LCD_WR_DATA(fc);
else LCD_WR_DATA(bc);
m++;
if(m%sizex==0) {
m=0;
break;
}
} else {
if(temp&(0x01<<t)) LCD_DrawPoint(x,y,fc);
x++;
if((x-x0)==sizex) {
x=x0;
y++;
break;
}
}
}
}
}
汉字显示原理类似,只是字模数据更大,通常使用16x16或24x24点阵。
提高刷新速度:
内存优化:
电源管理:
屏幕无显示:
显示花屏:
通信不稳定:
基于这个基础框架,可以进一步实现:
在实际项目中,我发现将显示逻辑与业务逻辑分离非常重要。可以采用分层架构:
这种结构使得代码更易维护,也方便在不同硬件平台间移植。