1. 项目概述
在嵌入式开发中,LCD点阵屏是常见的人机交互界面。作为一名长期从事STM32开发的工程师,我发现很多初学者在实现基础图形绘制时会遇到困难。本文将详细讲解如何在UC1611S驱动的LCD点阵屏上实现三种基本线条绘制:横线、竖线和斜线。
这个项目使用的硬件平台是STM32F103C8T6最小系统板搭配SGD GY2416C9显示屏(驱动芯片为UC1611S)。虽然示例针对特定硬件,但原理和方法适用于大多数点阵式LCD屏幕。
2. 硬件准备与环境搭建
2.1 硬件选型解析
STM32F103C8T6是一款性价比极高的Cortex-M3内核单片机,具有:
- 72MHz主频
- 64KB Flash
- 20KB RAM
- 丰富的GPIO和外设
UC1611S是一款单色LCD控制器,支持:
- 最大256x64分辨率
- 内置升压电路
- 串行/并行接口
- 低功耗模式
提示:购买LCD模块时,务必向卖家索取初始化代码。不同批次的屏幕可能需要微调初始化参数。
2.2 开发环境配置
- 安装Keil MDK-ARM开发环境(建议V5.25以上版本)
- 添加STM32F1系列设备支持包
- 配置工程时选择:
- Device: STM32F103C8
- Target: ARM Cortex-M3
- Use MicroLIB: 勾选(节省代码空间)
3. LCD基础驱动实现
3.1 屏幕初始化
UC1611S的典型初始化序列如下:
c复制void LCD_Init(void)
{
LCD_Reset(); // 硬件复位
// 发送初始化命令序列
LCD_Write_Cmd(0xE2); // 系统复位
Delay_ms(10);
LCD_Write_Cmd(0x2F); // 升压电路设置
LCD_Write_Cmd(0x81); // 设置对比度
LCD_Write_Cmd(0x1F); // 对比度值
// 更多初始化命令...
LCD_Write_Cmd(0xAF); // 开启显示
}
注意:不同厂商的屏幕可能需要调整对比度值(0x1F)和偏置电压设置。
3.2 基本绘图函数
在实现画线功能前,需要先实现两个基础函数:
- 设置光标位置:
c复制void Lcd_Address(uint8_t page, uint8_t column)
{
LCD_Write_Cmd(0xB0 | page); // 设置页地址
LCD_Write_Cmd(0x10 | (column>>4)); // 设置列地址高4位
LCD_Write_Cmd(0x00 | (column&0xF)); // 设置列地址低4位
}
- 写入显示数据:
c复制void send_dat(uint8_t dat)
{
LCD_Write_Data(dat);
}
4. 画线算法实现
4.1 画横线实现
横线是最简单的线条,原理是保持Y坐标不变,连续写入多个像素点:
c复制void Draw_HLine(uint8_t x, uint8_t y, uint8_t length)
{
Lcd_Address(y/8, x); // UC1611S的页地址对应Y坐标
uint8_t mask = 1 << (y%8);
for(uint8_t i=0; i<length; i++)
{
send_dat(mask);
}
}
关键点说明:
y/8计算像素点所在的页(每页8行)1 << (y%8)生成对应像素点的掩码- 循环写入length个像素点
实测技巧:连续写入多个像素点时,可以优化为一次发送多个字节,减少通信开销。
4.2 画竖线实现
竖线实现稍复杂,需要考虑跨页问题:
c复制void Draw_VLine(uint8_t x, uint8_t y, uint8_t length)
{
uint8_t start_page = y/8;
uint8_t end_page = (y+length-1)/8;
uint8_t start_bit = y%8;
// 处理起始不完整页
if(start_bit != 0)
{
uint8_t bits = 8 - start_bit;
if(bits > length) bits = length;
Lcd_Address(start_page, x);
send_dat(0xFF << start_bit);
length -= bits;
start_page++;
}
// 处理完整页
while(length >= 8)
{
Lcd_Address(start_page, x);
send_dat(0xFF);
length -= 8;
start_page++;
}
// 处理剩余不足一页的部分
if(length > 0)
{
Lcd_Address(start_page, x);
send_dat(0xFF >> (8-length));
}
}
算法解析:
- 计算线条跨越的页范围
- 处理起始不完整的页(部分像素)
- 处理完整的页(每页8个像素)
- 处理末尾不完整的页
4.3 画斜线实现
斜线采用经典的Bresenham算法,这是计算机图形学中最有效的线段生成算法:
c复制void Draw_Line(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1)
{
int dx = abs(x1-x0);
int dy = abs(y1-y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
while(1)
{
disp_point(x0, y0); // 绘制单个像素点
if(x0 == x1 && y0 == y1) break;
int e2 = 2*err;
if(e2 > -dy)
{
err -= dy;
x0 += sx;
}
if(e2 < dx)
{
err += dx;
y0 += sy;
}
}
}
算法核心思想:
- 根据x和y方向的变化量决定主要移动方向
- 使用误差项err决定何时需要在次要方向移动
- 每次循环绘制一个像素点,直到到达终点
性能优化:实际应用中可以将多个相邻像素点合并写入,减少通信次数。
5. 像素点操作优化
5.1 单像素点绘制函数
上述斜线算法依赖的disp_point函数实现:
c复制void disp_point(uint8_t x, uint8_t y)
{
uint8_t page = y/8;
uint8_t mask = 1 << (y%8);
// 读取原有数据
Lcd_Address(page, x);
uint8_t data = LCD_Read_Data();
// 更新对应位
data |= mask;
// 写回数据
Lcd_Address(page, x);
send_dat(data);
}
注意:UC1611S默认是竖向写入8位数据,低位在上,高位在下。如果屏幕显示方向相反,需要调整掩码生成方式。
5.2 显示缓存优化
频繁操作LCD会影响刷新速度,可以引入显示缓存:
c复制uint8_t frame_buffer[SCREEN_PAGES][SCREEN_WIDTH];
void disp_point_buffered(uint8_t x, uint8_t y)
{
uint8_t page = y/8;
uint8_t mask = 1 << (y%8);
frame_buffer[page][x] |= mask;
}
void LCD_Refresh(void)
{
for(uint8_t p=0; p<SCREEN_PAGES; p++)
{
Lcd_Address(p, 0);
for(uint8_t c=0; c<SCREEN_WIDTH; c++)
{
send_dat(frame_buffer[p][c]);
}
}
}
缓存优势:
- 减少与LCD的通信次数
- 支持局部刷新
- 便于实现更复杂的图形操作
6. 实际应用示例
6.1 绘制简单图形
结合三种画线方法,可以绘制基本图形:
c复制// 绘制矩形
void Draw_Rect(uint8_t x, uint8_t y, uint8_t width, uint8_t height)
{
Draw_HLine(x, y, width);
Draw_HLine(x, y+height-1, width);
Draw_VLine(x, y, height);
Draw_VLine(x+width-1, y, height);
}
// 绘制三角形
void Draw_Triangle(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2)
{
Draw_Line(x0, y0, x1, y1);
Draw_Line(x1, y1, x2, y2);
Draw_Line(x2, y2, x0, y0);
}
6.2 菜单框架基础
基于画线功能实现简单菜单框架:
c复制typedef struct {
char* text;
void (*action)(void);
} MenuItem;
void Draw_Menu(MenuItem* items, uint8_t count, uint8_t selected)
{
// 清屏
LCD_Clear();
// 绘制边框
Draw_Rect(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1);
// 绘制菜单项
for(uint8_t i=0; i<count; i++)
{
if(i == selected)
{
// 反白显示选中项
Draw_Rect(5, 10+i*15, LCD_WIDTH-10, 13);
LCD_Print_Inverse(10, 12+i*15, items[i].text);
}
else
{
LCD_Print(10, 12+i*15, items[i].text);
}
}
// 绘制分割线
for(uint8_t i=1; i<count; i++)
{
Draw_HLine(5, 10+i*15-2, LCD_WIDTH-10);
}
}
7. 性能优化与调试技巧
7.1 通信速度优化
- SPI接口优化:
c复制// 使用硬件SPI,配置为最大速度(PCLK/2)
void SPI_Config(void)
{
SPI_InitTypeDef SPI_InitStructure;
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_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
- 批量写入优化:
c复制void LCD_Write_Bulk(uint8_t* data, uint16_t length)
{
LCD_CS_Low();
LCD_DC_High(); // 数据模式
for(uint16_t i=0; i<length; i++)
{
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI1, data[i]);
}
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
LCD_CS_High();
}
7.2 常见问题排查
- 屏幕无显示:
- 检查电源电压(3.3V)
- 确认复位信号正常
- 验证初始化序列是否正确
- 检查背光电路
- 显示内容错乱:
- 确认通信时序符合规格书要求
- 检查SPI时钟极性设置
- 验证数据/命令选择信号
- 线条显示不连续:
- 检查坐标计算是否正确
- 确认页地址和列地址设置顺序
- 验证像素掩码生成逻辑
8. 进阶扩展思路
8.1 抗锯齿实现
基本思路:
c复制void Draw_Line_AA(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1)
{
// 计算线条角度
float angle = atan2(y1-y0, x1-x0);
// 根据角度调整像素亮度
// ...
}
8.2 曲线绘制
贝塞尔曲线实现示例:
c复制void Draw_Bezier(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1,
uint8_t x2, uint8_t y2, uint8_t steps)
{
for(uint8_t i=0; i<=steps; i++)
{
float t = (float)i/steps;
float u = 1-t;
uint8_t x = u*u*x0 + 2*u*t*x1 + t*t*x2;
uint8_t y = u*u*y0 + 2*u*t*y1 + t*t*y2;
disp_point(x, y);
}
}
8.3 多级菜单系统
扩展数据结构:
c复制typedef struct {
char* text;
MenuItem* children;
uint8_t child_count;
void (*action)(void);
} MenuItemEx;
void Navigate_Menu(MenuItemEx* menu)
{
uint8_t selected = 0;
while(1)
{
Draw_Menu_Ex(menu, selected);
// 处理按键输入
// ...
}
}
在实际项目中,我发现合理组织图形绘制代码可以显著提高界面响应速度。特别是在菜单系统中,预计算界面元素位置、使用显示缓存、优化刷新区域等方法,能够使操作体验更加流畅。对于需要频繁更新的界面,建议采用分层绘制策略,将静态内容和动态内容分开处理。