龙芯2k0300 - 走马观碑组ST7735驱动移植

在《21届智能汽车竞赛走马观碑软硬件设计》中我们介绍到我们的开发板使用了1.8TFT显示模块,显示模块使用的LCD驱动芯片为ST7735,屏幕分辨率为128*160,支持26万色、65K色(16-bit RGB565格式),尺寸为1.8寸。

屏幕显示接口采用SPI通信方式:4线SPI(包含CS/DC/RES/SCL/SDA/VCC/GND引脚),背光BL独立控制。最高支持50MHz SPI时钟。显示模块引脚与龙芯2K0300开发板的连接关系需严格对应,确保SPI通信与GPIO控制正常,连接表如下:

屏幕引脚 功能 连接的龙芯GPIO 说明
VCC 为屏幕提供工作电压 3.3V 必须接3.3V,不可接5V,避免烧毁屏幕
GND 电源共地,确保电压稳定性 GND 必须可靠接地,否则可能出现显示异常
SCL SPI 时钟引脚 SPI1_SCLK (GPIO60) 提供SPI同步通信时钟,最高50MHz
SDA SPI 主发从收引脚 SPI1_MOSI(GPIO62) 传输SPI命令与显示数据
DC 数据/命令控制引脚 GPIO48 高电平传输数据,低电平传输命令
RST 屏幕复位引脚 GPIO49 上电后需拉低复位,复位完成后拉高
CS SPI片选引脚 SPI1_CSn(GPIO63) 低电平]时选中当前SPI从设备

一、ST7735设备驱动

ST7735设备驱动涉及到两块内容:

  • 其采用了SPI通信协议,所以涉及到了SPI设备驱动;
  • 此外,我们希望应用程序来直接操作一块内存来实现实现在LCD上显示字符、图片等,因此又涉及到了framebuffer设备驱动。

其中SPI驱动源码移植可参考:

framebuffer设备驱动移植可以参考:

龙邱科技提供了ST7735设备驱动实现,位于TFT18_Driver,其仅仅是注册了SPI设备驱动,它创建的是一个自定义字符设备(通过 alloc_chrdev_region + cdev_add),提供了自己的 mmapioctlread 等接口,用户空间通过映射内核中的帧缓冲内存来操作像素,并通过自定义的 ioctl 命令(如 IOCTL_TFT_FLUSH)来刷新显示,并没有使用Linuxframebuffer框架。

1.1 内核配置

实际上我们下载的内核linux 6.12已经内置了ST7735的驱动,其使用了FBTFT框架,这里我们就直接使用内核驱动即可,无需从零开发驱动,需开启内核FBTFT相关配置,并添加ST7735驱动代码,实现屏幕初始化与显示控制。

进入内核配置界面:

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ cd ~
zhengyang@ubuntu:~$ cd /opt/2k0300/build-2k0300/workspace/linux-6.12

zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make menuconfig

依次进入以下菜单:

shell 复制代码
Device Drivers  → 
     [*]  Staging drivers  → 
        [*]   Support for small TFT LCD display modules  → 
        	<*>   FB driver for the ST7735R LCD Controller
     [*]  SPI support  →   
    	<*>   Loongson SPI Controller Platform Driver Support

默认会生成配置:

shell 复制代码
#使能FB_TFT框架
CONFIG_FB_TFT=y
#使能ST7735R驱动
CONFIG_FB_TFT_ST7735R=y
#龙芯SPI控制器驱动
CONFIG_SPI_LOONGSON_PLATFORM=y

我们直接修改arch/loongarch/configs/loongson_2k300_defconfig文件,加入这几个配置。

1.2 ST7735驱动

驱动源码位于drivers/staging/fbtft/fb_st7735r.cfb_st7735r.c驱动基于内核FBTFT框架开发,无需修改内核核心代码,只需编译进内核即可;

  • 默认支持128×160分辨率,16-bit RGB565颜色格式,最高50MHz SPI速率;
  • 包含完整的屏幕初始化命令序列,针对ST7735S进行了优化适配;
  • 支持0°/90°/180°/270°屏幕旋转,可通过设备树rotation属性配置;
  • 包含Gamma校准功能,可通过内核参数调整显示色彩效果;
  • 自动处理ST7735S的像素偏移问题,确保显示区域准确。

需要注意的是ST7735系列有多个型号(R/S/B等),核心区别在于电压容忍度、是否支持垂直滚动以及封装形式,内核中的fb_st7735r驱动主要针对RS版本。

1.2.1 fb_st7735r.c

drivers/staging/fbtft/fb_st7735r.c源码如下:
点击查看详情

c 复制代码
// SPDX-License-Identifier: GPL-2.0+
/*
 * FB driver for the ST7735R LCD Controller
 *
 * Copyright (C) 2013 Noralf Tronnes
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <video/mipi_display.h>

#include "fbtft.h"

#define DRVNAME "fb_st7735r"
#define DEFAULT_GAMMA   "0F 1A 0F 18 2F 28 20 22 1F 1B 23 37 00 07 02 10\n" \
			"0F 1B 0F 17 33 2C 29 2E 30 30 39 3F 00 07 03 10"

static const s16 default_init_sequence[] = {
	-1, MIPI_DCS_SOFT_RESET,
	-2, 150,                               /* delay */

	-1, MIPI_DCS_EXIT_SLEEP_MODE,
	-2, 500,                               /* delay */

	/* FRMCTR1 - frame rate control: normal mode
	 * frame rate = fosc / (1 x 2 + 40) * (LINE + 2C + 2D)
	 */
	-1, 0xB1, 0x01, 0x2C, 0x2D,

	/* FRMCTR2 - frame rate control: idle mode
	 * frame rate = fosc / (1 x 2 + 40) * (LINE + 2C + 2D)
	 */
	-1, 0xB2, 0x01, 0x2C, 0x2D,

	/* FRMCTR3 - frame rate control - partial mode
	 * dot inversion mode, line inversion mode
	 */
	-1, 0xB3, 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D,

	/* INVCTR - display inversion control
	 * no inversion
	 */
	-1, 0xB4, 0x07,

	/* PWCTR1 - Power Control
	 * -4.6V, AUTO mode
	 */
	-1, 0xC0, 0xA2, 0x02, 0x84,

	/* PWCTR2 - Power Control
	 * VGH25 = 2.4C VGSEL = -10 VGH = 3 * AVDD
	 */
	-1, 0xC1, 0xC5,

	/* PWCTR3 - Power Control
	 * Opamp current small, Boost frequency
	 */
	-1, 0xC2, 0x0A, 0x00,

	/* PWCTR4 - Power Control
	 * BCLK/2, Opamp current small & Medium low
	 */
	-1, 0xC3, 0x8A, 0x2A,

	/* PWCTR5 - Power Control */
	-1, 0xC4, 0x8A, 0xEE,

	/* VMCTR1 - Power Control */
	-1, 0xC5, 0x0E,

	-1, MIPI_DCS_EXIT_INVERT_MODE,

	-1, MIPI_DCS_SET_PIXEL_FORMAT, MIPI_DCS_PIXEL_FMT_16BIT,

	-1, MIPI_DCS_SET_DISPLAY_ON,
	-2, 100,                               /* delay */

	-1, MIPI_DCS_ENTER_NORMAL_MODE,
	-2, 10,                               /* delay */

	/* end marker */
	-3
};

