嵌入式Linux C应用编程——LCD横竖屏切换
LCD 横屏切换为竖屏
横屏/竖屏的切换与驱动程序无关,是应用层需要解决的一个问题。
背景知识:横屏下的显存寻址
LCD 硬件上像素点的排列顺序是 从左到右、从上到下。以 800×480 分辨率为例,物理坐标分布如下:
(0, 0) (799, 0)
┌──────────────────┐
│ │
│ │
│ │
└──────────────────┘
(0, 479) (799, 479)
对于任意物理坐标 (x, y),其对应的显存地址为:
地址 = base + (y × width + x) × pix_bytes
其中:
base— 显存基地址(mmap返回值)width— LCD 水平分辨率(如 800)pix_bytes— 每个像素占用的字节数(RGB565 为 2 字节)
竖屏显示的坐标变换
竖屏坐标定义
将横屏屏幕 顺时针旋转 90°,得到竖屏坐标系:
- 竖屏起点
(0, 0)对应物理坐标(0, height-1)(左下角) - 竖屏 X 轴向上增长,对应物理 Y 轴的反向
- 竖屏 Y 轴向右增长,对应物理 X 轴的正向
两个坐标系的对应关系如下:
横屏物理坐标(800×480) 竖屏应用坐标(480×800)
(0, 0) ┌──────────┐ (799, 0) (479, 0) ┌──────────┐ (479, 799)
│ ──→ Y │ │ ←── Y │
│ ↓ X │ │ ↑ X │
│ │ │ │
(0, 479)└──────────┘ (799, 479) (0, 0) └──────────┘ (0, 799)
对应关系对照表:
| 竖屏坐标 | 物理坐标 | 说明 |
|---|---|---|
| (0, 0) | (0, height-1) = (0, 479) | 左下角 |
| (0, width-1) = (0, 799) | (799, 479) | 右下角 |
| (height-1, 0) = (479, 0) | (0, 0) | 左上角 |
| (height-1, width-1) = (479, 799) | (799, 0) | 右上角 |
核心公式推导
目标:给定竖屏坐标 (x, y),找到它对应的显存地址。
步骤 1:竖屏 X → 物理 Y
竖屏 X 坐标从底部向上增长,而物理 Y 坐标从顶部向下增长。两者是反向线性关系:
物理 Y = (height - 1) - 竖屏_X
注意这里的 竖屏_X 是一个变量(取值 0 ~ height-1),不是固定值:
- 竖屏
X = 0(最底部)→ 物理y = (height-1) - 0 = height - 1(最底行) - 竖屏
X = 1(向上移一行)→ 物理y = (height-1) - 1(向上一行) - 竖屏
X = height - 2→ 物理y = 1 - 竖屏
X = height - 1(最顶部)→ 物理y = (height-1) - (height-1) = 0(最顶行)
不难看出,竖屏 X 每增加 1,物理 Y 就减少 1(向上移动一行),这就是
(height - 1) - x中减去x的原因。
步骤 2:竖屏 Y → 物理 X
竖屏 Y 坐标从左向右增长,物理 X 坐标也是从左向右增长。两者是正向线性关系:
物理 X = 竖屏 Y
- 竖屏
y = 0→ 物理x = 0(最左列) - 竖屏
y = width - 1→ 物理x = width - 1(最右列)
步骤 3:代入显存寻址公式
将上述映射关系代入物理显存地址公式:
地址 = base + (物理_y × width + 物理_x) × pix_bytes
= base + (((height - 1) - x) × width + y) × pix_bytes
公式的直观理解
| 竖屏坐标 | 物理坐标 | 显存地址计算 |
|---|---|---|
| (0, 0) 左下角 | (0, height-1) | base + ((height-1) × width + 0) × pix_bytes |
| (0, width-1) 右下角 | (width-1, height-1) | base + ((height-1) × width + (width-1)) × pix_bytes |
| (height-1, 0) 左上角 | (0, 0) | base + 0 |
| (height-1, width-1) 右上角 | (width-1, 0) | base + (width-1) × pix_bytes |
注意:这个公式仅适用于"顺时针旋转 90°,起点在左下角"这一种竖屏方式。如果旋转 180° 或其他角度,公式需要相应调整。
应用编程实战
完整源码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})
static int lcd_width; // LCD X 分辨率
static int lcd_height; // LCD Y 分辨率
static int lcd_max_y; // LCD Y 坐标最大值
static int user_width; // 竖屏模式下 X 分辨率
static int user_height; // 竖屏模式下 Y 分辨率
static unsigned short *screen_base = NULL; // 映射后的显存基地址
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);
if (x >= user_width)
x = user_width - 1;
if (y >= user_height)
y = user_height - 1;
/* 核心:竖屏坐标转物理显存地址 */
screen_base[(lcd_max_y - x) * lcd_width + y] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);
unsigned int end;
unsigned long temp;
if (x >= user_width)
x = user_width - 1;
if (y >= user_height)
y = user_height - 1;
temp = (lcd_max_y - x) * lcd_width + y;
if (dir) { // 水平线(竖屏X方向)
end = x + length - 1;
if (end >= user_width)
end = user_width - 1;
for ( ; x <= end; x++, temp -= lcd_width)
screen_base[temp] = rgb565_color;
} else { // 垂直线(竖屏Y方向)
end = y + length - 1;
if (end >= user_height)
end = user_height - 1;
for ( ; y <= end; y++, temp++)
screen_base[temp] = rgb565_color;
}
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
int x_len = end_x - start_x + 1;
int y_len = end_y - start_y - 1;
lcd_draw_line(start_x, start_y, 1, x_len, color); // 上边
lcd_draw_line(start_x, end_y, 1, x_len, color); // 下边
lcd_draw_line(start_x, start_y + 1, 0, y_len, color); // 左边
lcd_draw_line(end_x, start_y + 1, 0, y_len, color); // 右边
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充指定颜色
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y,
unsigned int color)
{
unsigned short rgb565_color = argb8888_to_rgb565(color);
unsigned long temp;
unsigned long step_size_count;
int x;
if (end_x >= user_width)
end_x = user_width - 1;
if (end_y >= user_height)
end_y = user_height - 1;
temp = (lcd_max_y - start_x) * lcd_width + start_y;
for ( ; start_y <= end_y; start_y++, temp++) {
step_size_count = 0;
for (x = start_x; x <= end_x; x++, step_size_count += lcd_width)
screen_base[temp - step_size_count] = rgb565_color;
}
}
int main(int argc, char *argv[])
{
struct fb_fix_screeninfo fb_fix;
struct fb_var_screeninfo fb_var;
unsigned int screen_size;
int fd;
/* 打开 framebuffer 设备 */
if (0 > (fd = open("/dev/fb0", O_RDWR))) {
perror("open error");
exit(EXIT_FAILURE);
}
/* 获取参数信息 */
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
screen_size = fb_fix.line_length * fb_var.yres;
lcd_width = fb_var.xres;
lcd_height = fb_var.yres;
lcd_max_y = lcd_height - 1;
user_width = fb_var.yres; // 竖屏宽 = 物理高
user_height = fb_var.xres; // 竖屏高 = 物理宽
/* 将显示缓冲区映射到进程地址空间 */
screen_base = mmap(NULL, screen_size, PROT_WRITE,
MAP_SHARED, fd, 0);
if (MAP_FAILED == (void *)screen_base) {
perror("mmap error");
close(fd);
exit(EXIT_FAILURE);
}
/* 画正方形方块 */
int w = user_height * 0.25;
lcd_fill(0, user_width-1, 0, user_height-1, 0x0); // 清屏黑色
lcd_fill(0, w, 0, w, 0xFF0000); // 红色方块
lcd_fill(user_width-w, user_width-1, 0, w, 0xFF00); // 绿色方块
lcd_fill(0, w, user_height-w, user_height-1, 0xFF); // 蓝色方块
lcd_fill(user_width-w, user_width-1,
user_height-w, user_height-1, 0xFFFF00); // 黄色方块
/* 画十字交叉线 */
lcd_draw_line(0, user_height * 0.5, 1,
user_width, 0xFFFFFF); // 白色水平线
lcd_draw_line(user_width * 0.5, 0, 0,
user_height, 0xFFFFFF); // 白色垂直线
/* 画嵌套矩形 */
unsigned int s_x, s_y, e_x, e_y;
s_x = 0.25 * user_width;
s_y = w;
e_x = user_width - s_x;
e_y = user_height - s_y;
for ( ; (s_x <= e_x) && (s_y <= e_y);
s_x+=5, s_y+=5, e_x-=5, e_y-=5)
lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
/* 退出 */
munmap(screen_base, screen_size);
close(fd);
exit(EXIT_SUCCESS);
}
与横屏代码的主要区别
以下是竖屏代码相比之前横屏代码的关键改动点:
| 对比项 | 横屏代码 | 竖屏代码(本程序) |
|---|---|---|
| 用户坐标范围 | user_width = lcd_width(800)user_height = lcd_height(480) |
user_width = lcd_height(480)user_height = lcd_width(800)—— 横竖互换 |
| 打点公式 | screen_base[y * lcd_width + x]用户坐标 = 物理坐标,直接映射 |
screen_base[(lcd_max_y - x) * lcd_width + y]竖屏坐标 (x, y) 经过变换后写入物理显存 |
| 画水平线 (dir=1) |
沿物理 X 方向步进:temp++(向右) |
沿竖屏 X 方向步进(视觉上为竖屏的水平方向):temp -= lcd_width(向上,因为竖屏X映射到物理Y的反向) |
| 画垂直线 (dir=0) |
沿物理 Y 方向步进:temp += lcd_width(向下) |
沿竖屏 Y 方向步进(视觉上为竖屏的垂直方向):temp++(向右,因为竖屏Y映射到物理X的正向) |
| 参数校验 | 校验 x 是否超出 lcd_width |
校验 x 是否超出 user_width(即物理 lcd_height) |
竖屏代码没有改变 LCD 硬件,只是改变了应用层坐标到显存地址的映射方式,让应用程序可以用竖屏坐标系来操作横屏的物理屏幕。
编译 & 测试
编译:
${CC} -o lcd_vertical_display lcd_vertical_display.c
测试:
- 将可执行文件拷贝到开发板。
- 关闭 Qt GUI 应用程序。
- 运行
./lcd_vertical_display,LCD 上将显示竖屏效果:四个角有红/绿/蓝/黄方块,中间有十字交叉线和嵌套矩形。

浙公网安备 33010602011771号