static void set_addr_win(struct fbtft_par *par, int xs, int ys, int xe, int ye)
{
	write_reg(par, MIPI_DCS_SET_COLUMN_ADDRESS,
		  xs >> 8, xs & 0xFF, xe >> 8, xe & 0xFF);

	write_reg(par, MIPI_DCS_SET_PAGE_ADDRESS,
		  ys >> 8, ys & 0xFF, ye >> 8, ye & 0xFF);

	write_reg(par, MIPI_DCS_WRITE_MEMORY_START);
}

#define MY BIT(7)
#define MX BIT(6)
#define MV BIT(5)
static int set_var(struct fbtft_par *par)
{
	/* MADCTL - Memory data access control
	 * RGB/BGR:
	 * 1. Mode selection pin SRGB
	 *    RGB H/W pin for color filter setting: 0=RGB, 1=BGR
	 * 2. MADCTL RGB bit
	 *    RGB-BGR ORDER color filter panel: 0=RGB, 1=BGR
	 */
	switch (par->info->var.rotate) {
	case 0:
		write_reg(par, MIPI_DCS_SET_ADDRESS_MODE,
			  MX | MY | (par->bgr << 3));
		break;
	case 270:
		write_reg(par, MIPI_DCS_SET_ADDRESS_MODE,
			  MY | MV | (par->bgr << 3));
		break;
	case 180:
		write_reg(par, MIPI_DCS_SET_ADDRESS_MODE,
			  par->bgr << 3);
		break;
	case 90:
		write_reg(par, MIPI_DCS_SET_ADDRESS_MODE,
			  MX | MV | (par->bgr << 3));
		break;
	}

	return 0;
}

/*
 * Gamma string format:
 * VRF0P VOS0P PK0P PK1P PK2P PK3P PK4P PK5P PK6P PK7P PK8P PK9P SELV0P SELV1P SELV62P SELV63P
 * VRF0N VOS0N PK0N PK1N PK2N PK3N PK4N PK5N PK6N PK7N PK8N PK9N SELV0N SELV1N SELV62N SELV63N
 */
#define CURVE(num, idx)  curves[(num) * par->gamma.num_values + (idx)]
static int set_gamma(struct fbtft_par *par, u32 *curves)
{
	int i, j;

	/* apply mask */
	for (i = 0; i < par->gamma.num_curves; i++)
		for (j = 0; j < par->gamma.num_values; j++)
			CURVE(i, j) &= 0x3f;

	for (i = 0; i < par->gamma.num_curves; i++)
		write_reg(par, 0xE0 + i,
			  CURVE(i, 0),  CURVE(i, 1),
			  CURVE(i, 2),  CURVE(i, 3),
			  CURVE(i, 4),  CURVE(i, 5),
			  CURVE(i, 6),  CURVE(i, 7),
			  CURVE(i, 8),  CURVE(i, 9),
			  CURVE(i, 10), CURVE(i, 11),
			  CURVE(i, 12), CURVE(i, 13),
			  CURVE(i, 14), CURVE(i, 15));

	return 0;
}

#undef CURVE

static struct fbtft_display display = {
	.regwidth = 8,
	.width = 128,
	.height = 160,
	.init_sequence = default_init_sequence,
	.gamma_num = 2,
	.gamma_len = 16,
	.gamma = DEFAULT_GAMMA,
	.fbtftops = {
		.set_addr_win = set_addr_win,
		.set_var = set_var,
		.set_gamma = set_gamma,
	},
};

FBTFT_REGISTER_DRIVER(DRVNAME, "sitronix,st7735r", &display);

MODULE_ALIAS("spi:" DRVNAME);
MODULE_ALIAS("platform:" DRVNAME);
MODULE_ALIAS("spi:st7735r");
MODULE_ALIAS("platform:st7735r");

MODULE_DESCRIPTION("FB driver for the ST7735R LCD Controller");
MODULE_AUTHOR("Noralf Tronnes");
MODULE_LICENSE("GPL");
1.2.1.1 构造struct fbtft_display
c 复制代码
static struct fbtft_display display = {
	.regwidth = 8,
	.width = 128,
	.height = 160,
	.init_sequence = default_init_sequence,
	.gamma_num = 2,
	.gamma_len = 16,
	.gamma = DEFAULT_GAMMA,
	.fbtftops = {
        //.init_display = init_display,
		.set_addr_win = set_addr_win,
		.set_var = set_var,
		.set_gamma = set_gamma,
	},
};

struct fbtft_display用于描述一款TFT-LCD,包括硬件参数+硬件访问操作函数,需要根据LCD驱动IC的手册进行填写:

  • regwidthLCD驱动IC寄存器的位宽;
  • width/heightLCD的分辨率;
  • init_sequence: 初始化序列;
  • fbtftopsLCD操作函数集;
    • init_displayinit_sequence一般只需要设置其中一项就行了;
    • set_addr_win : 设置显示窗口,当应用程序需要向屏幕写入像素数据时,框架会先调用此函数,告诉屏幕接下来要写入的矩形区域,之后所有发送的像素数据就会按行顺序填充到这个窗口内;
    • set_var :设置显示变量,当用户通过fbset等工具修改屏幕参数(如旋转方向、颜色格式)时,框架会调用此函数,以便驱动更新硬件寄存器,使显示效果与用户空间设置同步;
    • set_gamma :设置伽马曲线,伽马校正用于调整显示器的亮度响应,使显示效果更符合人眼视觉。不同的屏幕或批次可能需要微调伽马曲线以达到最佳显示效果。
1.2.1.2 定义platform_driver
c 复制代码
FBTFT_REGISTER_DRIVER(DRVNAME, "sitronix,st7735r", &display);

这是一个宏,它的关键内容是定义了一个platform_driver

c 复制代码
#define FBTFT_REGISTER_DRIVER(_name, _compatible, _display)                \
									   \
static int fbtft_driver_probe_spi(struct spi_device *spi)                  \
{                                                                          \
	return fbtft_probe_common(_display, spi, NULL);                    \
}                                                                          \
									   \
static int fbtft_driver_remove_spi(struct spi_device *spi)                 \
{                                                                          \
	struct fb_info *info = spi_get_drvdata(spi);                       \
									   \
	return fbtft_remove_common(&spi->dev, info);                       \
}                                                                          \
									   \
static int fbtft_driver_probe_pdev(struct platform_device *pdev)           \
{                                                                          \
	return fbtft_probe_common(_display, NULL, pdev);                   \
}                                                                          \
									   \
static int fbtft_driver_remove_pdev(struct platform_device *pdev)          \
{                                                                          \
	struct fb_info *info = platform_get_drvdata(pdev);                 \
									   \
	return fbtft_remove_common(&pdev->dev, info);                      \
}                                                                          \
									   \
static const struct of_device_id dt_ids[] = {                              \
	{ .compatible = _compatible },                                     \
	{},                                                                \
};                                                                         \
									   \
MODULE_DEVICE_TABLE(of, dt_ids);                                           \

platform_driver会和设备树里的带有sitronix,st7735r属性的节点匹配上,触发fbtft-core.c / fbtft_probe_common()

1.2.2 FBTFT框架介绍

FBTFT框架是一个基于fbdev的辅助框架,专门为小尺寸SPI/并口LCD控制器(如 ST7735ILI9341SSD1306等)设计,其与Framebuffer 区别:

维度 Framebuffer (fbdev) FBTFT
层次 底层框架 基于 fbdev 的中间件
复杂度 需要实现完整的 fb_ops 只需配置结构体,框架代劳
适用范围 所有显示硬件 小尺寸 SPI/并口 LCD 控制器
输出产物 驱动直接注册 /dev/fbX 驱动通过 FBTFT 注册 /dev/fbX
代码量 较大 极少(通常几百行)
例子 drivers/video/fbdev/ 下的各种驱动 drivers/staging/fbtft/fb_st7735r.c

FBTFT框架源码位于drivers/staging/fbtft/,几个重点源码文件:

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ ll drivers/staging/fbtft/fbtft*
-rw-rw-r-- 1 zhengyang zhengyang  7436  3月 19 19:48 drivers/staging/fbtft/fbtft-bus.c
-rw-rw-r-- 1 zhengyang zhengyang 33352  3月 19 19:48 drivers/staging/fbtft/fbtft-core.c
-rw-rw-r-- 1 zhengyang zhengyang 16844  3月 19 19:48 drivers/staging/fbtft/fbtft.h
-rw-rw-r-- 1 zhengyang zhengyang  5314  3月 19 19:48 drivers/staging/fbtft/fbtft-io.c
-rw-rw-r-- 1 zhengyang zhengyang  4736  3月 19 19:48 drivers/staging/fbtft/fbtft-sysfs.c

其中:

  • fbtft-core.c:核心层,实现了一个frambuffer设备驱动;
  • fbtft-bus.c:提供读写寄存器/显存的功能;
  • fbtft-io.c:提供最底层的SPI读写功能;
  • fbtft-sysfs.c:导出一些调试接口。
1.2.2.1 fbtft_probe_common

fbtft-core.c/fbtft_probe_common函数用于分配/设置/注册framebuffer

  • fbtft_framebuffer_alloc(),定义并初始化fb_info
  • 解析设备树里的sitronix,st7735r节点:fbtft_probe_dt(dev);
  • 使用struct fbtft_display来填充struct fbtft_parfbtft_parMain FBTFT data structure,负责保存fbtft所有的软硬件信息;
  • fbtft_register_framebuffer(info),注册fb_info

初始化流程:

1.2.2.2 fbtft_register_framebuffer

fbtft-core.c/fbtft_register_framebuffer函数用于 注册TFT设备的framebuffer

  • par->fbtftops.init_display(par),执行tft-lcd的初始化操作;
  • par->fbtftops.set_var(par),设置显示变量;
  • par->fbtftops.set_gamma(par, par->gamma.curves),设置伽马曲线;
  • par->fbtftops.....,还有一些操作函数也会执行;
  • 调用 register_framebuffer(info) 向系统注册fbdev设备;
  • fbtft_sysfs_init(par),注册一些sysfs调试节点。

因此,用户空间看到的/dev/fb0仍然是一个标准的framebuffer设备,只不过背后的实现由FBTFT框架统一管理。

1.2.2.3 fbtft_update_display

fbtft-core.c/fbtft_update_display函数用于刷新屏幕;

  • par->fbtftops.set_addr_win(par, 0, start_line,par->info->var.xres - 1, end_line):设置绘制窗口的坐标;
  • par->fbtftops.write_vmem(par, offset, len):将显存中的数据刷到屏幕上。

1.3 新增设备节点

这里我们将显示模块接到spi1接口,因此需要适当调整设备树。

1.3.1 spi1

spi1节点定义在arch/loongarch/boot/dts/loongson-2k0300.dtsi

makefile 复制代码
spi1: spi@0x16018000 {
        compatible = "loongson,ls-spi";
        reg = <0 0x16018000 0 0x10>;
        #address-cells = <1>;
        #size-cells = <0>;
        interrupt-parent = <&liointc1>;
        interrupts = <13 IRQ_TYPE_LEVEL_HIGH>; // 45 - 32 = 13
        clock-frequency = <200000000>;
        pinctrl-0 = <&spi1_pins>;
        pinctrl-names = "default";
        status = "disabled";
};

其中:

  • spi1::标签,用于在设备树中其他地方通过&spi1引用这个节点,方便添加属性或修改状态;
  • spi@0x16018000:节点名,格式为 设备名@寄存器基地址。这里spi是功能名,0x16108000是该SPI控制器的物理寄存器基地址。
  • compatible:驱动匹配字符串。内核通过该属性寻找能驱动此设备的驱动程序;
  • reg:寄存器地址范围。格式为 <地址高位 地址低位 长度高位 长度低位>,由于龙芯采用64位寻址,这里用两个32位数表示64位地址;
  • #address-cells#size-cells:定义该节点下子节点(即挂载的SPI从设备)的地址和长度格式。
    • #address-cells = <1>:子节点的 reg 属性中,地址部分占用132位单元;
    • #size-cells = <0>:子节点的 reg 属性中没有长度字段;
  • interrupt-parent:指定该设备的中断路由到哪个中断控制器。&liointc0 是龙芯2K0300内部的中断控制器节点(即龙芯I/O中断控制器)的标签;
  • interrupts:描述中断线的具体信息;
    • <3>:硬件中断编号(对应SPI1控制器在中断控制器中的编号);
    • IRQ_TYPE_LEVEL_HIGH:中断触发类型,这里定义为高电平触发。该宏在 <dt-bindings/interrupt-controller/irq.h> 中定义。
  • pinctrl-0:指定设备使用的第一组引脚配置,&spi1_pins 是另一个设备树节点的标签,该节点描述了SPIMOSIMISO引脚应复用为SPI功能,并可能包含上拉等电气属性;
  • pinctrl-names:为引脚的配置状态命名,与 pinctrl-0 对应。"default" 是默认状态,驱动在probe时会自动应用 pinctrl-0 的配置,引脚配置设置为&spi1_pins
  • clock-frequencySPI控制器的输入时钟频率(单位Hz);
  • status:设备状态,此时处于禁用状态。

更多有关设备树相关的内容可以参考:《linux设备树-基础介绍》。

1.3.2 st7735r

修改arch/loongarch/boot/dts/ls2k300_99pi.dtsi

makefile 复制代码
&spi1 {
        status = "okay";

        cs-gpios = <&gpio 63 GPIO_ACTIVE_LOW>;     // CS引脚,根据实际修改

        // 屏幕驱动
        st7735r@0{
                status = "okay";
                compatible = "sitronix,st7735r";
                reg = <0>;
                spi-max-frequency = <50000000>;
                fps = <60>;
                dc-gpios = <&gpio 48 GPIO_ACTIVE_HIGH>;
                reset-gpios = <&gpio 49 GPIO_ACTIVE_LOW>;
                rotate = <90>;
                buswidth = <8>;
        };
};

通过 &spi1 引用这个节点,将 status 改为 "okay",同时添加了SPI从设备节点。其中:

  • compatible:必须设置为sitronix,st7735r,与内核驱动的兼容性字段匹配;
  • dc-gpios/reset-gpiosDCRST引脚配置,需与实际接线一致;
  • spi-max-frequency:设置为5000000050MHz),不可超过ST7735支持的最大速率;
  • rotation:可根据实际需求设置旋转角度,支持90°、180°270°四个方向;
  • bgr:若屏幕显示颜色异常(如红色变蓝色),可添加或删除该属性调整颜色格式。
1.3.3 spi1_pins

spi1_pins定义在arch/loongarch/boot/dts/loongson-2k0300.dtsi

makefile 复制代码
pinmux: pinmux@16000490 {
	......
	spi1_pins: pinmux_G60_G63_as_spi1 {
            pinctrl-single,bits = <0xc 0xff000000 0xff000000>;
    };
	......
}

这个设备树节点是使用pinctrl-single驱动来配置引脚复用功能的典型写法,这行代码会在寄存器0x1600049c的高8位写入 0xff,而低24位保持不变,用于将芯片的GPIO0~GPIO62这四个引脚设置为SPI1功能。

注:pinctrl-single 是一个通用的引脚控制驱动,适用于那些引脚复用寄存器是"单寄存器位域"的芯片。当SoC没有提供复杂的pinctrl框架时,可以直接用这种方式"裸写"寄存器。

二、应用程序

接下来我们在example目录下创建子目录st7735_app

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example$ mkdir st7735_app

目录结构如下:

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/st7735_app$ tree .
.
├── main.c
└── Makefile

2.1 main.c

ST7735驱动注册为帧缓冲设备/dev/fb0。应用程序可以通过mmap将屏幕显存映射到用户空间,然后直接写入像素数据。

下面是一个完整的C程序,实现屏幕初始化、清屏、绘制像素、显示文字等基本功能,编译后可在帧缓冲设备(/dev/fb0)上运行;
点击查看详情

c 复制代码
/*
 * 测试 ST7735 屏幕(通过 /dev/fb0)的简单图形和文字显示
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <string.h>
#include <stdint.h> 

static int fb_fd;
static struct fb_var_screeninfo vinfo;
static struct fb_fix_screeninfo finfo;
static uint16_t *fb_mem = NULL;
static size_t fb_size;
static int fb_width, fb_height;

// 颜色宏:RGB565 格式
#define RGB565(r,g,b) ((((r)>>3)<<11) | (((g)>>2)<<5) | ((b)>>3))
#define COLOR_RED     RGB565(255,0,0)
#define COLOR_GREEN   RGB565(0,255,0)
#define COLOR_BLUE    RGB565(0,0,255)
#define COLOR_WHITE   RGB565(255,255,255)
#define COLOR_BLACK   0x0000

// 初始化帧缓冲设备
int fb_init(void) 
{
    fb_fd = open("/dev/fb0", O_RDWR);
    if (fb_fd < 0) {
        perror("open /dev/fb0");
        return -1;
    }

    if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) {
        perror("ioctl FBIOGET_VSCREENINFO");
        close(fb_fd);
        return -1;
    }

    if (ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0) {
        perror("ioctl FBIOGET_FSCREENINFO");
        close(fb_fd);
        return -1;
    }

    fb_width = vinfo.xres;
    fb_height = vinfo.yres;
    fb_size = fb_width * fb_height * vinfo.bits_per_pixel / 8;
    printf("FB: %dx%d, %d bpp, line_len=%d\n", fb_width, fb_height, vinfo.bits_per_pixel, finfo.line_length);

    fb_mem = mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if (fb_mem == MAP_FAILED) {
        perror("mmap");
        close(fb_fd);
        return -1;
    }

    return 0;
}

// 关闭帧缓冲设备
void fb_deinit(void) 
{
    if (fb_mem) munmap(fb_mem, fb_size);
    if (fb_fd >= 0) close(fb_fd);
}

// 绘制像素(x, y)坐标,颜色 color(RGB565)
void fb_draw_pixel(int x, int y, uint16_t color) 
{
    if (x >= 0 && x < fb_width && y >= 0 && y < fb_height)
        fb_mem[y * fb_width + x] = color;
}

// 清屏为指定颜色
void fb_clear(uint16_t color) 
{
    for (int i = 0; i < fb_width * fb_height; i++)
        fb_mem[i] = color;
}

// 绘制矩形(填充)
void fb_fill_rect(int x, int y, int w, int h, uint16_t color) 
{
    int x1 = (x < 0) ? 0 : x;
    int y1 = (y < 0) ? 0 : y;
    int x2 = (x + w > fb_width) ? fb_width : x + w;
    int y2 = (y + h > fb_height) ? fb_height : y + h;
    for (int i = y1; i < y2; i++)
        for (int j = x1; j < x2; j++)
            fb_mem[i * fb_width + j] = color;
}

// 绘制空心矩形框
void fb_draw_rect(int x, int y, int w, int h, uint16_t color) 
{
    fb_fill_rect(x, y, w, 1, color);      // 上边
    fb_fill_rect(x, y + h - 1, w, 1, color); // 下边
    fb_fill_rect(x, y, 1, h, color);      // 左边
    fb_fill_rect(x + w - 1, y, 1, h, color); // 右边
}

// 简易 8x8 字符点阵(仅 ASCII 可打印字符部分示例,这里只提供数字和部分字母)
static uint8_t font_8x8[95][8] = {0}; // 实际上需要完整字库,这里简化,只实现常用字母数字

// 预置一些常用字符(仅示例,实际需完整)
static uint8_t char_A[8] = {0x00, 0x18, 0x24, 0x42, 0x7E, 0x42, 0x42, 0x00};
static uint8_t char_B[8] = {0x7C, 0x42, 0x7C, 0x42, 0x42, 0x7C, 0x00, 0x00};
static uint8_t char_C[8] = {0x3C, 0x42, 0x40, 0x40, 0x40, 0x3C, 0x00, 0x00};
static uint8_t char_D[8] = {0x78, 0x44, 0x42, 0x42, 0x44, 0x78, 0x00, 0x00};
static uint8_t char_E[8] = {0x7E, 0x40, 0x7C, 0x40, 0x40, 0x7E, 0x00, 0x00};
static uint8_t char_F[8] = {0x7E, 0x40, 0x7C, 0x40, 0x40, 0x40, 0x00, 0x00};
static uint8_t char_G[8] = {0x3C, 0x42, 0x40, 0x4E, 0x42, 0x3C, 0x00, 0x00};
static uint8_t char_H[8] = {0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x00, 0x00};
static uint8_t char_I[8] = {0x3E, 0x08, 0x08, 0x08, 0x08, 0x3E, 0x00, 0x00};
static uint8_t char_J[8] = {0x1F, 0x04, 0x04, 0x04, 0x44, 0x38, 0x00, 0x00};
static uint8_t char_K[8] = {0x42, 0x44, 0x48, 0x70, 0x48, 0x44, 0x42, 0x00};
static uint8_t char_L[8] = {0x40, 0x40, 0x40, 0x40, 0x40, 0x7E, 0x00, 0x00};
static uint8_t char_M[8] = {0x42, 0x66, 0x5A, 0x42, 0x42, 0x42, 0x00, 0x00};
static uint8_t char_N[8] = {0x42, 0x62, 0x52, 0x4A, 0x46, 0x42, 0x00, 0x00};
static uint8_t char_O[8] = {0x3C, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00};
static uint8_t char_P[8] = {0x7C, 0x42, 0x42, 0x7C, 0x40, 0x40, 0x00, 0x00};
static uint8_t char_Q[8] = {0x3C, 0x42, 0x42, 0x52, 0x4C, 0x32, 0x00, 0x00};
static uint8_t char_R[8] = {0x7C, 0x42, 0x42, 0x7C, 0x44, 0x42, 0x00, 0x00};
static uint8_t char_S[8] = {0x3C, 0x42, 0x20, 0x1C, 0x02, 0x7C, 0x00, 0x00};
static uint8_t char_T[8] = {0x7E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00};
static uint8_t char_U[8] = {0x42, 0x42, 0x42, 0x42, 0x42, 0x3C, 0x00, 0x00};
static uint8_t char_V[8] = {0x42, 0x42, 0x42, 0x24, 0x24, 0x18, 0x00, 0x00};
static uint8_t char_W[8] = {0x42, 0x42, 0x42, 0x5A, 0x66, 0x42, 0x00, 0x00};
static uint8_t char_X[8] = {0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, 0x00};
static uint8_t char_Y[8] = {0x42, 0x24, 0x18, 0x08, 0x08, 0x08, 0x00, 0x00};
static uint8_t char_Z[8] = {0x7E, 0x02, 0x04, 0x08, 0x10, 0x7E, 0x00, 0x00};
static uint8_t char_0[8] = {0x3C, 0x42, 0x46, 0x4A, 0x52, 0x62, 0x3C, 0x00};
static uint8_t char_1[8] = {0x08, 0x18, 0x08, 0x08, 0x08, 0x08, 0x1C, 0x00};
static uint8_t char_2[8] = {0x3C, 0x42, 0x02, 0x0C, 0x30, 0x40, 0x7E, 0x00};
static uint8_t char_3[8] = {0x3C, 0x42, 0x02, 0x1C, 0x02, 0x42, 0x3C, 0x00};
static uint8_t char_4[8] = {0x08, 0x18, 0x28, 0x48, 0x7E, 0x08, 0x08, 0x00};
static uint8_t char_5[8] = {0x7E, 0x40, 0x7C, 0x02, 0x02, 0x42, 0x3C, 0x00};
static uint8_t char_6[8] = {0x3C, 0x40, 0x7C, 0x42, 0x42, 0x42, 0x3C, 0x00};
static uint8_t char_7[8] = {0x7E, 0x02, 0x04, 0x08, 0x10, 0x10, 0x10, 0x00};
static uint8_t char_8[8] = {0x3C, 0x42, 0x42, 0x3C, 0x42, 0x42, 0x3C, 0x00};
static uint8_t char_9[8] = {0x3C, 0x42, 0x42, 0x3E, 0x02, 0x42, 0x3C, 0x00};

void fb_draw_char(int x, int y, char ch, uint16_t fg, uint16_t bg) 
{
    uint8_t *glyph = NULL;
    if (ch >= 'A' && ch <= 'Z') {
        switch(ch) {
            case 'A': glyph = char_A; break;
            case 'B': glyph = char_B; break;
            case 'C': glyph = char_C; break;
            case 'D': glyph = char_D; break;
            case 'E': glyph = char_E; break;
            case 'F': glyph = char_F; break;
            case 'G': glyph = char_G; break;
            case 'H': glyph = char_H; break;
            case 'I': glyph = char_I; break;
            case 'J': glyph = char_J; break;
            case 'K': glyph = char_K; break;
            case 'L': glyph = char_L; break;
            case 'M': glyph = char_M; break;
            case 'N': glyph = char_N; break;
            case 'O': glyph = char_O; break;
            case 'P': glyph = char_P; break;
            case 'Q': glyph = char_Q; break;
            case 'R': glyph = char_R; break;
            case 'S': glyph = char_S; break;
            case 'T': glyph = char_T; break;
            case 'U': glyph = char_U; break;
            case 'V': glyph = char_V; break;
            case 'W': glyph = char_W; break;
            case 'X': glyph = char_X; break;
            case 'Y': glyph = char_Y; break;
            case 'Z': glyph = char_Z; break;
        }
    } else if (ch >= '0' && ch <= '9') {
        switch(ch) {
            case '0': glyph = char_0; break;
            case '1': glyph = char_1; break;
            case '2': glyph = char_2; break;
            case '3': glyph = char_3; break;
            case '4': glyph = char_4; break;
            case '5': glyph = char_5; break;
            case '6': glyph = char_6; break;
            case '7': glyph = char_7; break;
            case '8': glyph = char_8; break;
            case '9': glyph = char_9; break;
        }
    } else {
        // 其他字符(空格等)不绘制
        return;
    }

    if (!glyph) return;

    for (int row = 0; row < 8; row++) {
        uint8_t line = glyph[row];
        for (int col = 0; col < 8; col++) {
            if (line & (0x80 >> col)) {
                fb_draw_pixel(x + col, y + row, fg);
            } else {
                fb_draw_pixel(x + col, y + row, bg);
            }
        }
    }
}

void fb_draw_string(int x, int y, const char *str, uint16_t fg, uint16_t bg) 
{
    int cx = x;
    int cy = y;
    for (int i = 0; str[i]; i++) {
        if (str[i] == '\n') {
            cx = x;
            cy += 9; // 8像素高度+1行间距
            continue;
        }
        fb_draw_char(cx, cy, str[i], fg, bg);
        cx += 9; // 8像素宽度+1间距
        if (cx + 8 > fb_width) {
            cx = x;
            cy += 9;
        }
    }
}

// 简单测试:显示一些图形和文字
void test_display(void) 
{
    fb_clear(COLOR_BLACK);
    // 画红色填充矩形
    fb_fill_rect(10, 10, 100, 50, COLOR_RED);
    // 画蓝色边框
    fb_draw_rect(120, 10, 100, 50, COLOR_BLUE);
    // 显示文字
    fb_draw_string(10, 80, "Hello", COLOR_WHITE, COLOR_BLACK);
    fb_draw_string(10, 95, "ST7735", COLOR_GREEN, COLOR_BLACK);
    fb_draw_string(10, 110, "LCD", COLOR_BLUE, COLOR_BLACK);
    // 显示数字和字母混合
    fb_draw_string(10, 130, "A1B2C3", COLOR_RED, COLOR_BLACK);
}

int main(void) 
{
    if (fb_init() != 0) {
        fprintf(stderr, "Failed to init framebuffer\n");
        return 1;
    }

    test_display();

    // 保持显示10秒后退出(或按任意键)
    printf("Display test. Press Enter to exit...\n");
    getchar();

    fb_deinit();
    return 0;
}
2.1.1 获取参数

要对framebuffer进行操作,首先要知道所操作设备的相关参数,因为要驱动一个屏,首先需要知道该屏尺寸、单色还是彩色等,Linux在用户空间提供了两个跟framebuffer参数相关的结构体(在文件fb.h中):

c 复制代码
struct fb_fix_screeninfo {
	char id[16];			/* identification string eg "TT Builtin" */
	unsigned long smem_start;	/* Start of frame buffer mem */
					/* (physical address) */
	__u32 smem_len;			/* Length of frame buffer mem */
	__u32 type;			/* see FB_TYPE_*		*/
	__u32 type_aux;			/* Interleave for interleaved Planes */
	__u32 visual;			/* see FB_VISUAL_*		*/ 
	__u16 xpanstep;			/* zero if no hardware panning  */
	__u16 ypanstep;			/* zero if no hardware panning  */
	__u16 ywrapstep;		/* zero if no hardware ywrap    */
	__u32 line_length;		/* length of a line in bytes    */
	unsigned long mmio_start;	/* Start of Memory Mapped I/O   */
					/* (physical address) */
	__u32 mmio_len;			/* Length of Memory Mapped I/O  */
	__u32 accel;			/* Indicate to driver which	*/
					/*  specific chip/card we have	*/
	__u16 capabilities;		/* see FB_CAP_*			*/
	__u16 reserved[2];		/* Reserved for future compatibility */
};

struct fb_var_screeninfo {
	__u32 xres;			/* visible resolution		*/
	__u32 yres;
	__u32 xres_virtual;		/* virtual resolution		*/
	__u32 yres_virtual;
	__u32 xoffset;			/* offset from virtual to visible */
	__u32 yoffset;			/* resolution			*/

	__u32 bits_per_pixel;		/* guess what			*/
	__u32 grayscale;		/* 0 = color, 1 = grayscale,	*/
					/* >1 = FOURCC			*/
	struct fb_bitfield red;		/* bitfield in fb mem if true color, */
	struct fb_bitfield green;	/* else only length is significant */
	struct fb_bitfield blue;
	struct fb_bitfield transp;	/* transparency			*/	

	__u32 nonstd;			/* != 0 Non standard pixel format */

	__u32 activate;			/* see FB_ACTIVATE_*		*/

	__u32 height;			/* height of picture in mm    */
	__u32 width;			/* width of picture in mm     */

	__u32 accel_flags;		/* (OBSOLETE) see fb_info.flags */

	/* Timing: All values in pixclocks, except pixclock (of course) */
	__u32 pixclock;			/* pixel clock in ps (pico seconds) */
	__u32 left_margin;		/* time from sync to picture	*/
	__u32 right_margin;		/* time from picture to sync	*/
	__u32 upper_margin;		/* time from sync to picture	*/
	__u32 lower_margin;
	__u32 hsync_len;		/* length of horizontal sync	*/
	__u32 vsync_len;		/* length of vertical sync	*/
	__u32 sync;			/* see FB_SYNC_*		*/
	__u32 vmode;			/* see FB_VMODE_*		*/
	__u32 rotate;			/* angle we rotate counter clockwise */
	__u32 colorspace;		/* colorspace for FOURCC-based modes */
	__u32 reserved[4];		/* Reserved for future compatibility */
};

这两个参数都可以通过ioctl获得,首先定义3个变量,一个是打开的fb设备的句柄,剩下2个分别是fb_var_screeninfofb_fix_screeninfo

c 复制代码
static int fb_fd;
static struct fb_var_screeninfo vinfo;
static struct fb_fix_screeninfo finfo;

打开fb设备:

c 复制代码
fb_fd = open("/dev/fb0", O_RDWR);

然后分别获取两个结构体:

c 复制代码
ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo)
ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo)

我们可以创建两个函数分别输出这两个结构体元素:

c 复制代码
void show_fb_fix_info(struct fb_fix_screeninfo info)
{
    printf("fb's fix msg:\n");
    printf("\tid is:%s\n",info.id);
    printf("\tsmem_start is:%d\n",info.smem_start);
    printf("\tsmem_len is:%d\n",info.smem_len);
    printf("\ttype_aux is:%d\n",info.type_aux);
    printf("\tvisual is:%d\n",info.visual);
    printf("\txpanstep is:%d\n",info.xpanstep);
    printf("\typanstep is:%d\n",info.ypanstep);
    printf("\tywrapstep is:%d\n",info.ywrapstep);
    printf("\tline_length is:%d\n",info.line_length);
    printf("\tmmio_start is:%d\n",info.mmio_start);
}

void show_fb_var_info(struct fb_var_screeninfo info)
{
    printf("fb's var msg:\n");
    printf("\txres is:%d\n",info.xres);
    printf("\tyres is:%d\n",info.yres);
    printf("\txres_virtual is:%d\n",info.xres_virtual);
    printf("\tyres_virtual is:%d\n",info.yres_virtual);
    printf("\txoffset is:%d\n",info.xoffset);
    printf("\tyoffset is:%d\n",info.yoffset);
    printf("\tbits_per_pixel is:%d\n",info.bits_per_pixel);
    printf("\tgrayscale is:%d\n",info.grayscale);
    printf("\tnonstd is:%d\n",info.nonstd);
    printf("\tactivate is:%d\n",info.activate);
    printf("\theight is:%d\n",info.height);
    printf("\twidth is:%d\n",info.width);
    printf("\taccel_flags is:%d\n",info.accel_flags);
    printf("\tpixclock is:%d\n",info.pixclock);
    printf("\tleft_margin is:%d\n",info.left_margin);
    printf("\tright_margin is:%d\n",info.right_margin);
    printf("\tupper_margin is:%d\n",info.upper_margin);
    printf("\tlower_margin is:%d\n",info.lower_margin);
    printf("\thsync_len is:%d\n",info.hsync_len);
    printf("\tvsync_len is:%d\n",info.vsync_len);
    printf("\tsync is:%d\n",info.sync);
    printf("\tvmode is:%d\n",info.vmode);
    printf("\trotate is:%d\n",info.rotate);
    printf("\tcolorspace is:%d\n",info.colorspace);
}
2.1.2 填充颜色

给整个屏填充颜色,要填充屏的话,需要几个参数,一个是framebuffer所需内存的大小,一个是屏的像素的个数,还有就是颜色深度。

要对framebuffer进行操作首先需要做的是通过mmap进行地址映射,这里就要用到framebuffer所需内存的大小,framebuffer所需内存的大小从fb_fix_screeninfo获得,如下:

c 复制代码
static uint16_t *fb_mem = NULL;
static size_t fb_size;

fb_width = vinfo.xres;
fb_height = vinfo.yres;
fb_size = fb_width * fb_height * vinfo.bits_per_pixel / 8;
fb_mem = mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);

这里申请一段finfo.smem_len大小的连续内存,对这段内存进行操作就会反应到屏上了。

对整个屏进行填充就是操作屏上的所有像素(也可以说遍历所有像素),这里就需要知道屏幕的像素的个数,这个参数可以从之前获取到的fb_var_screeninfo中的参数中的xresyresxresyres是可见分辨率,可以理解为可见区域,应该就是对应屏幕显示区域的大小了。

还有一个很关键的参数颜色深度(bppbits per pixel),也就是表示一个像素颜色所需的位数(bit),一般来说有这么几种:1位,8位,16位,24位,32位。比如

  • 1位的屏,也就是单色屏,用1 bit来表示颜色,如果是白色单色屏的话,0表示黑色,1表示白色;
  • 24 bit屏,就是用24 bit3字节)表示颜色,也就是RGB888RGB分别占8 bit

比如我们使用的屏是就是16 bit的,采用RGB565格式(即红5位、绿6位、蓝5位)。

定义5个宏,分别表示红色、绿色、蓝色、白色、黑色;

c 复制代码
// 颜色宏:RGB565 格式
#define RGB565(r,g,b) ((((r)>>3)<<11) | (((g)>>2)<<5) | ((b)>>3))
#define COLOR_RED     RGB565(255,0,0)
#define COLOR_GREEN   RGB565(0,255,0)
#define COLOR_BLUE    RGB565(0,0,255)
#define COLOR_WHITE   RGB565(255,255,255)
#define COLOR_BLACK   0x0000

创建一个写framebuffer内存的函数,如下:

c 复制代码
// 绘制矩形(填充)
void fb_fill_rect(int x, int y, int w, int h, uint16_t color) 
{
    int x1 = (x < 0) ? 0 : x;
    int y1 = (y < 0) ? 0 : y;
    int x2 = (x + w > fb_width) ? fb_width : x + w;
    int y2 = (y + h > fb_height) ? fb_height : y + h;
    for (int i = y1; i < y2; i++)
        for (int j = x1; j < x2; j++)
            fb_mem[i * fb_width + j] = color;
}

// 画红色填充矩形
fb_fill_rect(10, 10, 100, 50, COLOR_RED);

2.2 Makefile

makefile 复制代码
all:
	loongarch64-linux-gnu-gcc -o main main.c
clean:
	rm -rf *.o main

2.3 编译应用程序

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/st7735_app$ make
loongarch64-linux-gnu-gcc -o main main.c
zhengyang@ubuntu:/opt/2k0300/loongson_2k300_lib/example/st7735_app$ ll
-rwxrwxr-x 1 zhengyang zhengyang 20680  3月 24 14:26 main*
-rw-rw-r-- 1 zhengyang zhengyang  1070  3月 24 14:23 main.c
-rw-rw-r-- 1 zhengyang zhengyang    71  3月 24 14:24 Makefile

三、测试

3.1 烧录设备树

3.1.1 编译设备树

如果需要单独编译设备树,在linux内核根目录执行如下命令:

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make  arch/loongarch/boot/dts/ls2k300_99pi_wifi.dts  dtbs V=1
3.1.2 更新设备树

ubuntu虚拟机,我们需要将设备树放在这个tftp根目录下;

复制代码
zhengyang@ubuntu:/opt$ cp /opt/2k0300/build-2k0300/workspace/linux-6.12/arch/loongarch/boot/dts/ls2k300_99pi_wifi.dtb /opt/tftpboot/

久久派进入uboot,使用tftp命令将设备树下载到内存地址${fdt_addr},fdt_addr的值来自配置参数FDT_ADDR,值为0x900000000a000000

shell 复制代码
=> tftp ${fdt_addr} ls2k300_99pi_wifi.dtb 

擦除板载SPI Nor Flashdtb分区,并将内存中的设备树数据写入到板载SPI Nor Flashdtb分区;

shell 复制代码
=> sf probe
=> sf erase dtb
=> sf write ${loadaddr} dtb 0xd000

3.2 安装驱动

由于我们在内核配置环节将驱动配置到内核中了,因此需要重新编译内核并烧录内核。

3.2.1 编译内核

ubuntu宿主接重新编译内核:

shell 复制代码
zhengyang@ubuntu:/opt/2k0300/build-2k0300/workspace/linux-6.12$ source ../set_env.sh && make uImage -j$(nproc)
3.2.2 烧录内核

久久派烧录内核,即将编译生成的uImage(内核镜像)拷贝到/boot目录;

shell 复制代码
root@buildroot:~$ scp zhengyang@172.23.34.186:/opt/2k0300/build-2k0300/workspace/linux-6.12/arch/loongarch/boot/uImage /boot/

root@buildroot:~$ reboot
3.2.3 查看信息

查看内核启动spi相关信息:

shell 复制代码
root@buildroot:~$ dmesg | grep "spi"

查看设备节点文件:

shell 复制代码
root@buildroot:~$ ls /dev/fb0

3.3 应用程序测试

久久派开发板执行如下命令:

shell 复制代码
root@buildroot:~$ scp zhengyang@172.23.34.186:/opt/2k0300/loongson_2k300_lib/example/st7735_app/main ./
root@buildroot:~$ ./main

参考文章

[1] 龙芯2K300_301软件开源库

[2] WwuSama 21届智能车走马观碑开源仓库

[3] 玩转STM32MP157- 使用fbtft驱动 lcd st7735r

[4] 玩转STM32MP157- 在应用层中使用 fbtft

[5] 瑞芯微RK3568适配1.8英寸ST7735S SPI屏幕教程

[6] Linux驱动开发 / fbtft源码速读