408

计算机网络

TCP/IP模型是基于广泛应用的互联网协议族(TCP/IP协议栈)而制定的,是现代互联网的事实标准。它更加精简,通常被划分为5层(为便于教学,将链路层和物理层分开)或4层。

5层模型(最常用于教学和描述)

  1. 物理层:与OSI物理层完全相同。
  2. 数据链路层:与OSI数据链路层基本相同。
  3. 网络层:对应OSI的网络层。核心协议是IP
  4. 传输层:对应OSI的传输层。核心协议是TCPUDP
  5. 应用层融合了OSI的应用层、表示层和会话层的功能。所有面向用户的应用程序和协议都在这一层。

4层模型(TCP/IP原模型)

  1. 网络接口层: 对应OSI的物理层+数据链路层。
  2. 网络层: 对应OSI的网络层。
  3. 传输层: 对应OSI的传输层。
  4. 应用层: 对应OSI的应用层+表示层+会话层。

第1层:物理层

  • 功能:负责在物理媒介(如光纤、双绞线、无线电波)上透明地传输原始比特流(0和1)。定义电气、机械、时序和接口标准。
  • 关键设备/协议:中继器、集线器;物理接口(如RJ-45)。

第2层:数据链路层

  • 功能:在相邻节点(直接相连的设备)之间进行可靠的数据帧传输。负责物理寻址(MAC地址)、差错控制、流量控制和介质访问控制。
  • 关键协议/设备:以太网(Ethernet)、Wi-Fi(802.11)、PPP;交换机、网桥。

第3层:网络层

  • 功能:负责数据包从源到目的地的逻辑寻址和路径选择(路由)。实现不同网络之间的通信。
  • 关键协议/设备IP协议(IPv4/IPv6)、ICMP、OSPF、BGP;路由器

第4层:传输层

  • 功能:提供端到端的可靠或不可靠的数据传输服务。负责分段、重组、流量控制、差错控制和端口寻址。
  • 关键协议TCP(可靠的、面向连接的)、UDP(不可靠的、无连接的)。

第5层:会话层

  • 功能:负责建立、管理和终止应用进程之间的会话(Session)。在通信实体之间进行对话控制(全双工、半双工)和同步。
  • 例子:RPC(远程过程调用)、SSH会话的建立/断开。

第6层:表示层

  • 功能:处理两个系统间交换信息的语法和语义问题。负责数据格式转换、加密解密、压缩解压缩。
  • 例子:将数据从应用层格式转换为网络标准格式(如ASCII与EBCDIC码转换)、SSL/TLS加密、JPEG图像编码。

第7层:应用层

  • 功能:最靠近用户的一层,为应用程序提供网络服务接口。提供用户可直接使用的网络服务。
  • 关键协议HTTP、HTTPS、FTP、SMTP/POP3/IMAP、DNS、DHCP。

C makefile cmake

C++

C base

对象创建

C 没有 new,对象创建 = 分配内存 + 初始化,分两种情况:

分配位置写法对应 Java
Point p; Point_init(&p, ...);
Point* p = Point_new(...);new Point(...)

结构体定义

typedef struct {
    char name[32];
    int x;
    int y;
} Point;

初始化函数(不分配内存,只写数据)

void Point_init(Point* p, const char* name, int x, int y) {
    strncpy(p->name, name, sizeof(p->name) - 1);
    p->name[sizeof(p->name) - 1] = '\0';
    p->x = x;
    p->y = y;
}

构造器(分配 + 初始化)

Point* Point_new(const char* name, int x, int y) {
    Point* p = malloc(sizeof(Point));     // 堆上分配
    if (p) {
        Point_init(p, name, x, y);        // 写入数据
    }
    return p;
}

void Point_del(Point* p) {
    free(p);
}

使用示例

/* 栈 —— 已划好空间,但内容是垃圾数据,必须 init 后才能用 */
Point p1;
Point_init(&p1, "stack", 10, 20);

/* 堆 —— Point_new 内部 = malloc + Point_init */
Point* p2 = Point_new("heap", 30, 40);
Point_del(p2);

关键理解

  • Point p1; 只是在栈上划了 sizeof(Point) 字节的空间,里面的值是未定义的(垃圾数据),必须初始化后才能用。
  • Point_init 不分配内存,它只负责往你传入的地址写入数据。内存来自栈还是堆,它不关心。
  • Point_new 内部 = malloc + Point_init,这就是 C 的"构造器"。
  • Java 的 new Point(...) 把"分配"和"初始化"合为一步;C 拆成两步,好处是可以在栈上分配、可以复用已有内存。

封装

隐藏内部数据,只暴露接口函数。

"头文件"暴露的内容

typedef struct BankAccount BankAccount;  // 前向声明,不暴露成员

// 只暴露函数接口
BankAccount* BankAccount_open(const char* owner, double balance);
void         BankAccount_deposit(BankAccount* a, double amount);
int          BankAccount_withdraw(BankAccount* a, double amount);
double       BankAccount_get_balance(const BankAccount* a);
const char*  BankAccount_get_owner(const BankAccount* a);
void         BankAccount_close(BankAccount* a);

"实现" —— 结构体完整定义对外不可见

struct BankAccount {
    char   owner[32];
    double balance;       /* 外部无法直接访问 */
    int    active;        /* 完全隐藏 */
    int    failed_count;  /* 对外不可见 */
};

函数实现

BankAccount* BankAccount_open(const char* owner, double balance) {
    BankAccount* a = malloc(sizeof(BankAccount));
    if (a) {
        strncpy(a->owner, owner, sizeof(a->owner) - 1);
        a->owner[sizeof(a->owner) - 1] = '\0';
        a->balance      = balance;
        a->active       = 1;
        a->failed_count = 0;
    }
    return a;
}

void BankAccount_deposit(BankAccount* a, double amount) {
    if (a->active && amount > 0) {
        a->balance += amount;
    }
}

int BankAccount_withdraw(BankAccount* a, double amount) {
    if (!a->active) return -1;
    if (amount > a->balance) {
        a->failed_count++;       // 记录失败次数,外部无感知
        return -1;
    }
    a->balance -= amount;
    return 0;
}

double BankAccount_get_balance(const BankAccount* a) {
    return a->balance;
}

const char* BankAccount_get_owner(const BankAccount* a) {
    return a->owner;
}

void BankAccount_close(BankAccount* a) {
    if (a) {
        a->active = 0;
        printf("[internal] failed_count=%d\n", a->failed_count);
        free(a);
    }
}

使用者视角

int main(void) {
    BankAccount* acc = BankAccount_open("Alice", 1000.0);
    BankAccount_deposit(acc, 500.0);
    BankAccount_withdraw(acc, 200.0);

    // 只能通过函数访问,无法 acc->balance 或 acc->failed_count
    printf("owner: %s\n",   BankAccount_get_owner(acc));
    printf("balance: %.2f\n", BankAccount_get_balance(acc));

    BankAccount_close(acc);
    return 0;
}

C 的封装靠"文件作用域 + 不透明指针"实现,而非 private 关键字。

继承

核心技巧:基类作为子类第一个字段

C 标准保证:结构体首字段的地址 == 结构体本身的地址,所以 (Vehicle*)&car 转换是安全的。

基类

typedef struct {
    char brand[32];
    int  year;
} Vehicle;

void Vehicle_init(Vehicle* v, const char* brand, int year) {
    strncpy(v->brand, brand, sizeof(v->brand) - 1);
    v->brand[sizeof(v->brand) - 1] = '\0';
    v->year = year;
}

void Vehicle_print(const Vehicle* v) {
    printf("Vehicle{brand=%s, year=%d}", v->brand, v->year);
}

子类 Car(继承 Vehicle)

typedef struct {
    Vehicle base;       /* 继承 —— 首字段就是基类 */
    int     doors;      /* 扩展字段 */
} Car;

void Car_init(Car* c, const char* brand, int year, int doors) {
    Vehicle_init(&c->base, brand, year);  // 先初始化"父类部分"
    c->doors = doors;                     // 再初始化自己
}

void Car_print(const Car* c) {
    printf("Car{brand=%s, year=%d, doors=%d}",
           c->base.brand, c->base.year, c->doors);
}

子类 Bike(继承 Vehicle)

typedef struct {
    Vehicle base;
    int     has_basket;
} Bike;

void Bike_init(Bike* b, const char* brand, int year, int has_basket) {
    Vehicle_init(&b->base, brand, year);
    b->has_basket = has_basket;
}

向上转型演示

int main(void) {
    Car  car;
    Bike bike;
    Car_init(&car,  "Toyota", 2020, 4);
    Bike_init(&bike, "Giant",  2023, 1);

    /* 向上转型:子类指针 → 基类指针(安全) */
    Vehicle* v1 = (Vehicle*)&car;
    Vehicle* v2 = (Vehicle*)&bike;

    Vehicle_print(v1);   // 输出基类共有的信息
    Vehicle_print(v2);
    return 0;
}

多态

核心技巧:虚函数表(vtable)—— 基类里放一个指向函数指针表的指针,不同子类填充不同的实现,调用时通过 vtable 间接跳转。

虚函数表 + 基类定义

// 虚函数表类型
typedef struct VTable VTable;
struct VTable {
    double (*area)(void* self);
    void   (*print)(void* self);
};

// 基类
typedef struct {
    const char* name;
    const VTable* vtable;      // 指向子类各自的虚函数表
} Shape;

多态调用入口(统一接口)

double Shape_area(void* self) {
    Shape* s = (Shape*)self;
    return s->vtable->area(self);    // 通过 vtable 跳转到子类实现
}

void Shape_print(void* self) {
    Shape* s = (Shape*)self;
    s->vtable->print(self);          // 通过 vtable 跳转到子类实现
}

子类 Circle

typedef struct {
    Shape  base;
    double radius;
} Circle;

static double Circle_area(void* self) {
    Circle* c = (Circle*)self;
    return M_PI * c->radius * c->radius;
}

static void Circle_print(void* self) {
    Circle* c = (Circle*)self;
    printf("Circle{radius=%.1f, area=%.1f}", c->radius, Circle_area(self));
}

// Circle 类的虚函数表(静态全局,所有实例共享)
static const VTable CIRCLE_VTABLE = {
    .area  = Circle_area,
    .print = Circle_print,
};

void Circle_init(Circle* c, const char* name, double radius) {
    c->base.name   = name;
    c->base.vtable = &CIRCLE_VTABLE;   // 绑定自己的 vtable
    c->radius      = radius;
}

子类 Rectangle

typedef struct {
    Shape base;
    double w, h;
} Rectangle;

static double Rect_area(void* self) {
    Rectangle* r = (Rectangle*)self;
    return r->w * r->h;
}

static void Rect_print(void* self) {
    Rectangle* r = (Rectangle*)self;
    printf("Rectangle{%.1f x %.1f, area=%.1f}", r->w, r->h, Rect_area(self));
}

static const VTable RECT_VTABLE = {
    .area  = Rect_area,
    .print = Rect_print,
};

void Rectangle_init(Rectangle* r, const char* name, double w, double h) {
    r->base.name   = name;
    r->base.vtable = &RECT_VTABLE;
    r->w = w;
    r->h = h;
}

子类 Triangle

typedef struct {
    Shape base;
    double a, b, c;
} Triangle;

static double Tri_area(void* self) {
    Triangle* t = (Triangle*)self;
    double s = (t->a + t->b + t->c) / 2;
    return sqrt(s * (s - t->a) * (s - t->b) * (s - t->c));
}

static void Tri_print(void* self) {
    Triangle* t = (Triangle*)self;
    printf("Triangle{sides=(%.1f,%.1f,%.1f), area=%.1f}",
           t->a, t->b, t->c, Tri_area(self));
}

static const VTable TRI_VTABLE = {
    .area  = Tri_area,
    .print = Tri_print,
};

void Triangle_init(Triangle* t, const char* name, double a, double b, double c) {
    t->base.name   = name;
    t->base.vtable = &TRI_VTABLE;
    t->a = a; t->b = b; t->c = c;
}

多态使用

void print_shape_info(void* self) {
    Shape_print(self);
    printf(" | area = %.1f\n", Shape_area(self));
}

int main(void) {
    Circle    c;
    Rectangle r;
    Triangle  t;

    Circle_init(&c, "c1", 5.0);
    Rectangle_init(&r, "r1", 4.0, 3.0);
    Triangle_init(&t, "t1", 3.0, 4.0, 5.0);

    // 通过 void* 数组统一管理,循环调用
    void* shapes[] = { &c, &r, &t };
    for (int i = 0; i < 3; i++) {
        print_shape_info(shapes[i]);   // 各自输出不同的 area
    }
    return 0;
}

运行结果

=== 多态:同一个函数,不同行为 ===
Circle{radius=5.0, area=78.5} | area = 78.5
Rectangle{4.0 x 3.0, area=12.0} | area = 12.0
Triangle{sides=(3.0,4.0,5.0), area=6.0} | area = 6.0

Linux c

CMake 生成 Makefile → Make 调用 GCC/G++ 编译代码。

在linux系统下通过apt install make ,可以直接在命令行输入make就能编译makefile项目。

但是在windows下,用的就不是make而是mingw32-make,跟make是一样的。

所有的c语言,编写完成后,生成可执行文件都有以下几个步骤。

1.预处理,将所有的#define删除,并且展开所有的宏定义。处理#if,#ifdef,#else等。以及#include,以及删除所有注释。添加行号和文件标识方便下一步编译。将引入的头文件进行处理。

2.编译:这一步主要是将预处理生成的文件(.i)生成汇编语言,这一步很重要。不同版本的gcc,比如gcc-x86,arm-linux-gcc(交叉编译器)。会将预处理生成的文件编译成不同的汇编指令集。

3.汇编:这个步骤将上面编译出来的汇编指令变成二进制机器码。生成目标文件(.o)

4.链接:这一步最重要,编写的c语言文件又很多个,以及要引用到libc库或者其他库中的文件比如(stdio.h)文件。这一步要将他们链接到一起。最后生成可执行文件。

在linux中,安装gcc工具链。

#include <stdio.h>
int main()
{
    printf("hello word!\n");
    return 0;
}

预处理过程,gcc命令

gcc -E test.c -o test.i

    //文件内容多,不展示

编译过程,gcc命令

gcc -S test.i -o test.s

	.file	"demo01-gcc-test.c"
	.text
	.section	.rodata
.LC0:
	.string	"hello world!"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	leaq	.LC0(%rip), %rax
	movq	%rax, %rdi
	call	puts@PLT
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	1f - 0f
	.long	4f - 1f
	.long	5
0:
	.string	"GNU"
1:
	.align 8
	.long	0xc0000002
	.long	3f - 2f
2:
	.long	0x3
3:
	.align 8
4:

汇编过程指令

gcc -c test.s -0 test.o

链接过程命令

//这一步因为涉及很多个文件

一步到位生成可执行文件

gcc test.c -o test //直接生成可执行文件test 为文件test分配执行权限后,执行./test 打印hello word!

GCC 基本用法

GCC 的基本命令格式如下:

gcc [选项] [文件名]
其中,[选项] 用于指定编译行为,[文件名] 是需要编译的源文件。

常用选项

-o [文件名]:指定输出文件名,默认生成 a.out。

-c:只编译生成目标文件(.o 文件),不进行链接。

-S:生成汇编代码文件(.s 文件)。

-E:只进行预处理,输出预处理结果。

-g:生成调试信息,便于使用调试工具(如 GDB)。

-O0/-O1/-O2/-O3:优化级别,-O0 表示无优化,-O3 优化级别最高。

-Wall:启用所有警告信息。

-static:使用静态链接库,生成独立的可执行文件。

-shared:生成共享库文件。

GCC 编译过程

GCC 的编译过程分为以下四个步骤:

预处理:展开宏定义、头文件等,生成 .i 文件。

gcc -E hello.c -o hello.i
编译:将预处理后的代码转为汇编代码,生成 .s 文件。

gcc -S hello.i -o hello.s
汇编:将汇编代码转为目标文件(机器码),生成 .o 文件。

gcc -c hello.s -o hello.o
链接:将目标文件与库文件链接,生成可执行文件。

gcc hello.o -o hello

Makefile

windows安装


​ 什么是makefile,makefile是一个用于自动化构建程序的配置文件。它指示 make 工具如何编译和链接程序。简单来说, Makefile 就像一个构建脚本,告诉编译器如何从源文件生成可执行文件或其他目标文件。

targets ...: prerequisites...
	command
	....
targets 要生成的目标文件
prerequisites 所需要的依赖文件或者targets
command 命令

编写makefile文件

demo01 : demo01.o //将目标文件链接生成可执行文件
    gcc -o demo01 demo01.o
demo01.o: demo01.c
    gcc -c demo01.c //生成目标文件demo01.o

例子(复习)

cc = gcc #选择编译器
objects = add.o d1.o #所需要链接的目标文件
target_ = d1 #链接成的可执行文件

$(target_) : $(objects)
	$(cc) -o $(target_) $(objects)

add.o :add.o add.h
	$(cc) -c add.c
d1.o : d1.c
	$(cc) -c d1.c

链接库

gcc test.c -o test -L ./xx-so -lxxx -lpthread
  • -lxxx -lpthread : 需要链接的库(.so)
  • -L ./xx-so : 链接库所在地址,如果是系统的库pthread,会自己去系统默认路径找
  • 注意:库的名字必须是libxxx.so,也能是带版本的libxxx.so.0,libxxx.so.0.2.0

Docker

Example

Jar包生成dockerfile并创建镜像

springboot打包的jar包生成dockerfile并创建镜像

编写Dockerfile
# Docker image for springboot file run
# 基础镜像使用java
FROM java:openjdk-8

# VOLUME 指定了临时文件目录为/tmp。
# 其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp
# VOLUME /tmp

# 将jar包添加到容器中并更名为app.jar
ADD test.jar app.jar
# 运行jar包
RUN bash -c 'touch /app.jar'
ENTRYPOINT ["java","-jar","/app.jar"]
###声明启动端口号
EXPOSE 8801
构建docker镜像
docker build -t my-spring-app:1.0 .  
//确保Dockerfile文件在 . 当前目录 my-spring-app是docker image的名字 1.0是版本

运行镜像

# 以后台模式运行 并映射端口
docker run -d -p 8801:8801 --name my-spring-app my-spring-app:1.0
# 镜像id的方式
docker run -d -p 8801:8801 --name my-spring-app a1b2c3d4e5f6

emqx

nginx

redis

mysql

nacos

作用:

容器编排。将docker命令将容器组织起来。

安装:

apt install docker.io
apt install docker-compose

介绍:

默认情况下,如果没有配置网络,在同一个docker-compose.yaml中的所有service(服务)会使用同一个网络,这个网络名是docker-compose 命令时所在目录的名称+_default。

  • 例如,如果项目文件夹叫 my_app,那么默认网络名就是 my_app_default

实战:

  • dockercompose结合Dockerfile一键启动后端java应用

    docker-compose.yml

    services:
      java-jar-app:
        build: ./
        container_name: java-jar-app
        ports:
          - 8088:8088
        depends_on:
          - mysql-jar-app
      mysql-jar-app:
        image: mysql:latest
        container_name: mysql-jar-app
        ports:
          - 3307:3306
    
    

    Dockerfile

    FROM openjdk:17
    COPY docker-jar-test-1.0.0.jar /usr/src/app.jar
    WORKDIR /usr/src/
    EXPOSE 8088
    CMD ["java","-jar","app.jar"]
    
    

构建最简单命令

  1. 编译hello.c文件,生成hello执行文件
  2. 新建Dockerfile文件。将以下命令写入Dockerfile
FROM scratch    #基础镜像。
ADD hello /     #添加当前目录下的hello文件到镜像的/ 目录
CMD ["/hello"]  #执行命令行命令(在镜像内部)
3. 执行构建命令
-t 指名镜像名称和tag
. 指名时当前目录(找当前目录的Dockerfile文件并执行)
docker build -t hello-my-world:1.0.0 .

docker tag

这个命令可以修改镜像的名称和tag

将hello-my-world:1.0.0修改为hmw:1.0.1
docker tag hello-my-world:1.0.0 hmw:1.0.1

RUN

构建镜像时,执行Shell命令。构建过程中安装软件包、配置环境、生成文件等。

RUN apt-get update && apt-get install -y python3
RUN ["apt-get", "update"]
RUN ["apt-get", "install", "-y", "python3"]

CMD和ENTRYPOINT

设置创建时的命令

ADD和COPY

ENV

设置环境变量

ENV <key>  <value>

ENV <key> =<value> <key> =<value> <key> =<value>


ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc"

AGE

定义变量

AGE name=TOM

docker build -t mytom:2.0 --build-arg name=Jerry //重新定义

WORKDIR

指定工作目录。

EXPOSE

暴露端口

VOLUME

在容器中创建挂载点。

docker volume create 

保存镜像

主要是将某个镜像保存为.tar文件

  • docker save:把一个或多个镜像保存为 tar 文件(保留层、历史、元数据)。
  • docker load:把 save 生成的 tar 文件加载为镜像

主要用途:镜像的备份、迁移

将tomcat:8.5.49镜像打包成tar包
docker save -o mytom.tar tomcat:8.5.49 或者docker save -o mytom.tar [imageId]

load加载mytom.tar包成镜像
docker load -i mytom.tar

**注意 加载 的镜像与原镜打包前的镜像完全一样。包括了镜像ID。所以必须 删除 之前的镜像才能 加载 

保存容器

  • docker commit:将容器的当前状态(文件系统+配置)保存为一个新镜像

  • docker export:将容器的文件系统导出为一个 tar 包(不含元数据、层历史等)。

  • docker import:将 export 生成的 tar 包(或其它文件系统快照)导入为一个新镜像(可指定启动命令等)。

基本命令

安装

apt install docker.io
apt install docker-compose

docker pull

docker pull ubuntu:20.04
拉取ubuntu版本为20.04。如果没有写版本会默认下载最新版本ubuntu:latest
docker pull -q ubuntu
-q:静默,不输出下载的镜像信息(不打印日志)
docker pull --platform=arm64 ubuntu:20.04
下载arm平台的ubuntu:20.04,用于将这个镜像放到arm开发板下。

docker images

docker images 用来查看当前已有的镜像
-a, --all: 显示所有镜像(包括中间层镜像)。
--digests: 显示镜像的摘要信息。显示sha256信息
-f, --filter: 过滤输出,基于提供的条件。
--format: 使用 Go 模板格式化输出。
--no-trunc: 显示完整的镜像 ID。
-q, --quiet: 只显示镜像 ID。
docker images unbuntu 查看名称是ubuntu的镜像
docker search ubuntu 查找名为ubuntu的镜像

docker rmi

docker rmi ubuntu 移除ubuntu镜像
docker rmi adb238 移除adb238开头的镜像ID对应的镜像。

docker run

docker run hello-world 启动hello-world容器,并且随机给一个名字。会默认使用latest版本。如果没有会去拉取
docker run hello-world:1.0.0 启动hello-world:1.0.0容器
docker run --name hello-wd hello-world:1.0.0 启动一个名为hello-wd容器

*注意,这个命令会启动一个新的容器,如果--name hello-wd 同一个名称会启动不了

//启动一个名为hello-wd 的容器,并且进入这个容器中。
docker run --name hello-wd -it hello-world /bin/bash

在这个容器中命令行exit 会退出这个容器。

**启动tomcat 配置暴露端口号**

**docker run --name mytomcat -it -p 8081:8080 tomcat:8.5.49**

-P 大P的话会随机分配端口号

退出容器并且保持容器状态:**ctrl+p+q**


后台方式启动

docker run --name mytomcat2 -p 8082:8080 -d tomcat:8.5.49

删除某种镜像的全部容器

docker rm -f ${docker ps ubuntu}

docker exec

  • 进入容器

docker exec -it mytomcat /bin/bash

docker exec -w /root -it mytomcat /bin/bash

  • 进入容器

docker attach 跟exec用法一致。但是这个使用exit会把容器也停掉。如果使用exec 不会停容器

docker ps

docker logs

docker logs -f -n 1000 tomcat 查看tomcat中1000行日志。并且跟踪。

docker rm ; docker rmi

docker删除容器,删除镜像

# 删除所有由 nginx:latest 创建的容器
docker rm $(docker ps -a -q -f "ancestor=nginx:latest")
  • docker ps -a -q-q 参数只输出容器ID,非常简洁。
  • -f "ancestor=nginx:latest"--filter 过滤器,ancestor= 表示筛选出所有由指定镜像创建的容器。

docker start 、stop 、kill 、restart

docker的启停命令。

docker cp

用于将宿主机的文件复制到容器内部、或将容器内部文件复制到宿主机。

docker cp ./test_dir mytomcat:/root 将宿主机当前目录下的文件夹复制到容器内部
docker cp ./test.txt mytomcat:/root 将宿主机当前目录下的文件复制到容器内部
docker cp ./test.txt mytomcat:/root/t.txt 将宿主机当前目录下的文件复制到容器内部并命名为t.txt

docker cp mytomcat:/root/test_dir ./ 将容器内的文件夹复制到宿主机当前目录
docker cp mytomcat:/root/test.txt ./ 将容器内的文件复制到宿主机当前目录
docker cp mytomcat:/root/test.txt ./t.txt 将容器内的文件复制到宿主机当前目录并命名为t.txt

容器之间文件交互(不支持)
docker cp mytomcat1:/root/test.txt mytomcat2:/root

docker commit

注意:容器挂载目录中的文件不会被打包

主要作用是将配置好的一些容器生成新的镜像,可以得到复用(再次使用不需要再配置)。

docker commit -a "wyl" -m "注释信息" mytomcat tomcat8:1.0

-a 指定作者

-m 注释这个镜像信息 可以用docker inspect contain_name 查看信息

mytomcat 将哪个容器打包

tomcat8:1.0 打包出来的镜像名称和tag

docker export、import 不使用

主要用来制作基础镜像

会丢弃历史记录和元数据。启动export与import命令导出导入的镜像必须加/bin/bash或者其他/bin/sh,否则会报错。

将mytomcat容器打包成tomcat-export:v1镜像
docker export -o tomcat-export.tar mytomcat:latest 
docker export mytomcat:latest > tomcat-export.tar

导入镜像并指定镜像的名称和tag (必须指定)
docker import mytom.tar tom8:export
这里会生成一个新的镜像,可以执行多次指定镜像的名称不同即可

docker save、load 只用这个

将指定 镜像 保存成tar文件,不会丢弃历史记录和元数据,并可以回滚版本

结合commit命令。将容器打包成镜像。再将这个镜像打包成tar文件。进行数据迁移。

将tomcat:8.5.49镜像打包成tar包
docker save -o mytom.tar tomcat:8.5.49 或者docker save -o mytom.tar [imageId]

load加载mytom.tar包成镜像
docker load -i mytom.tar

**注意 加载 的镜像与原镜打包前的镜像完全一样。包括了镜像ID。所以必须 删除 之前的镜像才能 加载 

docker tag

重命名,注意不会修改原来的镜像,只会新增一个镜像

docker tag nginx:1.27.0 nginx:latest
docker tag [imageId] nginx:latest

docker system

docker system df 查看镜像、容器、挂载卷的数量、硬盘占用、活动状态等
docker system events 跟踪容器的状态。启停等,会打印到终端
docker system info 显示当前一些系统信息
docker system prune 移除没有在使用的镜像

docker create

创建不启动

除了-d命令不能用,其他命令都与docker run 一致

使用docker start 启动命令

emqx

nginx

redis

mysql

nacos

方式1:

直接将容器打包,使用docker commit + docker save。将整个容器所有东西都进行保存。

方式2:

使用数据卷。

数据卷介绍:

​ 宿主机中的某个文件/目录与容器内的某个文件/目录进行关联,宿主机/容器内对这个文件/目录进行修改都会实时同步。

*只支持容器启动时挂载,后期想挂载不允许。只能另辟蹊径。

挂载方式

docker run --name test-volume -d -p 8080:8080 -v ./data:/usr/src/data tomcat:8.5.49

启动容器,设置容器名称,后台运行,端口映射,数据卷挂载,所用容器。

注意:挂载路径要么绝对路径,比如:/home/wyl/docker_data:/usr/src/data;要么相对路径./data:/usr/src/data。不能出现这种写法:data:/usr/src/data。这种写法会出现挂载问题。

只读方式

docker run -dp 8080:8080 -v ./data:/user/src/data:ro tomcat:8.5.49

在路径后面加ro(read only)

注意:这个操作是针对容器内部的。./data:ro:/user/src/data这种方式错的,不能这么操作。加上这个操作之后,容器内部就不能进行文件写/创建操作。防止容器往文件乱写数据

数据卷共享

**方式1:**两个容器的数据都挂载到同一个宿主机目录。

docker run -dp 8080:8080 -v ./data:/user/src/data:ro tomcat:8.5.49
docker run -dp 8080:8080 -v ./data:/user/src/data:ro tomcat:8.5.49

**方式2:**使用数据卷volume方式。

这里创建了两个nginx应用。同时挂载到同一个数据卷html和nginx_conf。他们的数据是共享的。

docker run --name nginx1 -dp 82:80 -v html:/usr/share/nginx/html -v nginx_conf:/etc/nginx nginx:latest
docker run --name nginx2 -dp 82:80 -v html:/usr/share/nginx/html -v nginx_conf:/etc/nginx nginx:latest

实践

1.docker run方式设置网络模式

# 桥接模式 -p 才有用,会把内部的断开和宿主机的端口进行映射。这种时候用来做集群好用,一台机器部署多个同类型服务

#默认就是桥接bridge 会创建一个网卡
docker run -d -p 6380:6379 --name redis redis:6.0
#设置模式bridge 
docker run -d -p 6380:6379 --name redis --network bridge redis:6.0

# 创建自定义 bridge 网络 这种方式创建的桥接名称可以 自定义

docker network create my-network
docker run -d --name redis -p 6380:6379 --network my-network bridge redis:6.0


# host模式 可以提高性能。相当于直接把docker当成一个服务启动。跟直接部署没什么区别

docker run -d --name redis --network host redis:6.0
#查看端口占用
losf -i:6379


#none模式 这个应用没有网络功能,如果是redis这种应用部署就没有意义了
docker run -d --name redis --network none redis:6.0


#容器模式container  -p指令同样没有意义 这种一般用于其他容器走nginx,只开放nginx80端口
#假设当前已经有一个nginx容器在运行
docker run -d --name redis --network container:nginx redis:6.0


#覆盖网络overlay
#略过

2.docker-compose设置网络模式

  • bridge模式

    version: '3.8'
    services:
    	web:
    		image: nginx:1.27.0
    		ports:
    		  - "8080:80" #设置多个端口映射
    		  - "443:443"
    		networks:
    		 - backnet #后端网络
    		 - beforenet # 前端网络
    	api:
    		image: java-app:1.0.0
    		ports:
    		  - "48080:48080"
    		networks:
    		  - backnet 
    	resource:
    		image: resource:1.0.0
    		ports:
    		  - "88:88"
    		network:
    		  - beforenet 
    networks:
    	backnet:
    		driver: bridge
    	beforenet:
    		driver: bridge
    		ipam:
          		config:
            	  - subnet: 172.21.0.0/16
    
    
    
  • host模式

    version: '3.8'
    services:
    	web:
    		image: nginx:1.27.0
    		network_mode: "host" #直接配置成host
        api:
        	image: java-app:1.0.0
        	network_mode: "host" #直接配置成host
    
  • none

    
    
  • container

    
    

四种模式解析

  • bridge(默认)容器创建网卡

    • docker安装完成后,docker建立一个虚拟网卡docker0,bridge模式下的容器会把这个网卡当作网关。即容器nat方式->docker0->宿主机

    • 容器创建后会在宿主机建立一个网卡,这个网卡172.17.18.1(一般来说18代表建立了多少个容器,1代表网卡地址,容器里面的服务会以.1->.2开始分配)

  • *host *容器不创建网卡,性能更好延迟更低

    直接把容器当作一个进程了。会占用端口

    网络2
  • none

    没有网络,也就是说,这个容器没法与外界进行交流,而且也不会有网卡。

  • container

    和已经存在的容器共享一个网卡。不会创建网卡和ip。如果已经存在的容器可以和宿主机进行网络数据交换,那么新建的容器也能和宿主机进行网络数据交换。

网络3
  • 自定义桥接

​ 这种可以指定桥接方式

Embedded

FreeRTOS

/*
 * FreeRTOS Kernel V11.1.0 — Windows 仿真配置文件
 *
 * 本文件为 FreeRTOS 提供了在 Windows (MinGW) 上模拟运行时的全部配置选项。
 * 每个宏都附有详细的中文说明,方便学习和调试。
 *
 * SPDX-License-Identifier: MIT
 */

#ifndef FREERTOS_CONFIG_H
#define FREERTOS_CONFIG_H

/*===========================================================================
 *  编译器兼容性
 *===========================================================================*/
#if defined(_MSC_VER)
    /* 消除 MSVC 关于不安全 CRT 函数的编译警告 */
    #define _CRT_SECURE_NO_WARNINGS
#endif

/*===========================================================================
 *  内核调度策略
 *===========================================================================*/

/*
 * configUSE_PREEMPTION —— 是否启用抢占式调度
 *   1 = 抢占式调度(高优先级任务就绪时立即抢占低优先级任务)
 *   0 = 协作式调度(任务必须主动让出 CPU,如调用 vTaskDelay() 或 taskYIELD())
 *   仿真环境通常用 1,更贴近真实嵌入式场景。
 */
#define configUSE_PREEMPTION                    1

/*
 * configUSE_PORT_OPTIMISED_TASK_SELECTION —— 是否使用硬件优化的任务选择算法
 *   1 = 使用位运算(如 BSR 指令)在 O(1) 时间内找到最高优先级就绪任务
 *   0 = 使用通用的链表遍历查找
 *   Windows 仿真下也支持,开启可提高效率。要求 configMAX_PRIORITIES ≤ 32。
 */
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1

/*
 * configUSE_TICKLESS_IDLE —— 低功耗无节拍模式
 *   0 = 关闭。空闲期间定时器中断仍然周期性产生
 *   1 = 开启。空闲期间停止周期性 Tick 中断以省电(用于电池供电设备)
 *   仿真环境不需要省电,始终设为 0。
 */
#define configUSE_TICKLESS_IDLE                 0

/*===========================================================================
 *  钩子函数 (Hook / Callback)
 *===========================================================================*/

/*
 * configUSE_IDLE_HOOK —— 空闲任务钩子
 *   1 = 使能,需要在代码中实现 vApplicationIdleHook()
 *   0 = 关闭
 *   空闲钩子在空闲任务每轮循环中被调用,可用于休眠或背景处理。
 */
#define configUSE_IDLE_HOOK                     0

/*
 * configUSE_TICK_HOOK —— Tick 中断钩子
 *   1 = 使能,需要在代码中实现 vApplicationTickHook()
 *   0 = 关闭
 *   Tick 钩子在每次 SysTick 中断中执行,要求非常短小精悍。
 */
#define configUSE_TICK_HOOK                     0

/*
 * configUSE_MALLOC_FAILED_HOOK —— 内存分配失败钩子
 *   1 = 使能,需要在代码中实现 vApplicationMallocFailedHook()
 *   0 = 关闭
 *   当 pvPortMalloc() 返回 NULL 时触发。
 */
#define configUSE_MALLOC_FAILED_HOOK            0

/*
 * configUSE_DAEMON_TASK_STARTUP_HOOK —— 守护任务启动钩子
 *   1 = 使能,定时器服务任务启动时会调用 vApplicationDaemonTaskStartupHook()
 *   0 = 关闭
 */
#define configUSE_DAEMON_TASK_STARTUP_HOOK      0

/*===========================================================================
 *  协程 (Co-routines,已过时)
 *===========================================================================*/

/*
 * configUSE_CO_ROUTINES —— 是否启用协程(旧版轻量级任务替代方案)
 *   1 = 启用,需要同时定义 configMAX_CO_ROUTINE_PRIORITIES
 *   0 = 关闭,协程是 v8.0 之前的特性,新项目建议直接用任务
 */
#define configUSE_CO_ROUTINES                   0

/*===========================================================================
 *  时钟与 Tick 配置
 *===========================================================================*/

/*
 * configCPU_CLOCK_HZ —— CPU 时钟频率 (Hz)
 *   在真实 MCU 上写入实际的主频,用于计算 tick 周期。
 *   在 Windows 仿真中只是一个参考值,实际定时精度取决于 Sleep()。
 */
#define configCPU_CLOCK_HZ                      ( ( unsigned long ) 12000000 )

/*
 * configTICK_RATE_HZ —— 系统 Tick 频率 (Hz)
 *   即每秒产生多少次 Tick 中断。1000 表示每 1ms 一次 Tick。
 *   vTaskDelay(1000) 会阻塞 1000 个 Tick = 1 秒。
 *   注意:Windows 仿真受 Sleep() 精度限制,实际节拍不会精确到毫秒级。
 */
#define configTICK_RATE_HZ                      ( ( TickType_t ) 1000 )

/*
 * configTICK_TYPE_WIDTH_IN_BITS —— Tick 计数的位宽
 *   可选值:
 *     TICK_TYPE_WIDTH_16_BITS — 16 位,最大计数值 65535
 *     TICK_TYPE_WIDTH_32_BITS — 32 位,最大计数值 ~49 天(@1000Hz)
 *     TICK_TYPE_WIDTH_64_BITS — 64 位(极少使用)
 *   Windows 64 位宿主下建议用 32 位,与 portmacro.h 配合最佳。
 */
#define configTICK_TYPE_WIDTH_IN_BITS           TICK_TYPE_WIDTH_32_BITS

/*===========================================================================
 *  任务参数
 *===========================================================================*/

/*
 * configMAX_PRIORITIES —— 系统支持的最大优先级数
 *   范围为 1~32(使用位运算优化时限制为 32)。
 *   优先级 0 为最低(空闲任务),数值越大优先级越高。
 *   这里设 5 级足够 HelloWorld 测试使用。
 */
#define configMAX_PRIORITIES                    ( 5 )

/*
 * configMINIMAL_STACK_SIZE —— 最小任务堆栈大小(单位:StackType_t 个数)
 *   空闲任务使用此大小。用户创建任务时建议不小于此值。
 *   在 Windows 仿真中,每个任务实际上是一个独立线程,
 *   堆栈只是映射到线程的预留空间,所以设小一点没关系。
 */
#define configMINIMAL_STACK_SIZE                ( ( unsigned short ) 70 )

/*
 * configTOTAL_HEAP_SIZE —— 动态分配的总堆大小(字节)
 *   仅 heap_1/2/4 使用(heap_3 使用 malloc 无此限制)。
 *   此处设置为 65KB,仿真时够用。
 */
#define configTOTAL_HEAP_SIZE                   ( ( size_t ) ( 65 * 1024 ) )

/*
 * configMAX_TASK_NAME_LEN —— 任务名称最大长度(含 '\0')
 *   设置太大会浪费内存,设 12 字节足够描述任务功能。
 */
#define configMAX_TASK_NAME_LEN                 ( 12 )

/*
 * configIDLE_SHOULD_YIELD —— 空闲任务是否主动让出 CPU
 *   1 = 空闲任务在执行一次回调后就让出 CPU,避免低优先级任务饥饿
 *   0 = 空闲任务与同优先级的其他任务共享时间片
 *   建议保持 1。
 */
#define configIDLE_SHOULD_YIELD                 1

/*
 * configUSE_TASK_NOTIFICATIONS —— 是否启用任务通知
 *   1 = 启用。每个任务自带一个 32 位通知值,可用于轻量级信号量/队列
 *   0 = 关闭以节省 RAM(每个任务省 8 字节)
 *   建议开启,任务通知比信号量更快。
 */
#define configUSE_TASK_NOTIFICATIONS            1

/*
 * configUSE_TIME_SLICING —— 是否启用同优先级时间片轮转
 *   1 = 开启,同优先级任务轮流运行(每个 Tick 切换一次)
 *   0 = 关闭,同优先级任务需主动让出 CPU
 */
#define configUSE_TIME_SLICING                  1

/*
 * configNUM_THREAD_LOCAL_STORAGE_POINTERS —— 线程本地存储指针的数量
 *   每个任务可以拥有 N 个 void* 类型的本地存储槽,
 *   用于任务私有的上下文数据,通过 vTaskSetThreadLocalStoragePointer() 访问。
 *   设为 0 可省一点 RAM。
 */
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5

/*===========================================================================
 *  内存分配方式
 *===========================================================================*/

/*
 * configSUPPORT_STATIC_ALLOCATION —— 支持静态分配
 *   1 = 启用,任务/队列/信号量等可以用静态内存(需提供回调函数)
 *   0 = 关闭,仅使用动态分配
 *   设为 0 则无需实现 vApplicationGetIdleTaskMemory 等回调,
 *   代码更简洁,适合 HelloWorld。
 */
#define configSUPPORT_STATIC_ALLOCATION         0

/*
 * configSUPPORT_DYNAMIC_ALLOCATION —— 支持动态分配
 *   1 = 启用,xTaskCreate()/xQueueCreate() 等自动从堆中分配
 *   0 = 关闭,只能用 xTaskCreateStatic() 等静态版本
 *   简单程序用动态分配最方便。
 */
#define configSUPPORT_DYNAMIC_ALLOCATION        1

/*===========================================================================
 *  同步与通信(IPC)
 *===========================================================================*/

/*
 * configUSE_MUTEXES —— 是否启用互斥量
 *   1 = 启用,支持 xSemaphoreCreateMutex() 和优先级继承
 */
#define configUSE_MUTEXES                       1

/*
 * configUSE_RECURSIVE_MUTEXES —— 是否启用递归互斥量
 *   1 = 启用,支持 xSemaphoreCreateRecursiveMutex()
 *   允许同一个任务多次获取同一互斥量而不会死锁。
 */
#define configUSE_RECURSIVE_MUTEXES             1

/*
 * configUSE_COUNTING_SEMAPHORES —— 是否启用计数信号量
 *   1 = 启用,支持 xSemaphoreCreateCounting()
 *   用于管理多个资源的场景(如生产者-消费者)。
 */
#define configUSE_COUNTING_SEMAPHORES           1

/*
 * configQUEUE_REGISTRY_SIZE —— 队列注册表大小
 *   调试辅助功能,用于在调试器中按名称查找队列/信号量。
 *   0 表示不注册。非调试时可设为 0 省 RAM。
 */
#define configQUEUE_REGISTRY_SIZE               8

/*
 * configUSE_QUEUE_SETS —— 是否启用队列集
 *   1 = 启用,支持 xQueueCreateSet() 等,可同时等待多个队列/信号量
 */
#define configUSE_QUEUE_SETS                    1

/*===========================================================================
 *  软件定时器
 *===========================================================================*/

/*
 * configUSE_TIMERS —— 是否启用软件定时器
 *   1 = 启用,需要使用 xTimerCreate()/xTimerStart() 等 API
 *   0 = 关闭,定时器相关的功能不可用
 */
#define configUSE_TIMERS                        1

/*
 * configTIMER_TASK_PRIORITY —— 定时器服务任务优先级
 *   建议设为一个较高的优先级,确保定时器回调及时执行。
 */
#define configTIMER_TASK_PRIORITY               ( configMAX_PRIORITIES - 1 )

/*
 * configTIMER_QUEUE_LENGTH —— 定时器命令队列长度
 *   定时器 API(如 xTimerStart)通过此队列向定时器任务发送命令。
 *   10 条对于测试程序足够。
 */
#define configTIMER_QUEUE_LENGTH                10

/*
 * configTIMER_TASK_STACK_DEPTH —— 定时器服务任务堆栈深度
 *   为定时器回调预留足够的堆栈空间。
 */
#define configTIMER_TASK_STACK_DEPTH            ( configMINIMAL_STACK_SIZE * 2 )

/*===========================================================================
 *  调试与追踪
 *===========================================================================*/

/*
 * configUSE_TRACE_FACILITY —— 是否启用运行时统计追踪功能
 *   1 = 启用,支持 uxTaskGetSystemState() / vTaskGetInfo() 等调试 API
 *   0 = 关闭以减小代码体积
 */
#define configUSE_TRACE_FACILITY                1

/*
 * configCHECK_FOR_STACK_OVERFLOW —— 堆栈溢出检测等级
 *   0 = 关闭检测(最快)
 *   1 = 检测栈指针是否超出范围(较安全)
 *   2 = 方法 1 + 检测栈尾部标记是否被破坏(更可靠,但稍慢)
 *   HelloWorld 中设为 0,省去实现 vApplicationStackOverflowHook() 的麻烦。
 */
#define configCHECK_FOR_STACK_OVERFLOW          0

/*
 * configASSERT —— 断言宏
 *   在 FreeRTOS API 参数校验失败时触发。
 *   调试时建议开启,发布版本可以定义为空。
 *   此处实现为:触发断言时关中断并死循环。
 */
#define configASSERT( x )                       if( ( x ) == 0 ) { taskDISABLE_INTERRUPTS(); for( ;; ); }

/*===========================================================================
 *  API 功能裁剪(设为 1 表示此 API 可用)
 *===========================================================================*/

#define INCLUDE_xTaskGetSchedulerState          1   /* 获取调度器状态(运行/挂起/未启动) */
#define INCLUDE_vTaskDelay                      1   /* 任务相对延时 */
#define INCLUDE_vTaskDelayUntil                 1   /* 任务绝对延时(固定周期) */
#define INCLUDE_vTaskDelete                     1   /* 删除任务 */
#define INCLUDE_eTaskGetState                   1   /* 查询任务状态 */
#define INCLUDE_xTaskGetCurrentTaskHandle       1   /* 获取当前任务句柄 */
#define INCLUDE_xTaskGetIdleTaskHandle          1   /* 获取空闲任务句柄 */
#define INCLUDE_xTaskGetHandle                  1   /* 通过名称查找任务句柄 */
#define INCLUDE_xSemaphoreGetMutexHolder        1   /* 查询互斥量持有者 */
#define INCLUDE_xTimerPendFunctionCall          1   /* 在定时器任务上下文中执行函数 */

#endif /* FREERTOS_CONFIG_H */

  • 下载

    https://github.com/FreeRTOS 仓库有很多freeRTOS关于协议栈的实现,HTTP,TCP,MQTT等

    这里只需要核心代码去模拟

    https://github.com/FreeRTOS/FreeRTOS-Kernel
    
  • 安装cmake

    省略,可以前往 LVGL-vscode模拟

  • vscode安装各种插件

    省略,可以前往 LVGL-vscode模拟

  • vscode项目配置

    .vscode/settings.json文件

    {
        "cmake.sourceDirectory": "E:/_1/embedded-study/freeRTOS/vscode/FreeRTOS-Kernel",
        "cmake.cmakePath": "E:/Program Files/CMake/bin/cmake.exe"
    }
    
    • cmake.sourceDirectory:cmake那个CMakeLists.txt文件目录
    • cmake.cmakePath:前面安装cmake的目录
  • 移除不需要的文件/文件夹

    • 删除所有目录下的CMakeFile.txt
    • 删除其他无用文件。如图:
    1
  • 测试

  • 编写测试程序

    main.c

    #include <FreeRTOS.h>
    #include <task.h>
    #include <stdio.h>
    
    void vTask(void *pv)
    {
        (void)pv;
        for (;;)
        {
            printf("Hello World\n");
            vTaskDelay(1000);
        }
    }
    
    void main(void)
    {
        xTaskCreate(vTask, "Task", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
        vTaskStartScheduler();
    }
    
  • freeRTOS配置文件

    FreeRTOSConfig.h

  • CmakeList.txt文件

    cmake_minimum_required(VERSION 3.15)
    project(example LANGUAGES C)
    
    # ─── Select FreeRTOS port & heap ───
    set(FREERTOS_PORT "MSVC-MingW" CACHE STRING "" FORCE)
    set(FREERTOS_HEAP "4"          CACHE STRING "" FORCE)
    
    # ─── User-provided FreeRTOSConfig.h ───
    add_library(freertos_config INTERFACE)
    target_include_directories(freertos_config INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
    
    # ─── FreeRTOS-Kernel library (from root) ───
    add_subdirectory(../ FreeRTOS-Kernel)
    
    # ─── Our test executable ───
    add_executable(${PROJECT_NAME} main.c)
    target_link_libraries(${PROJECT_NAME} freertos_kernel freertos_config)
    
    set_target_properties(${PROJECT_NAME} PROPERTIES C_STANDARD 90)
    

任务切换

在多任务情况下,利用中断功能,以及现场保护。

当前任务->停止->用到的寄存器入栈->时间片到了->出栈->恢复继续执行

一共有三个任务为例:

​ 任务1,任务2,任务三。

当时间片1ms到来的时候进入中断,这时候cpu将一部分寄存器入栈,然后freeRTOS又将其他用到的寄存器入栈。注意入的是任务栈,不是cpu的中断处理栈。然后freeRTOS将sp指针指向任务2的栈,将栈帧里面的PC指针指向任务2的PC,LR寄存器也改成任务2的LR。cpu恢复现场的时候就不是恢复任务1的现场,而是任务2的现场,从而达到任务切换的效果。

#include <FreeRTOS.h>
#include <task.h>
#include <stdio.h>
TaskHandle_t h1;
void vTask1(void *pv)
{
    (void)pv; /* 防止编译器警告:未使用参数 pv */
    for (;;) // 无限循环,FreeRTOS 任务绝不能直接 return
    {
        printf("Hello 1\n");
        vTaskDelay(100); //freertos专用定时
    }
}
void main(void)
{
     BaseType_t rt = xTaskCreate(vTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, &h1);
    vTaskStartScheduler();//启动 FreeRTOS 调度器
}
参数说明:
	1. vTask1              - 任务函数指针
	2. "Task1"             - 任务名(调试用,最大长度由 configMAX_TASK_NAME_LEN 决定)
	3. configMINIMAL_STACK_SIZE
                        - 任务栈大小(单位是“字”,不是字节)
                          configMINIMAL_STACK_SIZE 是 FreeRTOS 推荐的最小栈
                          对于 printf 这类函数,实际项目中可能需要更大
	4. NULL                - 传给任务函数的参数(这里不需要,传 NULL)
	5. 1                   - 任务优先级(数字越大,优先级越高) 0 是最低优先级
	6. &h1                 - 任务句柄,用于后续操作该任务

	BaseType_t rt		   -返回值:pdPASS / errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY

Linux技术栈

Linux IO

读取一个文件,并开启非阻塞式

  • O_RDONLY:只读

  • O_NONBLOCK:非阻塞

代码编写

int fd;
int init_read()
{
    int fd = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
    if(fd == -1)
        return -1;
    return 0;
}
int btns_read(int fd, btn_code_t *code, btn_event_t *event)
{
    struct input_event ev; // 输入事件

    ssize_t n = read(fd, &ev, sizeof(ev));
    if (n < 0) { //1.是否有错
        if (errno == EAGAIN || errno == EWOULDBLOCK) 
            return 0;
        return -1;
    }
    if ((size_t)n < sizeof(ev)) //2.返回的数据是不是input_event
        return -1;
    if (ev.type != EV_KEY) //3.如果是按钮才执行
        continue;
    //4.参数传递回去
    *code  = ev.code;
    *event = ev.value ? BTN_PRESSED : BTN_RELEASED;

    printf("btns: fd=%d ev.code=%u ev.value=%d -> btn=%d\n",
           fd, (unsigned)ev.code, ev.value, (int)c);
    fflush(stdout); // 5.fflush 保证日志不卡在缓冲区里
    return 1;
}

解析

  • 开启一个文件设置为非阻塞模式。

Linux驱动(模块)

001. 字符设备驱动

一、环境

apt update
apt install linux-headers-$(uname -r)

二、编写文件

  • 001_helloworld.c
#include <linux/init.h>
#include <linux/module.h>

// 模块许可证声明,必须是 GPL 兼容的许可证才能加载到内核中
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("wyl");
// 模块描述
MODULE_DESCRIPTION("Hello World");

// 模块加载时调用的函数
static int hello_init(void)
{
    printk("Hello, World! Driver loaded.\n");
    return 0; // 返回 0 表示加载成功
}

// 模块卸载时调用的函数
static void hello_exit(void)
{
    printk("Goodbye, World! Driver unloaded.\n");
}

// 注册模块的入口和出口函数
module_init(hello_init);
module_exit(hello_exit);
  • Makefile
# 如果未定义 KERNELDIR,则使用当前运行的内核版本路径
KERNELDIR ?= /lib/modules/$(shell uname -r)/build

# 当前目录
PWD := $(shell pwd)

# 模块名称,对应 .c 文件名去掉 .c 后缀
obj-m := 001_helloworld.o

# 默认目标:编译模块
all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules #-C : Change Directory(切换目录) -M : Module Directory(模块目录) modules : Make Target(构建目标)

# 清理目标
clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

# 安装模块 (可选)
install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

# 卸载模块 (可选)
uninstall:
	rm -f /lib/modules/$(shell uname -r)/extra/001_helloworld.ko
	depmod -a
  • 编译使用
make #必须使用root用户
insmod 001_helloworld.ko #加载驱动,必须使用root用户
dmesg | tail #查看驱动加载情况
rmmod 001_helloworld  #卸载驱动,必须使用root用户
dmesg | tail #查看驱动卸载情况
  • 查看挂载情况

    lsmod
    
    cat /proc/devices
    

动态申请设备号

只学习怎么申请设备号。暂时没什么用。

  • 挂载设备
insmod 002_alloc_chrdev.ko
  • 查看
root@docker:~/driver_std/002_alloc_char_dev# lsmod
Module                  Size  Used by
004_alloc_chrdev       16384  0
root@docker:~/driver_std/002_alloc_char_dev# cat /proc/devices
Character devices:
236 my_chrdev #(设备名称)

代码:002_alloc_chrdev.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/kdev_t.h>
#include <linux/moduleparam.h>

//主设备号
static int major = 0;
module_param(major, int, S_IRUGO);
//次设备号
static int minor = 0;
module_param(minor, int, S_IRUGO);
//设备名称
static char* name = "my_chrdev";

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("alloc_chrdev");

static dev_t dev_no; //设备号:32位unsigned int,主设备号(前12位)+次设备号(后20位)

static int moduleparam_init(void)
{
    int ret;

    // 分配设备号:如果指定了major则使用register_chrdev_region,否则自动分配
    if (major) {
        dev_no = MKDEV(major, minor);
        ret = register_chrdev_region(dev_no, 1, name);
        printk(KERN_INFO "Using specified major=%d, minor=%d\n", major, minor);
    } else {
        ret = alloc_chrdev_region(&dev_no, 0, 1, name);
        major = MAJOR(dev_no); //获取主设备号
        minor = MINOR(dev_no); //获取次设备号
        printk(KERN_INFO "Auto allocated major=%d, minor=%d\n", major, minor);
    }

    if (ret < 0) {
        printk(KERN_ALERT "Failed to allocate device number: %d\n", ret);
        return ret;
    }

    printk(KERN_INFO "Device number allocation success\n");
    return 0;
}

static void moduleparam_exit(void)
{
    unregister_chrdev_region(dev_no, 1);
    printk(KERN_INFO "unregister_chrdev_region success\n");
}

module_init(moduleparam_init);
module_exit(moduleparam_exit);

创建的是一个真正可被系统识别的字符设备

在 002 的基础上增加了:

新增内容作用
struct cdev cdev_test内核中代表一个字符设备
struct file_operations fops定义设备的操作接口(open/read/write 等)
cdev_init()把 cdev 和 file_operations 绑定
cdev_add()向内核注册,设备变为"活"状态
cdev_del()卸载时移除 cdev

代码:003_register_cdev.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>

dev_t dev_no; // 设备号
struct cdev cdev_test; // 字符设备结构体

struct file_operations fops = {
    .owner = THIS_MODULE
};

//设备名称
static char* name = "003_register_cdev";

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("003_register_cdev");
static int modulecdev_init(void)
{ 
    int ret;
    ret = alloc_chrdev_region(&dev_no, 0, 1, name);
    if (ret<0) {
        printk("alloc_chrdev_region failed\n");
        return ret;
    }
    printk("major:%d, minor:%d\n", MAJOR(dev_no), MINOR(dev_no));
    cdev_init(&cdev_test, &fops);
    cdev_add(&cdev_test, dev_no, 1);
    return 0;
}
static void modulecdev_exit(void)
{
    cdev_del(&cdev_test);
    unregister_chrdev_region(dev_no, 1);
    printk("modulecdev exit\n");
}

module_init(modulecdev_init);
module_exit(modulecdev_exit);

挂载

  • 挂载设备
insmod 004_file_operations.ko
  • 查看
lsmod
#可以看到
Module                  Size  Used by
004_file_operations       16384  0
cat /proc/devices
#可以看到
Character devices:
236 004_file_operations #(设备名称)
  • 创建设备节点(最好和代码中的一致)

    sudo mknod /dev/<设备名> c <主设备号> <次设备号>

    mknod /dev/004_register_cdev c 236 0
    
    chmod 666 /dev/004_register_cdev
    
    ls /dev/004*
    

卸载

  • 移除设备节点

    rm /dev/004_register_cdev
    
  • 取消挂载

    rmmod 004_file_operations.ko
    

代码:004_file_operations.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>

dev_t dev_no; // 设备号
struct cdev cdev_test; // 字符设备结构体

static int test_open(struct inode *inode,struct file *file)
{
    printk("open\n");
    return 0;
}
static ssize_t test_read(struct file *file,char __user *buf, size_t size,loff_t *off){
    printk("read\n");
    return 0;
}
static ssize_t test_write(struct file *file,const char __user *buf, size_t size,loff_t *off){
    printk("write\n");
    return 0;
}
static int test_release(struct inode *inode,struct file *file)
{
    printk("release\n");
    return 0;
}
struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .read = test_read,
    .write = test_write,
    .release = test_release,
};

//设备名称
static char* name = "004_register_cdev";

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("004_register_cdev");
static int modulecdev_init(void)
{ 
    int ret;
    ret = alloc_chrdev_region(&dev_no, 0, 1, name);
    if (ret<0) {
        printk("alloc_chrdev_region failed\n");
        return ret;
    }
    printk("major:%d, minor:%d\n", MAJOR(dev_no), MINOR(dev_no));
    cdev_init(&cdev_test, &fops);
    cdev_add(&cdev_test, dev_no, 1);
    return 0;
}
static void modulecdev_exit(void)
{
    cdev_del(&cdev_test);
    unregister_chrdev_region(dev_no, 1);
    printk("modulecdev exit\n");
}

module_init(modulecdev_init);
module_exit(modulecdev_exit);

自动创建设备节点,不需要使用mknod去创建/dev下的设备节点

流程

  • 编译和挂载

    make
    
    insmod 005_udev_mdev.ko
    
  • 查看

    ls /sys/class/005_udev_mdev #struct class *class_test 的 class_create(THIS_MODULE, name)中的name对应
    
    lsmod #命令
    Module                  Size  Used by
    005_udev_mdev          16384  0
    
    cat /proc/devices # 命令
    Character devices:
    	237 005_udev_mdev
    
    ls /dev/005_udev_mdev #device_create(class_test, NULL, dev_no, NULL, name)中的name对应
    
  • 卸载

    会卸载**/proc/devices**,/dev/sys/class下有的东西

    rmmod 005_udev_mdev.ko
    

代码:005_udev_mdev.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <linux/uaccess.h>

dev_t dev_no; // 设备号
struct cdev cdev_test; // 字符设备结构体

struct class *class_test; // 类
struct device *device_test; // 设备

static int test_open(struct inode *inode,struct file *file)
{
    printk("open\n");
    return 0;
}
static ssize_t test_read(struct file *file,char __user *buf, size_t size,loff_t *off){
    printk("read\n");
    return 0;
}
static ssize_t test_write(struct file *file,const char __user *buf, size_t size,loff_t *off){
    printk("write\n");
    *off += size;
    return size;
}
static int test_release(struct inode *inode,struct file *file)
{
    printk("release\n");
    return 0;
}
struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .read = test_read,
    .write = test_write,
    .release = test_release,
};

//设备名称
static char* name = "005_udev_mdev";

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("005_udev_mdev");
static int modulecdev_init(void)
{ 
    int ret;
    ret = alloc_chrdev_region(&dev_no, 0, 1, name);
    if (ret<0) {
        printk("alloc_chrdev_region failed\n");
        return ret;
    }
    printk("major:%d, minor:%d\n", MAJOR(dev_no), MINOR(dev_no));
    cdev_init(&cdev_test, &fops);
    cdev_add(&cdev_test, dev_no, 1);

    class_test = class_create(THIS_MODULE, name); //创建类
    device_test = device_create(class_test, NULL, dev_no, NULL, name);
    return 0;
}
static void modulecdev_exit(void)
{
    // 1. 先销毁设备节点
    device_destroy(class_test, dev_no);
    
    // 2. 销毁类 (原代码缺失此步,必须添加)
    class_destroy(class_test);
    
    // 3. 删除字符设备
    cdev_del(&cdev_test);
    
    // 4. 最后注销设备号
    unregister_chrdev_region(dev_no, 1);
    
    printk("modulecdev exit\n");
}

module_init(modulecdev_init);
module_exit(modulecdev_exit);

代码:app.c

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
  int fd = open("/dev/005_udev_mdev", O_RDWR);
  if (fd < 0) {
    printf("open error\n");
    return -1;
  }
  printf("open success\n");
  close(fd);
  return 0;
}
insmod → init() →
  ├─ alloc_chrdev_region() → /proc/devices 多一行  [黄页:告诉你 236 是谁]
  ├─ cdev_add()            → 内核 cdev 链表注册    [内核内部:使设备号可路由到 fops]
  ├─ class_create()        → /sys/class/ 下建目录  [公告牌:告知用户空间]
  │
  └─ device_create()
       ├─ /sys/class/xxx/dev 出现 "236:0"
       └─ 发 uevent ──→ udev/mdev
                            ↓
                      mknod /dev/005_udev_mdev  [门:用户程序访问入口]

编写的驱动用户空间和内核空间数据交互

代码:006_kernel_user_data.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <linux/uaccess.h>

struct device_data{
    dev_t dev_no; // 设备号

    struct cdev cdev_test; // 字符设备结构体
    struct class *class_test; // 类
    struct device *device_test; // 设备
    char kbuf[32];
};
struct device_data dev_data;
static int test_open(struct inode *inode,struct file *file)
{
    // 打开驱动时,配置私有数据
    file->private_data = &dev_data;
    printk("open\n");
    return 0;
}
static ssize_t test_read(struct file *file,char __user *buf, size_t size,loff_t *off){
    //拿到私有数据
    struct device_data *priv_data =  (struct device_data *)file->private_data;

    if(copy_to_user(buf,priv_data->kbuf , strlen(priv_data->kbuf))!=0){
        printk("copy_to_user failed\n");
        return -1;
    }
    printk("read\n");
    return 0;
}
static ssize_t test_write(struct file *file,const char __user *buf, size_t size,loff_t *off){
    // 拿到私有数据
    struct device_data *priv_data =  (struct device_data *)file->private_data;
    if(size > sizeof(priv_data->kbuf)){
        printk("data too large\n");
        return -ENOSPC;
    }
    if(copy_from_user(priv_data->kbuf, buf, size)!=0){
        printk("copy_from_user failed\n");
        return -EFAULT;
    }
    priv_data->kbuf[size - 1] = '\0'; // 确保字符串终止
    *off += size;
    printk("write\n");
    return size;
}
static int test_release(struct inode *inode,struct file *file)
{
    printk("release\n");
    return 0;
}
struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .read = test_read,
    .write = test_write,
    .release = test_release,
};

//设备名称
static char* name = "006_kernel_user_data";

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("006_kernel_user_data");
static int modulecdev_init(void)
{ 
    int ret;
    ret = alloc_chrdev_region(&dev_data.dev_no, 0, 1, name);
    if (ret<0) {
        printk("alloc_chrdev_region failed\n");
        return ret;
    }
    printk("major:%d, minor:%d\n", MAJOR(dev_data.dev_no), MINOR(dev_data.dev_no));
    cdev_init(&dev_data.cdev_test, &fops);
    cdev_add(&dev_data.cdev_test, dev_data.dev_no, 1);

    dev_data.class_test = class_create(THIS_MODULE, name); //创建类
    dev_data.device_test = device_create(dev_data.class_test, NULL, dev_data.dev_no, NULL, name);
    return 0;
}
static void modulecdev_exit(void)
{
    // 1. 先销毁设备节点
    device_destroy(dev_data.class_test, dev_data.dev_no);
    
    // 2. 销毁类 (原代码缺失此步,必须添加)
    class_destroy(dev_data.class_test);
    
    // 3. 删除字符设备
    cdev_del(&dev_data.cdev_test);
    
    // 4. 最后注销设备号
    unregister_chrdev_region(dev_data.dev_no, 1);
    
    printk("modulecdev exit\n");
}

module_init(modulecdev_init);
module_exit(modulecdev_exit);

代码:app.c

#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main()
{
    int fd = open("/dev/006_kernel_user_data", O_RDWR);
    char rbuf[64];
    char wbuf[64];

    if (fd < 0)
    {
        printf("open error\n");
        return -1;
    }
    printf("open success\n");

    // 用户 -> 内核:写入数据到驱动
    strcpy(wbuf, "hello from user space!");
    write(fd, wbuf, strlen(wbuf) + 1);
    printf("write to kernel: %s\n", wbuf);

    // 内核 -> 用户:从驱动读取数据
    memset(rbuf, 0, sizeof(rbuf));
    read(fd, rbuf, sizeof(rbuf));
    printf("read from kernel: %s\n", rbuf);

    close(fd);
    return 0;
}

杂项设备(misc device)在Linux内核驱动开发中有以下典型应用场景:

1. 简单字符设备的快速注册

  • 适用于功能相对简单、不需要复杂设备管理的字符设备
  • 自动分配主设备号(固定为10),只需关心次设备号
  • 简化了传统字符设备的注册流程(无需手动申请设备号、创建类和设备节点)

2. 系统级小工具驱动

  • 看门狗定时器(watchdog)
  • 温度传感器读取
  • LED控制
  • 蜂鸣器控制
  • 按键输入处理

3. 硬件抽象层

  • 为上层应用提供简单的硬件访问接口
  • 例如:GPIO控制、I2C/SPI设备访问、ADC读取等

4. 调试和测试设备

  • 内核模块开发时的测试驱动
  • 提供简单的读写接口用于验证内核与用户空间通信

5. 特殊功能设备

  • 随机数生成器(/dev/random, /dev/urandom)
  • 内存映射设备(/dev/mem, /dev/kmem)
  • 日志设备(/dev/kmsg)

杂项设备的优势:

简化注册流程:只需调用 misc_register(),自动完成设备号分配、设备节点创建 ✅ 统一管理:所有杂项设备共享主设备号10,便于系统管理 ✅ 自动创建设备节点:在 /dev/ 下自动生成设备文件 ✅ 适合小型驱动:代码量少,结构清晰

代码:007_misc.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>

static int test_open(struct inode *inode,struct file *file)
{
    printk("open\n");
    return 0;
}
static ssize_t test_read(struct file *file,char __user *buf, size_t size,loff_t *off){

    printk("read\n");
    return 0;
}
static ssize_t test_write(struct file *file,const char __user *buf, size_t size,loff_t *off){

    *off += size;
    printk("write\n");
    return size;
}
static int test_release(struct inode *inode,struct file *file)
{
    printk("release\n");
    return 0;
}
struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .read = test_read,
    .write = test_write,
    .release = test_release,
};


MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("007_misc");

// 杂项设备
static struct miscdevice misc_dev = {
    .minor = MISC_DYNAMIC_MINOR, //自动分配从设备号
    .name = "007_misc", //设备名称
    .fops = &fops,
};
static int modulecdev_init(void)
{ 
    int ret;
    ret = misc_register(&misc_dev);
    if(ret<0){
        printk(KERN_ERR "misc_register failed!\n");
        return ret;
    }
    printk(KERN_INFO "misc_register success!\n");
    return 0;
}
static void modulecdev_exit(void)
{
    misc_deregister(&misc_dev); //卸载
    printk("modulecdev exit\n");
}

module_init(modulecdev_init);
module_exit(modulecdev_exit);

在编写驱动时,返回的错误最好使用以下错误码

/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H

#define	EPERM		 1	/* Operation not permitted */
#define	ENOENT		 2	/* No such file or directory */
#define	ESRCH		 3	/* No such process */
#define	EINTR		 4	/* Interrupted system call */
#define	EIO		 5	/* I/O error */
#define	ENXIO		 6	/* No such device or address */
#define	E2BIG		 7	/* Argument list too long */
#define	ENOEXEC		 8	/* Exec format error */
#define	EBADF		 9	/* Bad file number */
#define	ECHILD		10	/* No child processes */
#define	EAGAIN		11	/* Try again */
#define	ENOMEM		12	/* Out of memory */
#define	EACCES		13	/* Permission denied */
#define	EFAULT		14	/* Bad address */
#define	ENOTBLK		15	/* Block device required */
#define	EBUSY		16	/* Device or resource busy */
#define	EEXIST		17	/* File exists */
#define	EXDEV		18	/* Cross-device link */
#define	ENODEV		19	/* No such device */
#define	ENOTDIR		20	/* Not a directory */
#define	EISDIR		21	/* Is a directory */
#define	EINVAL		22	/* Invalid argument */
#define	ENFILE		23	/* File table overflow */
#define	EMFILE		24	/* Too many open files */
#define	ENOTTY		25	/* Not a typewriter */
#define	ETXTBSY		26	/* Text file busy */
#define	EFBIG		27	/* File too large */
#define	ENOSPC		28	/* No space left on device */
#define	ESPIPE		29	/* Illegal seek */
#define	EROFS		30	/* Read-only file system */
#define	EMLINK		31	/* Too many links */
#define	EPIPE		32	/* Broken pipe */
#define	EDOM		33	/* Math argument out of domain of func */
#define	ERANGE		34	/* Math result not representable */

#endif

002. 平台总线驱动

平台总线时linux系统虚拟出来的总线。

原来的驱动拆成两个部分进行解耦。device.cdriver.c

device.c:用来描述硬件信息。相当于后面要学习的设备树

**driver.c:**用来控制硬件,即硬件的驱动。

先分离,后搭档。

如何分离

如何搭档

流程

make
insmod 001_helloworld_driver.ko #device和driver不分先后
insmod 001_helloworld_device.ko #device和driver不分先后
ls /sys/bus/platform/devices #注册设备时添加
#生成 my_device_test文件,my_device_test文件和device.c中platform_device.name一致
ls /sys/bus/platform/drivers
#生成 my_device_test文件,my_device_test文件和driver.c中platform_driver.name一致

linux系统会自动将名字一样的设备和驱动关联在一起。几乎是瞬间。

代码:001_helloworld_device.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>

static struct resource my_device_resource[]={
    [0]={
        .start = 0x0,
        .end = 0xA,
        .flags = IORESOURCE_MEM
    },
    [1]={
        .start = 0xB,
        .end = 0xF,
        .flags = IORESOURCE_IRQ
    }
};
void my_device_release(struct device *dev){
    printk("this is my_device_release");
}
struct platform_device my_device_test = {
    .name="my_device_test",
    .id = -1,
    .resource = my_device_resource,
    .dev={
        .release= my_device_release
    }
};
static int platform_init(void)
{
    platform_device_register(&my_device_test);
    printk("platform_init");
    return 0;
}
static void platform_exit(void)
{
    platform_device_unregister(&my_device_test);
    printk("platform_exit");
}

module_init(platform_init);
module_exit(platform_exit);

// 模块许可证声明,必须是 GPL 兼容的许可证才能加载到内核中
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("wyl");
// 模块描述
MODULE_DESCRIPTION("A simple Hello World Linux Driver");

代码:001_helloworld_driver.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
int my_probe(struct platform_device * dev){
    printk("my_probe");
    return 0;
}
int my_remove(struct platform_device * dev){
    printk("my_remove");
    return 0;
};
// const struct platform_device_id my_id_table[] = {
//     {.name = "my_device_test"},
//     {},
// };
struct platform_driver driver_test= {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device_test", //与device设备描述名字一致!!!
        .owner = THIS_MODULE,

        // const struct platform_device_id *id_table; //非必须,匹配时优先级最高比上面的name要高
    }
};
static int platform_driver_init(void)
{
    platform_driver_register(&driver_test);
    printk("platform_init");
    return 0;
}
static void platform_driver_exit(void)
{
    platform_driver_unregister(&driver_test);
    printk("platform_exit");
}

module_init(platform_driver_init);
module_exit(platform_driver_exit);

// 模块许可证声明,必须是 GPL 兼容的许可证才能加载到内核中
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("wyl");
// 模块描述
MODULE_DESCRIPTION("A simple Hello World Linux Driver");

什么时候需要 id_table

  • 一个驱动支持多个不同名字的设备时(比如同一芯片不同型号)
  • 需要 driver_data 传递设备差异信息时(每个 id 入口可以带一个 kernel_ulong_t 的数据)
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>

//设备资源(设备描述)
static struct resource led_resources[]={
    [0]={
        .start = 0x0,
        .end = 0xA,
        .flags = IORESOURCE_MEM
    },
    [1]={
        .start = 0xB,
        .end = 0xF,
        .flags = IORESOURCE_IRQ
    },
    [2]={
        .start = 0xAA,
        .end = 0xAF,
        .flags = IORESOURCE_IRQ
    }
};
static void led_device_release(struct device *dev)
{
    printk("led_device_release\n");
}

static struct platform_device led_device = {
    .name = "led_device",
    .id = -1,
    .resource = led_resources,
    .dev = {
        .release = led_device_release,
    },
};
static int led_module_init(void)
{
    platform_device_register(&led_device);
    printk("hello world\n");
    return 0;
}
static void led_module_exit(void)
{
    platform_device_unregister(&led_device);
    printk("goodbye world\n");
}

module_init(led_module_init);
module_exit(led_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("WYL");
MODULE_DESCRIPTION("led device");
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/device.h>
#include <linux/uaccess.h>
dev_t dev_no; // 设备号
struct cdev cdev_test; // 字符设备结构体

struct class *class_test; // 类
struct device *device_test; // 设备
static int test_open(struct inode *inode,struct file *file)
{
    printk("open\n");
    return 0;
}
static ssize_t test_read(struct file *file,char __user *buf, size_t size,loff_t *off){
    printk("read\n");
    return 0;
}
static ssize_t test_write(struct file *file,const char __user *buf, size_t size,loff_t *off){
    printk("write\n");
    *off += size;
    return size;
}
static int test_release(struct inode *inode,struct file *file)
{
    printk("release\n");
    return 0;
}
struct file_operations fops = {
    .owner = THIS_MODULE,
    .open = test_open,
    .read = test_read,
    .write = test_write,
    .release = test_release,
};
int my_probe(struct platform_device * dev){
    
    int ret;
    ret = alloc_chrdev_region(&dev_no, 0, 1, "led_device");
    if (ret<0) {
        printk("alloc_chrdev_region failed\n");
        return ret;
    }
    printk("major:%d, minor:%d\n", MAJOR(dev_no), MINOR(dev_no));
    cdev_init(&cdev_test, &fops);
    cdev_add(&cdev_test, dev_no, 1);

    class_test = class_create(THIS_MODULE, "led_device"); //创建类
    device_test = device_create(class_test, NULL, dev_no, NULL, "led_device");

    printk("my_probe"); //device和driver匹配成功时调用
    return 0;
}
int my_remove(struct platform_device * dev){
    
    printk("my_remove"); //移除设备或这个驱动被rmmod时调用
    return 0;
};
struct platform_driver driver_test= {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "led_device", //与device设备描述名字一致!!!
        .owner = THIS_MODULE,

        // const struct platform_device_id *id_table; //非必须,匹配时优先级最高比上面的name要高
    }
};
static int platform_driver_init(void)
{
    platform_driver_register(&driver_test);
    printk("platform_driver_init");
    return 0;
}
static void platform_driver_exit(void)
{
    // 1. 先销毁设备节点
    device_destroy(class_test, dev_no);
    
    // 2. 销毁类 (原代码缺失此步,必须添加)
    class_destroy(class_test);
    
    // 3. 删除字符设备
    cdev_del(&cdev_test);
    
    // 4. 最后注销设备号
    unregister_chrdev_region(dev_no, 1);
    platform_driver_unregister(&driver_test);
    printk("platform_driver_exit");
}

module_init(platform_driver_init);
module_exit(platform_driver_exit);

// 模块许可证声明,必须是 GPL 兼容的许可证才能加载到内核中
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("wyl");
// 模块描述
MODULE_DESCRIPTION("A simple Hello World Linux Driver");

003. 设备树

GPIO驱动

设备树驱动支持的属性

文档路径路径:

/Documentation/devicetree/bindings

概念

应用层 (LVGL / Qt / shell)
    ↓
接口层  (sysfs / libgpiod / /dev/mem / 字符设备)
    ↓
内核层  (pinctrl / gpiolib / gpio-leds / gpio-keys ...)
    ↓
硬件层  (SoC GPIO 控制器寄存器)

常见 compatible 一览

compatible用途控制方式
gpio-keys按键输入/dev/input/eventX
gpio-ledsLED 亮灭/sys/class/leds/
gpio-beeper蜂鸣器/sys/class/input/inputX/
regulator-fixed电源开关regulator 框架
gpio-poweroff关机断电内核 poweroff 流程自动触发
gpio-restart重启内核 restart 流程自动触发
gpio-muxGPIO 选通mux 框架
gpio-wdt硬件看门狗watchdog 框架

驱动源码路径:

drivers/input/keyboard/gpio_keys.c

文档路径:

Documentation/devicetree/bindings/input/gpio-keys.yaml

设备树配置:

gpio-keys {
    compatible = "gpio-keys";         // 绑定内核 drivers/input/keyboard/gpio_keys.c
    pinctrl-names = "default";        // 使用 default 状态的 pinmux 配置
    pinctrl-0 = <&soc_gpio_bell>;     // 引用下面的 pinmux 定义

    bell-key {
        label = "SOC_GPIO_BELL";       // 名字,debug 时能看到
        linux,code = <KEY_POWER>;      // 按键码 116,上报给 input 子系统
        gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>;  // GPIO0_A3,低电平触发
        debounce-interval = <10>;      // 10ms 消抖
        wakeup-source;                 // 休眠时这个键能唤醒 SoC
    };
};
&pinctrl {
    bell {                              // 分组名,方便管理
        soc_gpio_bell: soc-gpio-bell {  // 标签 + 节点名
            rockchip,pins = <0 RK_PA3 RK_FUNC_GPIO &pcfg_pull_up>; #对应引脚和模式,驱动方式
        };
    };
};

leds {
    compatible = "gpio-leds";

    led0: led-0 {
        gpios = <&gpio0 RK_PA0 GPIO_ACTIVE_HIGH>;
        linux,default-trigger = "heartbeat";  // 自动心跳闪烁
    };

    led1: led-1 {
        gpios = <&gpio0 RK_PA1 GPIO_ACTIVE_LOW>;
        default-state = "on";                 // 默认亮
    };
};

  • 如果使用pwm并且配置compatible = "pwm-backlight"(与pwm_bl 驱动进行匹配);会受到驱动影响,屏幕初始化失败有可能导致背光的电源强制关闭bl_power = 4

    DRM 面板驱动把背光禁用了

  • 在设备树的树根/下配置backlight节点

    // LCD Backlight
    backlight: backlight {
    	compatible = "pwm-backlight";
    	pwms = <&pwm3 0 50000 0>;
    	brightness-levels = <0 20 40 60 80 100 200 255>;
    	default-brightness-level = <7>;
    	status = "okay";
    };
    
  • 配置pwm3的复用(pinctrl子系统)

    &pwm3 {
    	status = "okay";
    	pinctrl-0 = <&pwm3m1_pins>;
    };
    
  • GPIO测试背光

    echo 40 > /sys/class/gpio/export
    echo out > /sys/class/gpio/gpio40/direction
    echo 1 > /sys/class/gpio/gpio40/value
    

Pinctrl子系统

RK引脚宏定义在rk_gpio.h中,其他厂商SOC也类型

不同板子的pinctrl相关配置一般在名为:xxx-pinctrl.dtsi文件中

关于引脚驱动方式

pcfg_ 配置一览:

含义
pcfg_pull_up内部上拉
pcfg_pull_down内部下拉
pcfg_pull_none浮空(不拉)
pcfg_output_high初始输出高
pcfg_output_low初始输出低
pcfg_pull_up_drv_等级上拉 + 驱动强度

案例

  • 借鉴已经有的lcd驱动配置

    这里借鉴:rv1106-evb-ext-rgb-v10.dtsi

  • DTS 中相关的几个节点:

    通过VOP(video output processor)视频输出处理器,rk内部处理模块。功能如下:

    1. 图层合成 — 支持多个显示图层(video layer、UI layer 等),将它们叠加合成输出
    2. 格式转换 — 将帧缓冲区的数据转换成显示接口所需的格式
    3. 输出路由 — 把信号路由到不同的显示接口(RGB、MIPI DSI、LVDS、HDMI 等)
    节点作用
    &vopVOP 模块本身,必须 status = "okay"
    &rgbRGB 并行接口(VOP 的输出之一)
    &route_rgb路由选择,告诉 VOP 把信号送到 RGB 接口
    &rgb_in_vopVOP 到 RGB 的输入连接
    &rgb {
    	status = "okay";
    	/**省略*/
    };
    &rgb_in_vop {
    	status = "okay";
    };
    
    &route_rgb {
    	status = "okay";
    };
    
    &sdio {
    	status = "disabled"; //有些sdmmc复用会影响,禁用掉
    };
    
    &vop {
    	status = "okay";
    };
    
    
  • 设备树下的rbg相关配置

    • clock-frequency 像素时钟频率

    • hactive 分辨率 行

    • hback-porch

    • hfront-porch

    • hsync-len

    • hsync-active

    • vactive 分辨率 高

    • vback-porch

    • vfront-porch

    • vsync-len

    • vsync-active

    • de-active

    • pixelclk-active

    /* 显示时序配置 */
    display-timings {
        native-mode = <&timing0>;         /* 指定默认时序 */
        timing0: timing0 {
             clock-frequency = <31250000>; /* 像素时钟频率 31.25MHz */
             hactive = <240>;               /* 水平有效像素 */
             vactive = <320>;               /* 垂直有效像素 */
             hfront-porch = <360>;          /* 水平前沿 */
             hback-porch = <360>;           /* 水平后沿 */
             hsync-len = <80>;              /* 水平同步宽度 */
             vfront-porch = <20>;           /* 垂直前沿 */
             vback-porch = <20>;            /* 垂直后沿 */
             vsync-len = <8>;               /* 垂直同步宽度 */
             hsync-active = <0>;             /* HSYNC 低电平有效 */
             vsync-active = <0>;             /* VSYNC 低电平有效 */
             de-active = <1>;                /* DE 高电平有效 */
             pixelclk-active = <1>;          /* 像素时钟上升沿采样 */
        };
    };
    
  • 纯色测试

    • 白色
    dd if=/dev/zero bs=4 count=$((240*320)) 2>/dev/null | tr '\0' '\377' > /dev/fb0
    
    • 黑色
    dd if=/dev/zero bs=4 count=$((240*320)) 2>/dev/null > /dev/fb0
    
    • 红色
    python3 - <<'PY' > /dev/fb0
    import sys
    w, h = 240, 320
    red   = b'\x00\x00\xff\x00'
    sys.stdout.buffer.write(red * (w * h))
    PY
    
    • 绿色
    python3 - <<'PY' > /dev/fb0
    import sys
    w, h = 240, 320
    green = b'\x00\xff\x00\x00'
    sys.stdout.buffer.write(green * (w * h))
    PY
    
    • 蓝色
    python3 - <<'PY' > /dev/fb0
    import sys
    w, h = 240, 320
    blue  = b'\xff\x00\x00\x00'
    sys.stdout.buffer.write(blue * (w * h))
    PY
    
  • modetest测试

    1. modetest -c
      
      [root@luckfox ]# modetest -c
      Connectors:
      id      encoder status          name            size (mm)       modes   encoders
      70      69      connected       DPI-1           43x57           1       69
        modes:
              index name refresh (Hz) hdisp hss hse htot vdisp vss vse vtot
        #0 240x320 68.98 240 282 292 302 320 328 332 336 7000 flags: nhsync, nvsync; type: preferred, driver
      
    2. modetest -M rockchip -s 70@66:240x320
      
      参数含义在你的场景中的具体作用
      modetestDRM测试工具本身调用用户态API (libdrm) 与内核通信
      -M rockchip指定驱动模块名强制直接打开Rockchip的DRM设备(如/dev/dri/card0),跳过之前那些漫长的i915/amdgpu等探测超时,实现瞬间执行
      -s设置显示模式 (Set Mode)核心动作:将指定的Connector(显示器接口)和CRTC(显示控制器)绑定起来,并启用显示输出
      70@66:240x320连接器ID @ CRTC ID : 分辨率70 是你的DPI屏幕接口,66 是显示控制器,强制以 240x320 分辨率输出
      -v详细模式 (Verbose)让程序在运行过程中打印每一帧的提交细节、像素格式转换、以及页面翻转(Fence)等内核事件
      trying to open device 'i915'...failed
      trying to open device 'amdgpu'...failed
      trying to open device 'radeon'...failed
      trying to open device 'nouveau'...failed
      trying to open device 'vmwgfx'...failed
      trying to open device 'omapdrm'...failed
      trying to open device 'exynos'...failed
      trying to open device 'tilcdc'...failed
      trying to open device 'msm'...failed
      trying to open device 'sti'...failed
      trying to open device 'tegra'...failed
      trying to open device 'imx-drm'...failed
      trying to open device 'rockchip'...done
      setting mode 240x320-68.98Hz on connectors 70, crtc 66
      failed to set gamma: Invalid argument
      

    看时钟

    # 挂载 debugfs(如果没挂载)
    mount -t debugfs none /sys/kernel/debug
    
    # 查看 VOP dclk 实际频率
    cat /sys/kernel/debug/clk/clk_summary | grep -i dclk
    
    # 或者看所有和显示相关的时钟
    cat /sys/kernel/debug/clk/clk_summary | grep -i "vop\|dclk\|rgb"
    

说明

SPI初始化+RGB显示:SPI负责初始化参数,RGB线路负责显示图像。可以通过SPI通信控制RGB显示,比如翻转,改变分辨率之类操作。

流程

  1. SPI初始化
  2. 开启背光
  3. RGB控制
# 挂载 debugfs(如果没挂载)
mount -t debugfs none /sys/kernel/debug

# 查看 VOP dclk 实际频率
cat /sys/kernel/debug/clk/clk_summary | grep -i dclk

# 或者看所有和显示相关的时钟
cat /sys/kernel/debug/clk/clk_summary | grep -i "vop\|dclk\|rgb"
/ {
    model = "JW Kernel Board V1";
    compatible = "jw,kernel-board-v1", "rockchip,rv1106";

    vcc_1v8: vcc-1v8 {
        compatible = "regulator-fixed";
        regulator-name = "vcc_1v8";
        regulator-always-on;
        regulator-boot-on;
        regulator-min-microvolt = <1800000>;
        regulator-max-microvolt = <1800000>;
    };

    vcc_3v3: vcc-3v3 {
        compatible = "regulator-fixed";
        regulator-name = "vcc_3v3";
        regulator-always-on;
        regulator-boot-on;
        regulator-min-microvolt = <3300000>;
        regulator-max-microvolt = <3300000>;
    };
    vcc5v0_usb: vcc5v0-usb {
		compatible = "regulator-fixed";
		regulator-name = "vcc5v0_usb";
		regulator-min-microvolt = <5000000>;
		regulator-max-microvolt = <5000000>;
		enable-active-high;
		gpio = <&gpio0 RK_PA2 GPIO_ACTIVE_HIGH>;
		pinctrl-names = "default";
		pinctrl-0 = <&usb_pwren>;
	};
};
写法实际控制硬件?干什么用
regulator-fixed 无 gpio❌ 不控制就是个标签,告诉内核"这个电压存在"
regulator-fixed 有 gpio✅ 控制一个 GPIO打开/关闭某个电源芯片的使能脚
pwm-regulator✅ 控制 PWM通过 PWM 占空比调压(CPU 核心电压)

cc_1v8vcc_3v3——它们没有 gpio,不控制任何硬件,纯属为了让依赖的驱动能 probe 通过

vcc5v0_usb 它有一个 GPIO 脚去开 USB 口的 5V 供电。

节点&属性

chosen

cpu

&spi1 {
	/* 不使用硬件 SPI 引脚功能,引脚全部当 GPIO 用做 bitbang */
	status = "okay";
	/delete-property/ pinctrl-0;
	/delete-property/ pinctrl-names;
}

删除属性,意思是删除节点下的某一个属性。

这里删除了spi1下的pinctrl引脚属性,直接就导致spi1在系统下用不了了。不允许用户通过**/dev/spi1操作spi1**。但是可以gpio去模拟。

设备树底层

设备树驱动支持的属性

内核源码路径:

Documentation/devicetree/bindings

概念

没有设备树之前。

要想建立一个/dev/xxx文件,用来操作硬件设备需要以下步骤。

  1. 创建xxx_device.c和xxx_device.h来描述设备。比如某个寄存器。
  2. 创建驱动xxx_driver.c和xxx_driver.h操作描述的设备。比如寄存器写入和读取。
  3. 内核编译和引入。(需补充详细过程)

有设备树之后

要想建立一个/dev/xxx文件,用来操作硬件设备需要以下步骤。

  1. 创建设备树,用来描述设备,代替前面的第一步。
  2. 创建驱动xxx_driver.c和xxx_driver.h操作描述的设备。比如寄存器写入和读取。更多时候编译成模块。
  3. 内核编译和引入。(需补充详细过程)

对于自己编写的驱动,设备树要描述这个驱动,设备树和驱动代码必须做关联联动。

而对于常见的驱动,比如iic,spi等。前辈们已经集成在linux内核中,只需要在设备树下描述硬件信息。就能自己识别到这些驱动。设备树最主要的compatible属性如何匹配的,还不是很明白。

如何与内核联动

设备树

hello {
    compatible = "mycompany,hello";
};

驱动代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Lingma Assistant");
MODULE_DESCRIPTION("Hello World driver with Device Tree support");

// 匹配成功后调用的 probe 函数
static int hello_probe(struct platform_device *pdev)
{
    printk(KERN_INFO "Hello, World! Device matched: %s\n", pdev->name);
    // 这里可以获取设备树资源,如寄存器、GPIO 等
    return 0;
}

// 设备移除时调用
static int hello_remove(struct platform_device *pdev)
{
    printk(KERN_INFO "Goodbye, World! Device removed.\n");
    return 0;
}

// 定义支持的 compatible 列表
static const struct of_device_id hello_of_match[] = {
    { .compatible = "mycompany,hello" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, hello_of_match);

// 定义 platform 驱动结构体
static struct platform_driver hello_driver = {
    .probe    = hello_probe,
    .remove   = hello_remove,
    .driver   = {
        .name = "hello",
        .of_match_table = hello_of_match,
    },
};

module_platform_driver(hello_driver);

概念

设备树是一种硬件描述机制,它替代了内核中静态定义的 platform_device(以及其他总线设备描述)。平台总线(platform)负责管理 SoC 内部没有真实物理总线的设备。 传统 platform 驱动通过名称匹配设备,使用设备树后,主要依靠 compatible 属性匹配。匹配成功后调用 probe 函数,驱动在 probe 中通过 of.h API 直接读取设备树节点的硬件信息(属性可以有多个值)。设备树与 /dev 下的设备文件无直接关系,后者是驱动为提供用户空间接口而主动创建的。

编译工具路径

/root/rk-toots/kernel/scripts/dtc/dtc

可以修改环境变量

/etc/profile添加

export PATH=$PATH:/root/rk-toots/kernel/scripts/dtc/dtc
source /etc/profile

编译设备树1

dtc -I dts -O dtb -o xxx.dtb xxx.dts
  • dtc:工具
  • -I dts:输入的文件格式为dts
  • -O dtb:输出的文件格式为dtb
  • -o xxx.dtb:输出的文件名称
  • xxx.dts:输入的文件

编译设备树2

进入到kernel目录执行

make <dts文件名>.dtb

反编译

dtc -I dtb -O dts -o xxx.dts xxx.dtb
  • dtc:工具
  • -I dtb:输入的文件格式为dtb
  • -O dts:输出的文件格式为dts
  • -o xxx.dts:输出的文件名称
  • xxx.dtb:输入的文件

这里学习设备树用的是qemu模拟器环境,具体qemu具体使用方式参考:

embedded/qemu模拟环境.md

默认最原始的dts

/dts-v1/;

/ {
    model = "My Custom QEMU virt Board";
    compatible = "my-custom,virt-board", "linux,dummy-virt";
    interrupt-parent = <&gic>;
    #address-cells = <2>;
    #size-cells = <2>;

    /*
     * chosen: 传递给内核的参数
     * stdout-path 告诉内核控制台用哪个串口
     */
    chosen {
        bootargs = "console=ttyAMA0,115200";
        stdout-path = "/pl011@9000000";
    };

    /* 别名: 方便 U-Boot 和内核查找设备 */
    aliases {
        serial0 = &uart0;
    };

    /* ========== CPU ========== */
    cpus {
        #address-cells = <1>;
        #size-cells    = <0>;

        cpu@0 {
            compatible = "arm,cortex-a7";
            device_type = "cpu";
            reg = <0>;
            enable-method = "psci";
        };
    };

    /* ========== PSCI (电源管理) ========== */
    psci {
        compatible = "arm,psci-0.2", "arm,psci";
        method = "hvc";
        cpu_on   = <0x84000003>;
        cpu_off  = <0x84000002>;
        cpu_suspend = <0x84000001>;
        migrate  = <0x84000005>;
    };

    /* ========== 内存 ========== */
    memory@40000000 {
        device_type = "memory";
        reg = <0x0 0x40000000 0x0 0x20000000>;
    };

    /* ========== 定时器 ========== */
    timer {
        compatible = "arm,armv7-timer";
        interrupts = <1 13 0x104>,   /* PPI 13, 安全 */
                     <1 14 0x104>,   /* PPI 14, 非安全 */
                     <1 11 0x104>,   /* PPI 11, 虚拟 */
                     <1 10 0x104>;   /* PPI 10, Hypervisor */
        always-on;
    };

    /* ========== PL011 UART (控制台) ========== */
    uart0: pl011@9000000 {
        compatible = "arm,pl011", "arm,primecell";
        reg = <0x0 0x09000000 0x0 0x1000>;
        interrupts = <0 1 4>;        /* SPI 1, 高电平触发 */
        clocks = <&pclk &pclk>;
        clock-names = "uartclk", "apb_pclk";
    };

    /* ========== GICv2 中断控制器 ========== */
    gic: intc@8000000 {
        compatible = "arm,cortex-a7-gic";
        #interrupt-cells = <3>;
        #address-cells = <2>;
        #size-cells = <2>;
        interrupt-controller;
        reg = <0x0 0x08000000 0x0 0x10000>,   /* GIC 分配器 */
              <0x0 0x08010000 0x0 0x10000>;    /* GIC CPU 接口 */
        ranges;

        v2m@8020000 {
            compatible = "arm,gic-v2m-frame";
            msi-controller;
            reg = <0x0 0x08020000 0x0 0x1000>;
        };
    };

    /* ========== APB PCLK (24MHz 固定时钟) ========== */
    pclk: apb-pclk {
        compatible = "fixed-clock";
        #clock-cells = <0>;
        clock-frequency = <24000000>;
        clock-output-names = "clk24mhz";
    };

    /* ========== 平台总线 ========== */
    platform@c000000 {
        compatible = "qemu,platform", "simple-bus";
        ranges = <0 0 0x0c000000 0x2000000>;
        #address-cells = <1>;
        #size-cells = <1>;
        interrupt-parent = <&gic>;
    };
};

所有设备树的节点都会放到

/sys/firmware/devicetree/base
1

004. Kobject&kset 设备模型框架

# ARM 交叉编译内核模块
# 用法: make                     (编译)
#       make clean               (清理)
#       make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
#          -C /path/to/kernel M=$(PWD) modules

KERNELDIR ?= ../../../kernel

PWD := $(shell pwd)

obj-m := 001_kobject.o

all:
	$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
		-C $(KERNELDIR) M=$(PWD) modules
	@# 清理中间文件,只保留源文件 .c、模块 .ko 和 Makefile
	@find $(PWD) -maxdepth 1 -type f ! -name '*.ko' ! -name 'Makefile' ! \( -name '*.c' ! -name '*.mod.c' \) -delete 2>/dev/null || true
	@rm -rf $(PWD)/.tmp_versions 2>/dev/null || true

clean:
	$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
		-C $(KERNELDIR) M=$(PWD) clean

kobject即kernel object,内核对象。

kobject在内核中是一个结构体

在/sys目录下全是kobject

struct kobject {
	const char		*name;
	struct list_head	entry;
	struct kobject		*parent;
	struct kset		*kset;
	struct kobj_type	*ktype;
	struct kernfs_node	*sd; /* sysfs directory entry */
	struct kref		kref;
#ifdef CONFIG_DEBUG_KOBJECT_RELEASE
	struct delayed_work	release;
#endif
	unsigned int state_initialized:1;
	unsigned int state_in_sysfs:1;
	unsigned int state_add_uevent_sent:1;
	unsigned int state_remove_uevent_sent:1;
	unsigned int uevent_suppress:1;
};

设备,驱动,总线,类

  • 设备:struct device
  • 驱动:struct device_driver
  • 类:struct class ,会在/sys/class/xxx_class
  • 总线:struct bus
  • 编译使用
insmod 001_kobject.ko #加载驱动,必须使用root用户
dmesg | tail #查看驱动加载情况
rmmod 001_kobject  #卸载驱动,必须使用root用户
dmesg | tail #查看驱动卸载情况
  • 查看挂载情况
lsmod
ls /sys

代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/kobject.h>

struct kobject *o1;
struct kobject *o2;

struct kobject *o3;
struct kobj_type kt;
// 模块加载时调用的函数
static int my_kobject_init(void)
{
    int ret;
    o1 = kobject_create_and_add("myobject01",NULL);
    o2 = kobject_create_and_add("myobject02",o1);
    //创建方式二
    o3 = kzalloc(sizeof(struct kobject),GFP_KERNEL);
    ret=kobject_init_and_add(o3,&kt,NULL,"%s","myobject03");

    return 0; // 返回 0 表示加载成功
}

// 模块卸载时调用的函数
static void my_kobject_exit(void)
{
    kobject_put(o1);
    kobject_put(o2);
    kobject_put(o3);
    printk("Goodbye, World! Driver unloaded.\n");
}

// 注册模块的入口和出口函数
module_init(my_kobject_init);
module_exit(my_kobject_exit);

// 模块许可证声明,必须是 GPL 兼容的许可证才能加载到内核中
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("wyl");
// 模块描述
MODULE_DESCRIPTION("kobject");

代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/kobject.h>

struct kobject *o1;
struct kobject *o2;

struct kset *ks;
struct kobj_type kt;
// 模块加载时调用的函数
static int my_kobject_init(void)
{
    int ret;
    ks = kset_create_and_add("mykset",NULL,NULL);

    o1 = kzalloc(sizeof(struct kobject),GFP_KERNEL);
    o1->kset = ks;
    ret = kobject_init_and_add(o1,&kt,NULL,"%s","mykobjcet01");

    o2 = kzalloc(sizeof(struct kobject),GFP_KERNEL);
    o2->kset = ks;
    ret = kobject_init_and_add(o2,&kt,NULL,"%s","mykobjcet02");
    return 0; // 返回 0 表示加载成功
}

// 模块卸载时调用的函数
static void my_kobject_exit(void)
{
    kobject_put(o1);
    kobject_put(o2);
    kset_put(ks);
    printk("Goodbye, World! Driver unloaded.\n");
}

// 注册模块的入口和出口函数
module_init(my_kobject_init);
module_exit(my_kobject_exit);

// 模块许可证声明,必须是 GPL 兼容的许可证才能加载到内核中
MODULE_LICENSE("GPL");
// 模块作者信息
MODULE_AUTHOR("wyl");
// 模块描述
MODULE_DESCRIPTION("kobject");

005. Mdev udev

什么是热插拔

带电插拔,不用关闭系统。

热插拔当内核发生某种热插拔事件时,内核会调用用户空间的程序实现交互。比如内核发生TF插入,调用用户空间程序,用户空间程序自动挂载等

机制

devfsudevmdevdevfs基本不用了。mdevudev简化版本。

udev是基于netlink机制实现的。通过监听内核发送的uevent来执行相应的热插拔操作。mdev是基于uevent_helper机制,内核产生的uevent会调用uevent_helper所指向的用户程序,mdev执行热插拔动作。

特性Netlink (NETLINK_KOBJECT_UEVENT)uevent_helper
通信方式基于套接字(Socket)的异步消息通信内核直接执行用户空间程序
实现方式内核广播消息,用户态进程监听接收内核为每个事件 fork() 一个新进程执行助手程序
效率(消息传递,无进程创建开销)极低(每次事件都创建进程,开销巨大)
扩展性,支持多播,可供多个程序同时监听,一次只能指定一个助手程序
当前状态主流、推荐已废弃,现代系统默认不启用
典型使用者udevsystemd-udevd 等现代设备管理工具早期的 hotplug 脚本
  • 编译使用
insmod 001_helloworld.ko #加载驱动,必须使用root用户
dmesg | tail #查看驱动加载情况
rmmod 001_helloworld  #卸载驱动,必须使用root用户
dmesg | tail #查看驱动卸载情况
  • 查看挂载情况
lsmod

测试脚本

  • 罗列已有节点:

    ls /proc/device-tree/
    
  • 查看设备树的model

    cat /proc/device-tree/model
    

Linux驱动有多种实现方式

.ko文件insmod

  • 直接在运行中的系统执行insmod安装模块驱动
  • 放在文件系统,开机自启脚本执行insmod
  • 标准位置自动加载:/lib/modules/$(uname -r)/
  • 比如001-字符设备驱动 以及 002-平台总线驱动 下的驱动

linux目录下xxx.c + Kconfig + Makefile + DTS

内核直接加载模块

  1. 编译一堆自己的.ko文件,打包进文件系统里面。
  2. 由系统启动脚本insmod
  3. 匹配设备树/总线设备!
  4. 匹配成功才会probe()

uboot中初始化

基本不太属于linux驱动范畴,虽说用的是差不多的概念,但是不在linux驱动中。

完整关联表

文件/命令看什么和 DT 的关系
cat /proc/device-tree/model板子型号DT 根节点 model
ls /sys/firmware/devicetree/base/DT 所有节点原始 DT 数据
cat /sys/bus/platform/devices/xxx/uevent设备 OF 属性DT 节点的内核导出
cat /proc/devices所有已注册驱动的主设备号驱动调 register_chrdev() 产生
ls -l /dev/实际可用的设备文件主:次 = devtmpfs 根据驱动注册创建
cat /proc/interrupts中断号和绑定的驱动DT 中 interrupts 属性
cat /proc/iomem物理地址映射DT 中 reg 属性
cat /sys/class/*/*/dev主:次 设备号驱动注册时设定的
lsmod已加载的内核模块模块加载时匹配 DT compatible
`dmesggrep -i ofdt

Usb控制器

EHCI,OHCI,DWC3/xHCI

Usb红外摄像头

海康红外摄像头

HM-TM30

初始化+sdk路径

static USB_DEVICE_INFO *dev_list = NULL;   /* USB 枚举结果列表 */
static int dev_count = 0;                  /* 检测到的 USB 设备数 */
/*
 * 将工作目录切到可执行文件所在目录。
 * HCUsbSDK 通过 dlopen 加载同目录下的 .so 文件,必须运行在 .so 所在目录。
 */
static void setup_chdir(void)
{
    char exe[PATH_MAX], *p;
    ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
    if (n <= 0) return;
    exe[n] = 0;
    p = strrchr(exe, '/');
    if (!p) return;
    *p = 0;
    if (access(exe, F_OK) == 0) (void)chdir(exe);
}
int camera_init(void)
{
    LONG login_id; /
    USB_USER_LOGIN_INFO login_info = {0}; //登录配置结构体
    USB_DEVICE_REG_RES reg_result = {0}; //注册成功结构体

    setup_chdir(); //设置路径
    if (!USB_Init()) return -1; //usb初始化,摄像头会切换到usb模式

    dev_count = USB_GetDeviceCount(); //获取连接的usb摄像头数量
    if (dev_count <= 0) { USB_Cleanup(); return -1; }
    printf("Devices: %d\n", dev_count);

    dev_list = malloc(dev_count * sizeof(USB_DEVICE_INFO)); //为每个设备分配内存
    USB_EnumDevices(dev_count, dev_list);

    login_info.dwSize = sizeof(login_info); //配置登录信息
    login_info.dwTimeout = 5000;
    login_info.dwVID = dev_list[0].dwVID;
    login_info.dwPID = dev_list[0].dwPID;
    login_info.byLoginMode = 1;
    memcpy(login_info.szSerialNumber, dev_list[0].szSerialNumber, sizeof(login_info.szSerialNumber));
    memcpy(login_info.szUserName, "admin", 5);
    memcpy(login_info.szPassword, "12345", 5);

    reg_result.dwSize = sizeof(reg_result);
    login_id = USB_Login(&login_info, &reg_result); //登录测试
    if (login_id < 0) { printf("Login fail\n"); camera_shutdown(-1, -1); return -1; }
    printf("Login OK login_id=%ld\n", (long)uid);
    return (int)login_id;
}

配置

int camera_configure(LONG uid)
{
    USB_COMMON_COND channel_cond = {0};
    USB_CONFIG_INPUT_INFO input_info = {0};
    USB_CONFIG_OUTPUT_INFO output_info = {0};

    channel_cond.dwSize = sizeof(channel_cond);
    channel_cond.byChannelID = USB_CHANNEL_IR;

    /* ---- 配置测温流(coding type 8 = 测温+YUV) ---- */
    USB_THERMAL_STREAM_PARAM thermal_param = {0};
    thermal_param.dwSize = sizeof(thermal_param);
    thermal_param.byVideoCodingType = 8; //模式
    thermal_param.dwWidth  = 4;    /* 根据 YUV_MODE 选择传输帧宽 */
    thermal_param.dwHeight = 5188;    /* 根据 YUV_MODE 选择传输帧高 */
    thermal_param.dwFrameRate = 25;  //帧数
    input_info.lpCondBuffer = &channel_cond;
    input_info.dwCondBufferSize = sizeof(channel_cond);
    input_info.lpInBuffer = &thermal_param;
    input_info.dwInBufferSize = sizeof(thermal_param);
    if (!USB_SetDeviceConfig(uid, USB_SET_THERMAL_STREAM_PARAM, &input_info, &output_info))
        { printf("Set thermal param fail\n"); return 0; }

    /* ---- 配置视频格式为 YUY2 ---- */
    USB_VIDEO_PARAM video_param = {0};
    video_param.dwVideoFormat = USB_STREAM_YUY2;
    video_param.dwWidth  = 4;
    video_param.dwHeight = 5188;
    video_param.dwFramerate = 25;
    memset(&input_info, 0, sizeof(input_info));
    memset(&output_info, 0, sizeof(output_info));
    input_info.lpCondBuffer = &channel_cond;
    input_info.dwCondBufferSize = sizeof(channel_cond);
    input_info.lpInBuffer = &video_param;
    input_info.dwInBufferSize = sizeof(video_param);
    if (!USB_SetDeviceConfig(login_id, USB_SET_VIDEO_PARAM, &input_info, &output_info))
        { printf("Set video param fail\n"); return 0; }
    return 1;
}

回调开启

/* 完整帧数据(协议头 + 测温区 + YUY2,按 YUV_MODE 定长) */
static uint8_t frame_data[HEADER_BYTES + TEMP_BYTES + YUV_BYTES];
static pthread_mutex_t frame_mutex = PTHREAD_MUTEX_INITIALIZER;  /* 保护 frame_data/frame_ready/ */
static volatile int frame_ready = 0;   /* 新帧到达标志:on_frame 置 1,camera_get_frame 消费后置 0 */
void CALLBACK on_frame(LONG handle, USB_FRAME_INFO *frame, void *user)
{
    (void)handle;
    (void)user;
    if (!frame || !frame->pBuf) return;
    if (frame->dwBufSize < YUV_OFFSET + YUV_BYTES) return;

    pthread_mutex_lock(&frame_mutex);
    memcpy(frame_data, frame->pBuf, YUV_OFFSET + YUV_BYTES);
    frame_ready = 1;
    pthread_mutex_unlock(&frame_mutex);
}
int camera_start_stream(LONG uid)
{
    USB_STREAM_CALLBACK_PARAM callback_param = {0};
    callback_param.dwSize = sizeof(callback_param);
    callback_param.dwStreamType = USB_STREAM_YUY2;
    callback_param.funcStreamCallBack = on_frame;
    LONG stream_handle = USB_StartStreamCallback(uid, &callback_param);
    if (stream_handle < 0) { printf("Start stream fail\n"); return -1; }
    printf("Stream started\n");
    return (int)stream_handle;
}

其他地方获取回调

int camera_get_frame(uint8_t *buf)
{
    int ready;
    pthread_mutex_lock(&frame_mutex);
    ready = frame_ready;
    if (ready) {
        memcpy(buf, frame_data + YUV_OFFSET, YUV_BYTES);
        frame_ready = 0;
    }
    pthread_mutex_unlock(&frame_mutex);
    return ready;
}

结束

void camera_shutdown(LONG uid, int stream_handle)
{
    if (stream_handle >= 0) USB_StopChannel(uid, (DWORD)stream_handle);
    if (uid >= 0) USB_Logout(uid);
    free(dev_list);
    dev_list = NULL;
    USB_Cleanup();
}
  • 停止流
  • 修改配置
  • 启动流
#红外
thermal_param.dwWidth  = g_usb_w;
thermal_param.dwHeight = g_usb_h;
#yuv
video_param.dwWidth  = g_usb_w;
video_param.dwHeight = g_usb_h;

开启流的时候需要配置红外的dwWidth,dwHeight,关闭之后修改配置,然后再打开就好

/*
 * camera_config.c — 热成像摄像头参数配置接口实现
 *
 * 参考官方示例 ThermalDevice.cpp 封装的全部红外配置项。
 * 配置操作统一走 USB 通道 1(官方约定),码流通道 2 只用于视频流。
 * 图像增强使用 V20→V1 自动回退;其余均使用 V1 命令。
 */

#include "camera_config.h"
#include <stdio.h>
#include <string.h>

/* ======================================================================== *
 *  内部辅助函数                                                              *
 * ======================================================================== */

/*
 * 通用 GET:通过 USB_GetDeviceConfig 读取设备参数。
 * 通道固定为 1(控制通道),与流数据通道(2)分离。
 */
static int get_config(LONG uid, DWORD cmd, void *out, DWORD out_size)
{
    USB_COMMON_COND         cond = {0};
    USB_CONFIG_INPUT_INFO   ii   = {0};
    USB_CONFIG_OUTPUT_INFO  oo   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;
    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = out;
    ii.dwInBufferSize   = out_size;
    oo.lpOutBuffer      = out;
    oo.dwOutBufferSize  = out_size;

    if (!USB_GetDeviceConfig(uid, cmd, &ii, &oo)) {
        printf("[camera_config] GET fail cmd=0x%lx (%lu)\n",
               (unsigned long)cmd, (unsigned long)cmd);
        return 0;
    }
    return 1;
}

/*
 * 通用 SET:通过 USB_SetDeviceConfig 写入设备参数。
 */
static int set_config(LONG uid, DWORD cmd, void *in, DWORD in_size)
{
    USB_COMMON_COND         cond = {0};
    USB_CONFIG_INPUT_INFO   ii   = {0};
    USB_CONFIG_OUTPUT_INFO  oo   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;
    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = in;
    ii.dwInBufferSize   = in_size;

    if (!USB_SetDeviceConfig(uid, cmd, &ii, &oo)) {
        printf("[camera_config] SET fail cmd=0x%lx (%lu)\n",
               (unsigned long)cmd, (unsigned long)cmd);
        return 0;
    }
    return 1;
}

/*
 * 通用 CONTROL:通过 USB_Control 发送无数据/简单参数的控制命令。
 */
static int control_cmd(LONG uid, DWORD cmd)
{
    USB_COMMON_COND         cond = {0};
    USB_CONTROL_INPUT_INFO  ci   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;
    ci.lpCondBuffer     = &cond;
    ci.dwCondBufferSize = sizeof(cond);

    if (!USB_Control(uid, cmd, &ci)) {
        printf("[camera_config] CTRL fail cmd=0x%lx (%lu)\n",
               (unsigned long)cmd, (unsigned long)cmd);
        return 0;
    }
    return 1;
}

/* ======================================================================== *
 *  1.  系统管理                                                             *
 * ======================================================================== */

int camera_get_device_info(LONG uid, USB_SYSTEM_DEVICE_INFO *info)
{
    memset(info, 0, sizeof(*info));
    info->dwSize = sizeof(*info);
    return get_config(uid, USB_GET_SYSTEM_DEVICE_INFO, info, sizeof(*info));
}

int camera_set_reboot(LONG uid)
{
    return control_cmd(uid, USB_SET_SYSTEM_REBOOT);
}

int camera_set_factory_reset(LONG uid)
{
    return control_cmd(uid, USB_SET_SYSTEM_RESET);
}

int camera_set_background_correct(LONG uid)
{
    return control_cmd(uid, USB_SET_IMAGE_BACKGROUND_CORRECT);
}

int camera_set_manual_correct(LONG uid)
{
    return control_cmd(uid, USB_SET_IMAGE_MANUAL_CORRECT);
}

int camera_get_hardware_server(LONG uid, USB_SYSTEM_HARDWARE_SERVER *hs)
{
    memset(hs, 0, sizeof(*hs));
    hs->dwSize = sizeof(*hs);
    return get_config(uid, USB_GET_SYSTEM_HARDWARE_SERVER, hs, sizeof(*hs));
}

int camera_set_hardware_server(LONG uid, const USB_SYSTEM_HARDWARE_SERVER *hs)
{
    return set_config(uid, USB_SET_SYSTEM_HARDWARE_SERVER,
                      (void *)hs, sizeof(*hs));
}

int camera_get_local_time(LONG uid, USB_SYSTEM_LOCALTIME *lt)
{
    memset(lt, 0, sizeof(*lt));
    lt->dwSize = sizeof(*lt);
    return get_config(uid, USB_GET_SYSTEM_LOCALTIME, lt, sizeof(*lt));
}

int camera_set_local_time(LONG uid, const USB_SYSTEM_LOCALTIME *lt)
{
    return set_config(uid, USB_SET_SYSTEM_LOCALTIME, (void *)lt, sizeof(*lt));
}

/* ======================================================================== *
 *  2.  图像增强 Enhancement(V20 → V1 自动回退)                             *
 * ======================================================================== */

int camera_get_enhancement(LONG uid, USB_IMAGE_ENHANCEMENT *enh)
{
    USB_COMMON_COND         cond = {0};
    USB_CONFIG_INPUT_INFO   ii   = {0};
    USB_CONFIG_OUTPUT_INFO  oo   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;
    enh->dwSize = sizeof(*enh);

    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = enh;
    ii.dwInBufferSize   = sizeof(*enh);
    oo.lpOutBuffer      = enh;
    oo.dwOutBufferSize  = sizeof(*enh);

    /* 先试 V20 */
    if (USB_GetDeviceConfig(uid, USB_GET_IMAGE_ENHANCEMENT_V20, &ii, &oo))
        return 1;

    /* 回退 V1 */
    memset(&ii, 0, sizeof(ii));
    memset(&oo, 0, sizeof(oo));
    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = enh;
    ii.dwInBufferSize   = sizeof(*enh);
    oo.lpOutBuffer      = enh;
    oo.dwOutBufferSize  = sizeof(*enh);

    if (!USB_GetDeviceConfig(uid, USB_GET_IMAGE_ENHANCEMENT, &ii, &oo)) {
        printf("[camera_config] get_enhancement: V20 和 V1 都失败\n");
        return 0;
    }
    return 1;
}

int camera_set_enhancement(LONG uid, const USB_IMAGE_ENHANCEMENT *enh)
{
    USB_COMMON_COND         cond = {0};
    USB_CONFIG_INPUT_INFO   ii   = {0};
    USB_CONFIG_OUTPUT_INFO  oo   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;
    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = (void *)enh;
    ii.dwInBufferSize   = sizeof(*enh);

    /* 先试 V20 */
    if (USB_SetDeviceConfig(uid, USB_SET_IMAGE_ENHANCEMENT_V20, &ii, &oo))
        return 1;

    /* 回退 V1 */
    memset(&ii, 0, sizeof(ii));
    memset(&oo, 0, sizeof(oo));
    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = (void *)enh;
    ii.dwInBufferSize   = sizeof(*enh);

    if (!USB_SetDeviceConfig(uid, USB_SET_IMAGE_ENHANCEMENT, &ii, &oo)) {
        printf("[camera_config] set_enhancement: V20 和 V1 都失败\n");
        return 0;
    }
    return 1;
}

/* ---- 2a. 伪彩色 ---- */

int camera_get_palette(LONG uid, int *mode)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *mode = enh.byPaletteMode;
    return 1;
}

int camera_set_palette(LONG uid, int mode)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byPaletteMode = (BYTE)mode;
    return camera_set_enhancement(uid, &enh);
}

/* ---- 2b. AGC 模式 ---- */

int camera_get_agc_mode(LONG uid, int *mode)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *mode = enh.byIspAgcMode;
    return 1;
}

int camera_set_agc_mode(LONG uid, int mode)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byIspAgcMode = (BYTE)mode;
    return camera_set_enhancement(uid, &enh);
}

/* ---- 2c. DDE ---- */

int camera_get_dde_enabled(LONG uid, int *enabled)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *enabled = enh.byLSEDetailEnabled;
    return 1;
}

int camera_set_dde_enabled(LONG uid, int enabled)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byLSEDetailEnabled = enabled ? 1 : 0;
    return camera_set_enhancement(uid, &enh);
}

int camera_get_dde_level(LONG uid, DWORD *level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *level = enh.dwLSEDetailLevel;
    return 1;
}

int camera_set_dde_level(LONG uid, DWORD level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.dwLSEDetailLevel = level;
    return camera_set_enhancement(uid, &enh);
}

/* ---- 2d. 降噪 ---- */

int camera_get_noise_reduce_mode(LONG uid, int *mode)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *mode = enh.byNoiseReduceMode;
    return 1;
}

int camera_set_noise_reduce_mode(LONG uid, int mode)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byNoiseReduceMode = (BYTE)mode;
    return camera_set_enhancement(uid, &enh);
}

int camera_get_general_denoise(LONG uid, DWORD *level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *level = enh.dwGeneralLevel;
    return 1;
}

int camera_set_general_denoise(LONG uid, DWORD level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byNoiseReduceMode = 1;
    enh.dwGeneralLevel = level;
    return camera_set_enhancement(uid, &enh);
}

int camera_get_spatial_denoise(LONG uid, DWORD *level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *level = enh.dwFrameNoiseReduceLevel;
    return 1;
}

int camera_set_spatial_denoise(LONG uid, DWORD level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byNoiseReduceMode = 2;
    enh.dwFrameNoiseReduceLevel = level;
    return camera_set_enhancement(uid, &enh);
}

int camera_get_temporal_denoise(LONG uid, DWORD *level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *level = enh.dwInterFrameNoiseReduceLevel;
    return 1;
}

int camera_set_temporal_denoise(LONG uid, DWORD level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byNoiseReduceMode = 2;
    enh.dwInterFrameNoiseReduceLevel = level;
    return camera_set_enhancement(uid, &enh);
}

/* ---- 2e. 勾边 ---- */

int camera_get_hook_edge(LONG uid, int *enabled, int *level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    *enabled = enh.byHookEdgeMode;
    *level   = enh.byHookEdgeLevel;
    return 1;
}

int camera_set_hook_edge(LONG uid, int enabled, int level)
{
    USB_IMAGE_ENHANCEMENT enh = {0};
    if (!camera_get_enhancement(uid, &enh)) return 0;
    enh.byHookEdgeMode  = enabled ? 1 : 0;
    enh.byHookEdgeLevel = (BYTE)level;
    return camera_set_enhancement(uid, &enh);
}

/* ======================================================================== *
 *  3.  视频调整                                                             *
 * ======================================================================== */

int camera_get_video_adjust(LONG uid, USB_IMAGE_VIDEO_ADJUST *va)
{
    memset(va, 0, sizeof(*va));
    va->dwSize = sizeof(*va);
    return get_config(uid, USB_GET_IMAGE_VIDEO_ADJUST_CAMERA, va, sizeof(*va));
}

int camera_set_video_adjust(LONG uid, const USB_IMAGE_VIDEO_ADJUST *va)
{
    return set_config(uid, USB_SET_IMAGE_VIDEO_ADJUST_CAMERA,
                      (void *)va, sizeof(*va));
}

int camera_get_mirror_mode(LONG uid, int *mode)
{
    USB_IMAGE_VIDEO_ADJUST va;
    if (!camera_get_video_adjust(uid, &va)) return 0;
    *mode = va.byImageFlipStyle;
    return 1;
}

int camera_set_mirror_mode(LONG uid, int mode)
{
    USB_IMAGE_VIDEO_ADJUST va;
    if (!camera_get_video_adjust(uid, &va)) return 0;
    va.byImageFlipStyle = (BYTE)mode;
    return camera_set_video_adjust(uid, &va);
}

int camera_get_digital_zoom(LONG uid, int *zoom)
{
    USB_IMAGE_VIDEO_ADJUST va;
    if (!camera_get_video_adjust(uid, &va)) return 0;
    *zoom = va.byDigitalZoom;
    return 1;
}

int camera_set_digital_zoom(LONG uid, int zoom)
{
    USB_IMAGE_VIDEO_ADJUST va;
    if (!camera_get_video_adjust(uid, &va)) return 0;
    va.byDigitalZoom = (BYTE)zoom;
    return camera_set_video_adjust(uid, &va);
}

/* ======================================================================== *
 *  4.  亮度 / 对比度                                                        *
 * ======================================================================== */

int camera_get_brightness(LONG uid, DWORD *value)
{
    USB_IMAGE_BRIGHTNESS b = {0};
    b.dwSize = sizeof(b);
    if (!get_config(uid, USB_GET_IMAGE_BRIGHTNESS, &b, sizeof(b)))
        return 0;
    *value = b.dwBrightness;
    return 1;
}

int camera_set_brightness(LONG uid, DWORD value)
{
    USB_IMAGE_BRIGHTNESS b = {0};
    b.dwSize = sizeof(b);
    b.dwBrightness = value;
    return set_config(uid, USB_SET_IMAGE_BRIGHTNESS, &b, sizeof(b));
}

int camera_get_contrast(LONG uid, DWORD *value)
{
    USB_IMAGE_CONTRAST c = {0};
    c.dwSize = sizeof(c);
    if (!get_config(uid, USB_GET_IMAGE_CONTRAST, &c, sizeof(c)))
        return 0;
    *value = c.dwContrast;
    return 1;
}

int camera_set_contrast(LONG uid, DWORD value)
{
    USB_IMAGE_CONTRAST c = {0};
    c.dwSize = sizeof(c);
    c.dwContrast = value;
    return set_config(uid, USB_SET_IMAGE_CONTRAST, &c, sizeof(c));
}

/* ======================================================================== *
 *  5.  测温基本参数                                                          *
 * ======================================================================== */

int camera_get_thermometry(LONG uid, USB_THERMOMETRY_BASIC_PARAM *tp)
{
    memset(tp, 0, sizeof(*tp));
    tp->dwSize = sizeof(*tp);
    return get_config(uid, USB_GET_THERMOMETRY_BASIC_PARAM, tp, sizeof(*tp));
}

int camera_set_thermometry(LONG uid, const USB_THERMOMETRY_BASIC_PARAM *tp)
{
    return set_config(uid, USB_SET_THERMOMETRY_BASIC_PARAM,
                      (void *)tp, sizeof(*tp));
}

/* ---- 5a. 发射率 ---- */

int camera_get_emissivity(LONG uid, DWORD *value_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *value_x100 = tp.dwEmissivity;
    return 1;
}

int camera_set_emissivity(LONG uid, DWORD value_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwEmissivity = value_x100;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5b. 测温距离 ---- */

int camera_get_distance(LONG uid, DWORD *distance_cm)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *distance_cm = tp.dwDistance;
    return 1;
}

int camera_set_distance(LONG uid, DWORD distance_cm)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwDistance = distance_cm;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_distance_unit(LONG uid, int *unit)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *unit = tp.byDistanceUnit;
    return 1;
}

int camera_set_distance_unit(LONG uid, int unit)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byDistanceUnit = (BYTE)unit;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5c. 温度单位 ---- */

int camera_get_temp_unit(LONG uid, int *unit)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *unit = tp.byTemperatureUnit;
    return 1;
}

int camera_set_temp_unit(LONG uid, int unit)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byTemperatureUnit = (BYTE)unit;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5d. 测温范围 ---- */

int camera_get_temp_range(LONG uid, int *range)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *range = tp.byTemperatureRange;
    return 1;
}

int camera_set_temp_range(LONG uid, int range)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byTemperatureRange = (BYTE)range;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_temp_range_auto_enabled(LONG uid, int *enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *enabled = tp.byTemperatureRangeAutoChangedEnabled;
    return 1;
}

int camera_set_temp_range_auto_enabled(LONG uid, int enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byTemperatureRangeAutoChangedEnabled = enabled ? 1 : 0;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5e. 标定系数 ---- */

int camera_get_calibration_coeff(LONG uid, DWORD *value_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *value_x100 = tp.dwCalibrationCoefficient;
    return 1;
}

int camera_set_calibration_coeff(LONG uid, DWORD value_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwCalibrationCoefficient = value_x100;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_calibration_coeff_enabled(LONG uid, int *enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *enabled = tp.byCalibrationCoefficientEnabled;
    return 1;
}

int camera_set_calibration_coeff_enabled(LONG uid, int enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byCalibrationCoefficientEnabled = enabled ? 1 : 0;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5f. 反射温度 ---- */

int camera_get_reflective_temp(LONG uid, DWORD *temp_x10)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *temp_x10 = tp.dwReflectiveTemperature;
    return 1;
}

int camera_set_reflective_temp(LONG uid, DWORD temp_x10)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwReflectiveTemperature = temp_x10;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_reflective_enabled(LONG uid, int *enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *enabled = tp.byReflectiveEnable;
    return 1;
}

int camera_set_reflective_enabled(LONG uid, int enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byReflectiveEnable = enabled ? 1 : 0;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5g. 环境温度 ---- */

int camera_get_env_temp(LONG uid, int *enabled, DWORD *temp)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *enabled = tp.byEnviromentTemperatureEnable;
    *temp    = tp.dwEnviromentTemperature;
    return 1;
}

int camera_set_env_temp(LONG uid, int enabled, DWORD temp)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byEnviromentTemperatureEnable = (BYTE)enabled;
    tp.dwEnviromentTemperature       = temp;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5h. 报警温度 ---- */

int camera_get_alert_temp(LONG uid, DWORD *temp_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *temp_x100 = tp.dwAlert;
    return 1;
}

int camera_set_alert_temp(LONG uid, DWORD temp_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwAlert = temp_x100;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_alarm_temp(LONG uid, DWORD *temp_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *temp_x100 = tp.dwAlarm;
    return 1;
}

int camera_set_alarm_temp(LONG uid, DWORD temp_x100)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwAlarm = temp_x100;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5i. 显示元素 ---- */

int camera_get_display_flags(LONG uid, int *max_en, int *min_en,
                             int *avg_en, int *cen_en)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *max_en = tp.byDisplayMaxTemperatureEnabled;
    *min_en = tp.byDisplayMinTemperatureEnabled;
    *avg_en = tp.byDisplayAverageTemperatureEnabled;
    *cen_en = tp.byDisplayCenTempEnabled;
    return 1;
}

int camera_set_display_flags(LONG uid, int max_en, int min_en,
                             int avg_en, int cen_en)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byDisplayMaxTemperatureEnabled     = max_en ? 1 : 0;
    tp.byDisplayMinTemperatureEnabled     = min_en ? 1 : 0;
    tp.byDisplayAverageTemperatureEnabled = avg_en ? 1 : 0;
    tp.byDisplayCenTempEnabled            = cen_en ? 1 : 0;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5j. 全局开关 / 叠加 ---- */

int camera_get_thermometry_enabled(LONG uid, int *enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *enabled = tp.byEnabled;
    return 1;
}

int camera_set_thermometry_enabled(LONG uid, int enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byEnabled = enabled ? 1 : 0;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_thermometry_overlay(LONG uid, int *enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *enabled = tp.byThermometryStreamOverlay;
    return 1;
}

int camera_set_thermometry_overlay(LONG uid, int enabled)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.byThermometryStreamOverlay = enabled ? 1 : 0;
    return camera_set_thermometry(uid, &tp);
}

/* ---- 5k. 外部光学 ---- */

int camera_get_external_optics_correction(LONG uid, DWORD *temp)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *temp = tp.dwExternalOpticsWindowCorrection;
    return 1;
}

int camera_set_external_optics_correction(LONG uid, DWORD temp)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwExternalOpticsWindowCorrection = temp;
    return camera_set_thermometry(uid, &tp);
}

int camera_get_atmospheric_humidity(LONG uid, DWORD *humidity)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    *humidity = tp.dwAtmosphericHumidity;
    return 1;
}

int camera_set_atmospheric_humidity(LONG uid, DWORD humidity)
{
    USB_THERMOMETRY_BASIC_PARAM tp;
    if (!camera_get_thermometry(uid, &tp)) return 0;
    tp.dwAtmosphericHumidity = humidity;
    return camera_set_thermometry(uid, &tp);
}

/* ======================================================================== *
 *  6.  测温模式                                                             *
 * ======================================================================== */

int camera_get_thermometry_mode(LONG uid, USB_THERMOMETRY_MODE *mode)
{
    memset(mode, 0, sizeof(*mode));
    mode->dwSize = sizeof(*mode);
    return get_config(uid, USB_GET_THERMOMETRY_MODE, mode, sizeof(*mode));
}

int camera_set_thermometry_mode(LONG uid, const USB_THERMOMETRY_MODE *mode)
{
    return set_config(uid, USB_SET_THERMOMETRY_MODE,
                      (void *)mode, sizeof(*mode));
}

/* ======================================================================== *
 *  7.  测温规则区域                                                          *
 * ======================================================================== */

int camera_get_thermometry_regions(LONG uid, USB_THERMOMETRY_REGIONS *regs)
{
    memset(regs, 0, sizeof(*regs));
    regs->dwSize = sizeof(*regs);
    return get_config(uid, USB_GET_THERMOMETRY_REGIONS, regs, sizeof(*regs));
}

int camera_set_thermometry_regions(LONG uid,
                                   const USB_THERMOMETRY_REGIONS *regs)
{
    return set_config(uid, USB_SET_THERMOMETRY_REGIONS,
                      (void *)regs, sizeof(*regs));
}

/* ======================================================================== *
 *  8.  测温修正                                                             *
 * ======================================================================== */

int camera_get_temperature_correct(LONG uid, USB_TEMPERATURE_CORRECT *tc)
{
    memset(tc, 0, sizeof(*tc));
    tc->dwSize = sizeof(*tc);
    return get_config(uid, USB_GET_TEMPERATURE_CORRECT, tc, sizeof(*tc));
}

int camera_set_temperature_correct(LONG uid,
                                   const USB_TEMPERATURE_CORRECT *tc)
{
    return set_config(uid, USB_SET_TEMPERATURE_CORRECT,
                      (void *)tc, sizeof(*tc));
}

/* ======================================================================== *
 *  9.  黑体参数                                                             *
 * ======================================================================== */

int camera_get_black_body(LONG uid, USB_BLACK_BODY *bb)
{
    memset(bb, 0, sizeof(*bb));
    bb->dwSize = sizeof(*bb);
    return get_config(uid, USB_GET_BLACK_BODY, bb, sizeof(*bb));
}

int camera_set_black_body(LONG uid, const USB_BLACK_BODY *bb)
{
    return set_config(uid, USB_SET_BLACK_BODY, (void *)bb, sizeof(*bb));
}

/* ======================================================================== *
 *  10. 体温补偿                                                             *
 * ======================================================================== */

int camera_get_bodytemp_compensation(LONG uid,
                                     USB_BODYTEMP_COMPENSATION *bc)
{
    memset(bc, 0, sizeof(*bc));
    bc->dwSize = sizeof(*bc);
    return get_config(uid, USB_GET_BODYTEMP_COMPENSATION,
                      bc, sizeof(*bc));
}

int camera_set_bodytemp_compensation(LONG uid,
                                     const USB_BODYTEMP_COMPENSATION *bc)
{
    return set_config(uid, USB_SET_BODYTEMP_COMPENSATION,
                      (void *)bc, sizeof(*bc));
}

/* ======================================================================== *
 *  11. 全屏测温参数                                                         *
 * ======================================================================== */

int camera_get_p2p_param(LONG uid, USB_P2P_PARAM *p2p)
{
    memset(p2p, 0, sizeof(*p2p));
    p2p->dwSize = sizeof(*p2p);
    return get_config(uid, USB_GET_P2P_PARAM, p2p, sizeof(*p2p));
}

int camera_set_p2p_param(LONG uid, const USB_P2P_PARAM *p2p)
{
    return set_config(uid, USB_SET_P2P_PARAM, (void *)p2p, sizeof(*p2p));
}

/* ======================================================================== *
 *  12. 热成像码流参数                                                       *
 * ======================================================================== */

int camera_get_thermal_stream_param(LONG uid, USB_THERMAL_STREAM_PARAM *sp)
{
    memset(sp, 0, sizeof(*sp));
    sp->dwSize = sizeof(*sp);
    return get_config(uid, USB_GET_THERMAL_STREAM_PARAM, sp, sizeof(*sp));
}

/* ======================================================================== *
 *  13. 算法版本                                                             *
 * ======================================================================== */

int camera_get_thermal_alg_version(LONG uid, USB_THERMAL_ALG_VERSION *ver)
{
    memset(ver, 0, sizeof(*ver));
    ver->dwSize = sizeof(*ver);
    return get_config(uid, USB_GET_THERMAL_ALG_VERSION, ver, sizeof(*ver));
}

/* ======================================================================== *
 *  14. 诊断信息导出                                                         *
 * ======================================================================== */

int camera_get_system_diagnosed_data(LONG uid,
                                     USB_SYSTEM_DIAGNOSED_DATA *dd)
{
    dd->dwSize = sizeof(*dd);
    return get_config(uid, USB_GET_SYSTEM_DIAGNOSED_DATA, dd, sizeof(*dd));
}

/* ======================================================================== *
 *  15. ROI 最高温搜索                                                       *
 * ======================================================================== */

int camera_get_roi_max_temperature_search(
        LONG uid,
        USB_ROI_MAX_TEMPERATURE_SEARCH *in,
        USB_ROI_MAX_TEMPERATURE_SEARCH_RESULT *out)
{
    USB_COMMON_COND         cond = {0};
    USB_CONFIG_INPUT_INFO   ii   = {0};
    USB_CONFIG_OUTPUT_INFO  oo   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;
    in->dwSize = sizeof(*in);

    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = in;
    ii.dwInBufferSize   = sizeof(*in);
    oo.lpOutBuffer      = out;
    oo.dwOutBufferSize  = sizeof(*out);

    if (!USB_GetDeviceConfig(uid, USB_GET_ROI_MAX_TEMPERATURE_SEARCH,
                             &ii, &oo)) {
        printf("[camera_config] get_roi_max_temperature_search 失败\n");
        return 0;
    }
    return 1;
}

/* ======================================================================== *
 *  16. 双光校准                                                             *
 * ======================================================================== */

int camera_post_double_lights_correct(
        LONG uid,
        const USB_DOUBLE_LIGHTS_CORRECT *in,
        USB_DOUBLE_LIGHTS_CORRECT_RESULT *out)
{
    USB_COMMON_COND         cond = {0};
    USB_CONFIG_INPUT_INFO   ii   = {0};
    USB_CONFIG_OUTPUT_INFO  oo   = {0};

    cond.dwSize = sizeof(cond);
    cond.byChannelID = 1;

    ii.lpCondBuffer     = &cond;
    ii.dwCondBufferSize = sizeof(cond);
    ii.lpInBuffer       = (void *)in;
    ii.dwInBufferSize   = sizeof(*in);
    oo.lpOutBuffer      = out;
    oo.dwOutBufferSize  = sizeof(*out);

    if (!USB_GetDeviceConfig(uid, USB_POST_DOUBLE_LIGHTS_CORRECT,
                             &ii, &oo)) {
        printf("[camera_config] post_double_lights_correct 失败\n");
        return 0;
    }
    return 1;
}

int camera_get_double_lights_correct_points_ctrl(
        LONG uid, USB_DOUBLE_LIGHTS_CORRECT_POINTS_CTRL *ctrl)
{
    memset(ctrl, 0, sizeof(*ctrl));
    ctrl->dwSize = sizeof(*ctrl);
    return get_config(uid, USB_GET_DOUBLE_LIGHTS_CORRECT_POINTS_CTRL,
                      ctrl, sizeof(*ctrl));
}

int camera_set_double_lights_correct_points_ctrl(
        LONG uid, const USB_DOUBLE_LIGHTS_CORRECT_POINTS_CTRL *ctrl)
{
    return set_config(uid, USB_SET_DOUBLE_LIGHTS_CORRECT_POINTS_CTRL,
                      (void *)ctrl, sizeof(*ctrl));
}

/* ======================================================================== *
 *  17. 测温标定文件                                                         *
 * ======================================================================== */

int camera_get_thermometry_calibration_file(
        LONG uid, USB_THERMOMETRY_CALIBRATION_FILE *cf)
{
    cf->dwSize = sizeof(*cf);
    return get_config(uid, USB_GET_THERMOMETRY_CALIBRATION_FILE,
                      cf, sizeof(*cf));
}

int camera_set_thermometry_calibration_file(
        LONG uid, const USB_THERMOMETRY_CALIBRATION_FILE *cf)
{
    return set_config(uid, USB_SET_THERMOMETRY_CALIBRATION_FILE,
                      (void *)cf, sizeof(*cf));
}

/* ======================================================================== *
 *  18. 命令状态                                                             *
 * ======================================================================== */

int camera_get_command_state(LONG uid, USB_COMMAND_STATE *state)
{
    memset(state, 0, sizeof(*state));
    state->dwSize = sizeof(*state);
    if (!USB_GetCommandState(uid, state)) {
        printf("[camera_config] get_command_state 失败\n");
        return 0;
    }
    return 1;
}
/*
 * camera_config.h — 热成像摄像头参数配置接口
 *
 * 基于 HCUsbSDK 封装,提供热成像设备的全部配置项。
 * 所有配置操作通过 USB 通道 1(官方 Hikvision demo 约定)发送。
 * 返回 1 成功 / 0 失败(失败时自动打印错误码)。
 *
 * 字段访问器采用 读-改-写 模式:先 GET 整个结构体,修改单个字段,再 SET 回设备。
 * 命名约定:camera_{get/set}_{参数名},无特殊声明时走通道 1、无额外数据。
 */

#ifndef CAMERA_CONFIG_H
#define CAMERA_CONFIG_H

#include "HCUsbSDK.h"

#ifdef __cplusplus
extern "C" {
#endif

/* ======================================================================== *
 *  1.  系统管理                                                             *
 * ======================================================================== */

int camera_get_device_info(LONG uid, USB_SYSTEM_DEVICE_INFO *info);
/*  读取设备信息(固件版本、硬件版本、设备型号、序列号等)。
 *  @param uid    登录句柄
 *  @param info   输出:设备信息结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_reboot(LONG uid);
/*  远程重启设备。
 *  @param uid    登录句柄
 *  @return       1 成功 / 0 失败                              */

int camera_set_factory_reset(LONG uid);
/*  恢复设备出厂设置。
 *  @param uid    登录句柄
 *  @return       1 成功 / 0 失败                              */

int camera_set_background_correct(LONG uid);
/*  一键背景校正(快门校正),消除热成像背景不均匀。
 *  @param uid    登录句柄
 *  @return       1 成功 / 0 失败                              */

int camera_set_manual_correct(LONG uid);
/*  一键手动校正(快门校正)。
 *  @param uid    登录句柄
 *  @return       1 成功 / 0 失败                              */

int camera_get_hardware_server(LONG uid, USB_SYSTEM_HARDWARE_SERVER *hs);
/*  读取硬件服务参数(USB 模式、设备初始化/运行状态)。
 *  @param uid    登录句柄
 *  @param hs     输出:硬件服务参数结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_hardware_server(LONG uid, const USB_SYSTEM_HARDWARE_SERVER *hs);
/*  设置硬件服务参数。
 *  @param uid    登录句柄
 *  @param hs     输入:硬件服务参数结构体
 *  @return       1 成功 / 0 失败                              */

int camera_get_local_time(LONG uid, USB_SYSTEM_LOCALTIME *lt);
/*  读取设备本地时间(年/月/日/时/分/秒/毫秒)。
 *  @param uid    登录句柄
 *  @param lt     输出:系统时间结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_local_time(LONG uid, const USB_SYSTEM_LOCALTIME *lt);
/*  校时——设置设备本地时间。
 *  @param uid    登录句柄
 *  @param lt     输入:期望的系统时间结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  2.  图像增强 Enhancement(V20 → V1 自动回退)                             *
 *      USB_IMAGE_ENHANCEMENT 结构体,先试 _V20 命令,失败回退 V1。           *
 * ======================================================================== */

int camera_get_enhancement(LONG uid, USB_IMAGE_ENHANCEMENT *enh);
/*  读取完整的图像增强参数(降噪/伪彩/DDE/勾边/宽动态等所有字段)。
 *  @param uid    登录句柄
 *  @param enh    输出:图像增强结构体(注意 enh->dwSize 必须预置 sizeof(*enh))
 *  @return       1 成功 / 0 失败                              */

int camera_set_enhancement(LONG uid, const USB_IMAGE_ENHANCEMENT *enh);
/*  写入完整的图像增强参数(读-改-写风格:先 get 再 put)。
 *  @param uid    登录句柄
 *  @param enh    输入:填充好的图像增强结构体
 *  @return       1 成功 / 0 失败                              */

/* ---- 2a. 伪彩色 ---- */

int camera_get_palette(LONG uid, int *mode);
/*  获取当前伪彩色模式。
 *  @param mode   输出:1=白热 2=黑热 10=融合1 11=彩虹 12=铁红1
 *                13=琥珀1 14=琥珀2 15=高对比 16=色彩1 17=色彩2
 *                18=冰火 19=雨 20=高亮 21=聚焦 22=紫红
 *  @return       1 成功 / 0 失败                              */

int camera_set_palette(LONG uid, int mode);
/*  设置伪彩色模式。
 *  @param mode   同上取值
 *  @return       1 成功 / 0 失败                              */

/* ---- 2b. AGC 模式 ---- */

int camera_get_agc_mode(LONG uid, int *mode);
/*  读取 AGC(自动增益控制)模式。
 *  @param mode   输出:1=线性 2=直方图 3=巡检 4=手动
 *  @return       1 成功 / 0 失败                              */

int camera_set_agc_mode(LONG uid, int mode);
/*  设置 AGC 模式。
 *  @param mode   同上取值
 *  @return       1 成功 / 0 失败                              */

/* ---- 2c. DDE(数字细节增强) ---- */

int camera_get_dde_enabled(LONG uid, int *enabled);
/*  读取 DDE 开关状态。
 *  @param enabled 输出:0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_set_dde_enabled(LONG uid, int enabled);
/*  设置 DDE 开关。
 *  @param enabled 0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_get_dde_level(LONG uid, DWORD *level);
/*  读取 DDE 细节增强级别。
 *  @param level  输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_dde_level(LONG uid, DWORD level);
/*  设置 DDE 细节增强级别。
 *  @param level  0~100
 *  @return       1 成功 / 0 失败                              */

/* ---- 2d. 降噪 ---- */

int camera_get_noise_reduce_mode(LONG uid, int *mode);
/*  读取降噪模式。
 *  @param mode   输出:0=关闭 1=普通 2=专家
 *  @return       1 成功 / 0 失败                              */

int camera_set_noise_reduce_mode(LONG uid, int mode);
/*  设置降噪模式。
 *  @param mode   同上取值。注意:设为 2(专家)时需配合 temporal/spatial 级别。
 *  @return       1 成功 / 0 失败                              */

int camera_get_general_denoise(LONG uid, DWORD *level);
/*  读取普通降噪级别(普通模式下生效)。
 *  @param level  输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_general_denoise(LONG uid, DWORD level);
/*  设置普通降噪级别。自动将降噪模式设为 1(普通)。
 *  @param level  0~100
 *  @return       1 成功 / 0 失败                              */

int camera_get_spatial_denoise(LONG uid, DWORD *level);
/*  读取空域降噪级别(专家模式下生效)。
 *  @param level  输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_spatial_denoise(LONG uid, DWORD level);
/*  设置空域降噪级别。自动将降噪模式设为 2(专家)。
 *  @param level  0~100
 *  @return       1 成功 / 0 失败                              */

int camera_get_temporal_denoise(LONG uid, DWORD *level);
/*  读取时域降噪级别(专家模式下生效)。
 *  @param level  输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_temporal_denoise(LONG uid, DWORD level);
/*  设置时域降噪级别。自动将降噪模式设为 2(专家)。
 *  @param level  0~100
 *  @return       1 成功 / 0 失败                              */

/* ---- 2e. 勾边 ---- */

int camera_get_hook_edge(LONG uid, int *enabled, int *level);
/*  读取勾边(边缘增强)参数。
 *  @param enabled 输出:0=关闭 1=开启
 *  @param level   输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_hook_edge(LONG uid, int enabled, int level);
/*  设置勾边参数。
 *  @param enabled 0=关闭 1=开启
 *  @param level   0~100
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  3.  视频调整 VideoAdjust(镜像/数字变倍/工频)                            *
 * ======================================================================== */

int camera_get_video_adjust(LONG uid, USB_IMAGE_VIDEO_ADJUST *va);
/*  读取完整的视频调整参数(镜像/变倍/工频/走廊模式等)。
 *  @param uid    登录句柄
 *  @param va     输出:视频调整结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_video_adjust(LONG uid, const USB_IMAGE_VIDEO_ADJUST *va);
/*  写入完整的视频调整参数。
 *  @param uid    登录句柄
 *  @param va     输入:填充好的视频调整结构体
 *  @return       1 成功 / 0 失败                              */

int camera_get_mirror_mode(LONG uid, int *mode);
/*  读取镜像模式。
 *  @param mode   输出:0=关闭 1=中心 2=左右 3=上下
 *  @return       1 成功 / 0 失败                              */

int camera_set_mirror_mode(LONG uid, int mode);
/*  设置镜像模式。
 *  @param mode   同上取值
 *  @return       1 成功 / 0 失败                              */

int camera_get_digital_zoom(LONG uid, int *zoom);
/*  读取数字变倍倍率。
 *  @param zoom   输出:0=1× 1=2× 2=4× 3=8×
 *  @return       1 成功 / 0 失败                              */

int camera_set_digital_zoom(LONG uid, int zoom);
/*  设置数字变倍。
 *  @param zoom   同上取值
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  4.  亮度 / 对比度(单字段结构体)                                         *
 * ======================================================================== */

int camera_get_brightness(LONG uid, DWORD *value);
/*  读取图像亮度。
 *  @param value  输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_brightness(LONG uid, DWORD value);
/*  设置图像亮度。
 *  @param value  0~100
 *  @return       1 成功 / 0 失败                              */

int camera_get_contrast(LONG uid, DWORD *value);
/*  读取图像对比度。
 *  @param value  输出:0~100
 *  @return       1 成功 / 0 失败                              */

int camera_set_contrast(LONG uid, DWORD value);
/*  设置图像对比度。
 *  @param value  0~100
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  5.  测温基本参数 Thermometry                                              *
 *      USB_THERMOMETRY_BASIC_PARAM 结构体(发射率/距离/范围/单位/报警/环境等)   *
 * ======================================================================== */

int camera_get_thermometry(LONG uid, USB_THERMOMETRY_BASIC_PARAM *tp);
/*  读取完整的测温基本参数结构体(所有字段)。
 *  @param uid    登录句柄
 *  @param tp     输出:测温参数结构体(tp->dwSize 预置 sizeof(*tp))
 *  @return       1 成功 / 0 失败                              */

int camera_set_thermometry(LONG uid, const USB_THERMOMETRY_BASIC_PARAM *tp);
/*  写入完整的测温基本参数结构体。
 *  @param uid    登录句柄
 *  @param tp     输入:填充好的测温参数结构体
 *  @return       1 成功 / 0 失败                              */

/* ---- 5a. 发射率 ---- */

int camera_get_emissivity(LONG uid, DWORD *value_x100);
/*  读取发射率。
 *  @param value_x100  输出:实际值×100(例如 0.97 → 97)
 *  @return       1 成功 / 0 失败                              */

int camera_set_emissivity(LONG uid, DWORD value_x100);
/*  设置发射率。
 *  @param value_x100  实际值×100(范围 1~100,对应 0.01~1.00)
 *  @return       1 成功 / 0 失败                              */

/* ---- 5b. 测温距离 ---- */

int camera_get_distance(LONG uid, DWORD *distance_cm);
/*  读取测温距离。
 *  @param distance_cm  输出:厘米
 *  @return       1 成功 / 0 失败                              */

int camera_set_distance(LONG uid, DWORD distance_cm);
/*  设置测温距离。
 *  @param distance_cm  厘米,范围 30~200(即 0.3~2.0 米)
 *  @return       1 成功 / 0 失败                              */

int camera_get_distance_unit(LONG uid, int *unit);
/*  读取距离单位。
 *  @param unit   输出:1=米 2=厘米 3=英尺
 *  @return       1 成功 / 0 失败                              */

int camera_set_distance_unit(LONG uid, int unit);
/*  设置距离单位。
 *  @param unit   同上取值
 *  @return       1 成功 / 0 失败                              */

/* ---- 5c. 温度单位 ---- */

int camera_get_temp_unit(LONG uid, int *unit);
/*  读取温度单位。
 *  @param unit   输出:1=℃  2=℉  3=K
 *  @return       1 成功 / 0 失败                              */

int camera_set_temp_unit(LONG uid, int unit);
/*  设置温度单位。
 *  @param unit   同上取值
 *  @return       1 成功 / 0 失败                              */

/* ---- 5d. 测温范围 ---- */

int camera_get_temp_range(LONG uid, int *range);
/*  读取测温范围档位。
 *  @param range  输出:1~5(各档位对应的上下限温度由硬件决定,只读)
 *  @return       1 成功 / 0 失败                              */

int camera_set_temp_range(LONG uid, int range);
/*  设置测温范围档位。
 *  @param range  1~5
 *  @return       1 成功 / 0 失败                              */

int camera_get_temp_range_auto_enabled(LONG uid, int *enabled);
/*  读取测温档位自动切换开关。
 *  @param enabled  输出:0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_set_temp_range_auto_enabled(LONG uid, int enabled);
/*  设置测温档位自动切换开关。
 *  @param enabled  0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

/* ---- 5e. 标定系数 ---- */

int camera_get_calibration_coeff(LONG uid, DWORD *value_x100);
/*  读取标定系数。
 *  @param value_x100  输出:实际值×100(例如 1.00 → 100),范围 0~3000
 *  @return       1 成功 / 0 失败                              */

int camera_set_calibration_coeff(LONG uid, DWORD value_x100);
/*  设置标定系数。
 *  @param value_x100  实际值×100(0.00~30.00 对应 0~3000)
 *  @return       1 成功 / 0 失败                              */

int camera_get_calibration_coeff_enabled(LONG uid, int *enabled);
/*  读取标定系数启用状态。
 *  @param enabled  输出:0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_set_calibration_coeff_enabled(LONG uid, int enabled);
/*  设置标定系数启用状态。
 *  @param enabled  0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

/* ---- 5f. 反射温度 ---- */

int camera_get_reflective_temp(LONG uid, DWORD *temp_x10);
/*  读取反射温度补偿值。
 *  @param temp_x10  输出:(实际温度 + 100) × 10(例如 25.0℃ → 1250)
 *  @return       1 成功 / 0 失败                              */

int camera_set_reflective_temp(LONG uid, DWORD temp_x10);
/*  设置反射温度补偿值。
 *  @param temp_x10  (实际温度 + 100) × 10
 *  @return       1 成功 / 0 失败                              */

int camera_get_reflective_enabled(LONG uid, int *enabled);
/*  读取反射温度使能。
 *  @param enabled  输出:0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_set_reflective_enabled(LONG uid, int enabled);
/*  设置反射温度使能。
 *  @param enabled  0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

/* ---- 5g. 环境温度 ---- */

int camera_get_env_temp(LONG uid, int *enabled, DWORD *temp);
/*  读取环境温度参数。
 *  @param enabled 输出:0=关闭 1=开启
 *  @param temp    输出:(实际值 + 100) × 10,-99.0~99.0℃
 *  @return       1 成功 / 0 失败                              */

int camera_set_env_temp(LONG uid, int enabled, DWORD temp);
/*  设置环境温度参数。
 *  @param enabled 0=关闭 1=开启
 *  @param temp    (实际值 + 100) × 10
 *  @return       1 成功 / 0 失败                              */

/* ---- 5h. 报警温度 ---- */

#define CAMERA_ALERT_TEMP_MIN  0u
#define CAMERA_ALERT_TEMP_MAX  20000u  /* 200.00℃ × 100 */

int camera_get_alert_temp(LONG uid, DWORD *temp_x100);
/*  读取预警温度(根据位号显示框标记)。
 *  @param temp_x100  输出:实际值×100
 *  @return       1 成功 / 0 失败                              */

int camera_set_alert_temp(LONG uid, DWORD temp_x100);
/*  设置预警温度。
 *  @param temp_x100  实际值×100
 *  @return       1 成功 / 0 失败                              */

int camera_get_alarm_temp(LONG uid, DWORD *temp_x100);
/*  读取报警温度(超温报警)。
 *  @param temp_x100  输出:实际值×100
 *  @return       1 成功 / 0 失败                              */

int camera_set_alarm_temp(LONG uid, DWORD temp_x100);
/*  设置报警温度。
 *  @param temp_x100  实际值×100
 *  @return       1 成功 / 0 失败                              */

/* ---- 5i. 显示元素 ---- */

int camera_get_display_flags(LONG uid, int *max_en, int *min_en,
                             int *avg_en, int *cen_en);
/*  读取测温信息叠加显示选项。
 *  @param max_en  输出:0=关闭 1=开启  最高温显示
 *  @param min_en  输出:0=关闭 1=开启  最低温显示
 *  @param avg_en  输出:0=关闭 1=开启  平均温显示
 *  @param cen_en  输出:0=关闭 1=开启  中心温显示
 *  @return       1 成功 / 0 失败                              */

int camera_set_display_flags(LONG uid, int max_en, int min_en,
                             int avg_en, int cen_en);
/*  设置测温信息叠加显示选项。
 *  @param max_en/min_en/avg_en/cen_en  0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

/* ---- 5j. 全局开关与码流叠加 ---- */

int camera_get_thermometry_enabled(LONG uid, int *enabled);
/*  读取测温功能全局使能。
 *  @param enabled  输出:0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_set_thermometry_enabled(LONG uid, int enabled);
/*  设置测温功能全局使能。
 *  @param enabled  0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_get_thermometry_overlay(LONG uid, int *enabled);
/*  读取测温信息码流叠加使能(由设备在视频流中叠加温度文字/框)。
 *  @param enabled  输出:0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

int camera_set_thermometry_overlay(LONG uid, int enabled);
/*  设置测温信息码流叠加使能。
 *  @param enabled  0=关闭 1=开启
 *  @return       1 成功 / 0 失败                              */

/* ---- 5k. 外部光学参数 ---- */

int camera_get_external_optics_correction(LONG uid, DWORD *temp);
/*  读取外部光学窗口校正温度。
 *  @param temp    输出:实际值×100
 *  @return       1 成功 / 0 失败                              */

int camera_set_external_optics_correction(LONG uid, DWORD temp);
/*  设置外部光学窗口校正温度。
 *  @param temp    实际值×100
 *  @return       1 成功 / 0 失败                              */

int camera_get_atmospheric_humidity(LONG uid, DWORD *humidity);
/*  读取大气湿度补偿值。
 *  @param humidity 输出:百分比×100(例如 5000 = 50.00%)
 *  @return       1 成功 / 0 失败                              */

int camera_set_atmospheric_humidity(LONG uid, DWORD humidity);
/*  设置大气湿度补偿值。
 *  @param humidity 百分比×100
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  6.  测温模式 ThermometryMode                                              *
 *      USB_THERMOMETRY_MODE:普通 / 专家 + ROI 使能                          *
 * ======================================================================== */

int camera_get_thermometry_mode(LONG uid, USB_THERMOMETRY_MODE *mode);
/*  读取测温模式(普通/专家)及 ROI 使能状态。
 *  @param uid    登录句柄
 *  @param mode   输出:测温模式结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_thermometry_mode(LONG uid, const USB_THERMOMETRY_MODE *mode);
/*  设置测温模式。
 *  @param uid    登录句柄
 *  @param mode   输入:填充好的测温模式结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  7.  测温规则区域 ThermometryRegions                                       *
 *      USB_THERMOMETRY_REGIONS:多个 THERMAL_REGION,归一化坐标 0-1000       *
 * ======================================================================== */

int camera_get_thermometry_regions(LONG uid, USB_THERMOMETRY_REGIONS *regs);
/*  读取测温规则区域配置。
 *  @param uid    登录句柄
 *  @param regs   输出:测温规则区域结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_thermometry_regions(LONG uid,
                                   const USB_THERMOMETRY_REGIONS *regs);
/*  写入测温规则区域配置。
 *  @param uid    登录句柄
 *  @param regs   输入:填充好的测温规则区域结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  8.  测温修正 TemperatureCorrect                                           *
 *      USB_TEMPERATURE_CORRECT:黑体温度修正(发射率/距离/黑体温度/修正温度)   *
 * ======================================================================== */

int camera_get_temperature_correct(LONG uid, USB_TEMPERATURE_CORRECT *tc);
/*  读取测温修正参数(黑体修正使能/发射率/距离/黑体温度/中心坐标/修正温度)。
 *  @param uid    登录句柄
 *  @param tc     输出:测温修正结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_temperature_correct(LONG uid,
                                   const USB_TEMPERATURE_CORRECT *tc);
/*  写入测温修正参数。
 *  @param uid    登录句柄
 *  @param tc     输入:填充好的测温修正结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  9.  黑体参数 BlackBody                                                    *
 *      USB_BLACK_BODY:黑体使能/发射率/距离/黑体温度/中心坐标                 *
 * ======================================================================== */

int camera_get_black_body(LONG uid, USB_BLACK_BODY *bb);
/*  读取黑体参数(黑体使能/发射率/距离/温度/中心坐标)。
 *  @param uid    登录句柄
 *  @param bb     输出:黑体参数结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_black_body(LONG uid, const USB_BLACK_BODY *bb);
/*  写入黑体参数。
 *  @param uid    登录句柄
 *  @param bb     输入:填充好的黑体参数结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  10. 体温补偿 BodytempCompensation                                         *
 *      USB_BODYTEMP_COMPENSATION:人体测温使能/补偿类型/补偿值/环境温度/曲线   *
 * ======================================================================== */

int camera_get_bodytemp_compensation(LONG uid,
                                     USB_BODYTEMP_COMPENSATION *bc);
/*  读取体温补偿参数(使能/补偿方式/补偿值/环境温度/曲线灵敏度等)。
 *  @param uid    登录句柄
 *  @param bc     输出:体温补偿结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_bodytemp_compensation(LONG uid,
                                     const USB_BODYTEMP_COMPENSATION *bc);
/*  写入体温补偿参数。
 *  @param uid    登录句柄
 *  @param bc     输入:填充好的体温补偿结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  11. 全屏测温参数 P2PParam                                                 *
 *      USB_P2P_PARAM:全屏测温 JPEG 图返回使能                                *
 * ======================================================================== */

int camera_get_p2p_param(LONG uid, USB_P2P_PARAM *p2p);
/*  读取全屏测温参数(JPEG 图返回使能)。
 *  @param uid    登录句柄
 *  @param p2p    输出:全屏测温参数结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_p2p_param(LONG uid, const USB_P2P_PARAM *p2p);
/*  写入全屏测温参数。
 *  @param uid    登录句柄
 *  @param p2p    输入:填充好的全屏测温参数结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  12. 热成像码流参数 ThermalStreamParam                                     *
 *      USB_THERMAL_STREAM_PARAM:编码类型(裸数据/全屏测温/YUV 等)           *
 *      注意:camera_configure() 已在 camera.c 中调用 SET,此处只提供 GET。    *
 * ======================================================================== */

int camera_get_thermal_stream_param(LONG uid,
                                    USB_THERMAL_STREAM_PARAM *sp);
/*  读取当前热成像码流参数(编码类型/分辨率/帧率)。
 *  @param uid    登录句柄
 *  @param sp     输出:码流参数结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  13. 算法版本 ThermalAlgVersion                                            *
 * ======================================================================== */

int camera_get_thermal_alg_version(LONG uid,
                                   USB_THERMAL_ALG_VERSION *ver);
/*  读取热成像算法版本(算法库名称、逻辑版本号)。
 *  @param uid    登录句柄
 *  @param ver    输出:算法版本结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  14. 诊断信息导出(系统诊断)                                               *
 *      USB_SYSTEM_DIAGNOSED_DATA:导出设备诊断信息到文件                      *
 * ======================================================================== */

int camera_get_system_diagnosed_data(LONG uid,
                                     USB_SYSTEM_DIAGNOSED_DATA *dd);
/*  导出设备诊断信息。调用前 dd->pDiagnosedData 需指向足够大的缓冲区,
 *  dd->dwDataLenth 需预置缓冲区大小。
 *  @param uid    登录句柄
 *  @param dd     输出:诊断数据结构体(诊断数据写入 pDiagnosedData)
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  15. ROI 最高温搜索(复杂操作,涉及 JPEG 输出)                              *
 *      USB_ROI_MAX_TEMPERATURE_SEARCH:设置 ROI 区域查询最高温                 *
 * ======================================================================== */

int camera_get_roi_max_temperature_search(
        LONG uid,
        USB_ROI_MAX_TEMPERATURE_SEARCH *in,
        USB_ROI_MAX_TEMPERATURE_SEARCH_RESULT *out);
/*  ROI 最高温信息查询。输入 ROI 区域和时间,输出各 ROI 最高温及可选的 JPEG 图。
 *  调用前 out->pJpegPic 需指向足够大的缓冲区,out->dwJpegPicLen 预置缓冲区大小。
 *  @param uid    登录句柄
 *  @param in     输入:查询条件(ROI 区域/时间/JPEG 使能等)
 *  @param out    输出:查询结果(最高温/坐标/JPEG 数据)
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  16. 双光校准(复杂操作,涉及可见光图片文件)                                *
 * ======================================================================== */

int camera_post_double_lights_correct(
        LONG uid,
        const USB_DOUBLE_LIGHTS_CORRECT *in,
        USB_DOUBLE_LIGHTS_CORRECT_RESULT *out);
/*  双光校准。输入可见光图片和校准参数,输出校准结果 JPEG。
 *  调用前 in->pVisiblePic 指向已读入的可见光图片数据。
 *         out->pJpegPic 指向输出 JPEG 缓冲区,dwJpegPicLen 预置缓冲区大小。
 *  @param uid    登录句柄
 *  @param in     输入:校准参数
 *  @param out    输出:校准结果
 *  @return       1 成功 / 0 失败                              */

/* --- 双光校准坐标控制 --- */

int camera_get_double_lights_correct_points_ctrl(
        LONG uid, USB_DOUBLE_LIGHTS_CORRECT_POINTS_CTRL *ctrl);
/*  读取双光校准坐标控制使能。
 *  @param ctrl   输出:坐标控制结构体
 *  @return       1 成功 / 0 失败                              */

int camera_set_double_lights_correct_points_ctrl(
        LONG uid, const USB_DOUBLE_LIGHTS_CORRECT_POINTS_CTRL *ctrl);
/*  设置双光校准坐标控制使能。
 *  @param ctrl   输入:填充好的坐标控制结构体
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  17. 测温标定文件导入导出                                                  *
 * ======================================================================== */

int camera_get_thermometry_calibration_file(
        LONG uid, USB_THERMOMETRY_CALIBRATION_FILE *cf);
/*  导出测温标定文件。
 *  调用前 cf->pCalibrationFile 指向足够大的缓冲区,cf->dwFileLenth 预置缓冲区大小。
 *  @param uid    登录句柄
 *  @param cf     输出:标定文件数据
 *  @return       1 成功 / 0 失败                              */

int camera_set_thermometry_calibration_file(
        LONG uid, const USB_THERMOMETRY_CALIBRATION_FILE *cf);
/*  导入测温标定文件。
 *  调用前 cf->pCalibrationFile 指向已读入的标定文件数据,cf->dwFileLenth 为数据长度,
 *  cf->byFileName 需填文件名。
 *  @param uid    登录句柄
 *  @param cf     输入:标定文件数据
 *  @return       1 成功 / 0 失败                              */

/* ======================================================================== *
 *  18. 命令状态查询                                                        *
 * ======================================================================== */

int camera_get_command_state(LONG uid, USB_COMMAND_STATE *state);
/*  查询设备命令状态(用于判断设备是否空闲可接收新命令)。
 *  @param state  输出:命令状态结构体
 *  @return       1 成功 / 0 失败                              */

#ifdef __cplusplus
}
#endif

#endif /* CAMERA_CONFIG_H */

参考目录:

参考

  • 原始码值 → 摄氏度
  • 温度统计结果,对一帧 96×96 原始数据分析(一个结构体包含原始数据,温度数据,温度最高最低点坐标)
  • 最高/最低/平均/中心温及其坐标
  • 缩放原始测温矩阵
/*
 * thermal.h — 热成像测温数据处理声明
 *
 * 原始值 → 摄氏度换算(由固件决定):
 *   TEMP_RAW_TO_C(raw) = raw/64.0 - 50.0
 *   raw = 16-bit 无符号原始值,范围约 3200~12800(0℃~150℃)
 *
 * 坐标系统:
 *   原始测温矩阵 96×96(由传感器分辨率固定),
 *   compute_thermal_stats() 在该分辨率上搜索最高/最低温 x,y。
 */

#ifndef THERMAL_H
#define THERMAL_H

#include <stdint.h>

/* 原始码值 → 摄氏度 */
#define TEMP_RAW_TO_C(raw)  ((raw) / 64.0f - 50.0f)

/* 温度统计结果(对一帧 96×96 原始数据分析得出) */
typedef struct {
    uint16_t raw_min, raw_max, raw_center;  /* 原始码值 */
    float    min_c, max_c, avg_c, center_c; /* 对应摄氏度 */
    int      min_x, min_y, max_x, max_y;    /* 最高/最低温在 96×96 网格中的坐标 */
} thermal_stats_t;

/* 分析一帧原始测温数据。
 * @param temp  96×96 uint16_t 原始数据
 * @param w,h  宽度和高度(固定 96×96)
 * @return      thermal_stats_t 包含最高/最低/平均/中心温及其坐标
 */
thermal_stats_t compute_thermal_stats(const uint16_t *temp, int w, int h);

/* 缩放原始测温矩阵(最近邻插值)。
 * @param src  输入矩阵 sw×sh
 * @param dst  输出矩阵 dw×dh(调用者分配)
 */
void scale_thermal(const uint16_t *src, int sw, int sh,
                   uint16_t *dst, int dw, int dh);

#endif
/*
 * thermal.c — 测温数据处理实现
 *
 * 提供两个核心函数:
 *   compute_thermal_stats — 遍历 96×96 原始数据,找最高温/最低温/中心温/平均温
 *   scale_thermal         — 最近邻插值缩放原始矩阵
 *
 * 原始值 → 摄氏度换算(固件约定):
 *   °C = raw_value / 64 - 50
 *   例如 raw=6400 → 50.0°C
 */

#include "thermal.h"
#include <limits.h>

/*
 * 最近邻插值缩放原始测温矩阵。
 *
 * @param src  输入矩阵 sw×sh
 * @param dst  输出矩阵 dw×dh(由调用者分配)
 * @param sw,sh  输入尺寸
 * @param dw,dh  输出尺寸
 */
void scale_thermal(const uint16_t *src, int sw, int sh,
                   uint16_t *dst, int dw, int dh)
{
    for (int dy = 0; dy < dh; dy++) {
        int sy = dy * sh / dh;
        for (int dx = 0; dx < dw; dx++) {
            int sx = dx * sw / dw;
            dst[dy * dw + dx] = src[sy * sw + sx];
        }
    }
}

/*
 * 计算一帧原始测温数据的统计信息。
 *
 * 遍历整个矩阵,同时找出最高温/最低温的原始值及其坐标,
 * 计算平均温,读取中心点温度。
 * 所有温度同时以原始值和摄氏度返回。
 *
 * @param temp  96×96 uint16_t 原始数据
 * @param w,h   矩阵尺寸(固定 96×96)
 * @return      填充好的 thermal_stats_t
 */
thermal_stats_t compute_thermal_stats(const uint16_t *temp, int w, int h)
{
    thermal_stats_t stats;
    int      pixels = w * h;
    uint16_t tmin = UINT16_MAX, tmax = 0;
    double   sum = 0.0;
    int      cx = w / 2, cy = h / 2;
    int      min_xi = 0, min_yi = 0, max_xi = 0, max_yi = 0;

    for (int yi = 0; yi < h; yi++) {
        for (int xi = 0; xi < w; xi++) {
            uint16_t v = temp[yi * w + xi];
            if (v < tmin) { tmin = v; min_xi = xi; min_yi = yi; }
            if (v > tmax) { tmax = v; max_xi = xi; max_yi = yi; }
            sum += v;
        }
    }

    stats.raw_min    = tmin;
    stats.raw_max    = tmax;
    stats.raw_center = temp[cy * w + cx];
    stats.min_c      = TEMP_RAW_TO_C(tmin);
    stats.max_c      = TEMP_RAW_TO_C(tmax);
    stats.avg_c      = TEMP_RAW_TO_C(sum / pixels);
    stats.center_c   = TEMP_RAW_TO_C(stats.raw_center);
    stats.min_x      = min_xi;
    stats.min_y      = min_yi;
    stats.max_x      = max_xi;
    stats.max_y      = max_yi;

    return stats;
}

V4L2

Media control

media-controller框架专门用于控制soc内部 视频相关 硬件连接状态。

以rk的SOC为例:

mipi摄像头(DPHY输出)
	 ↓
SOC: DPHY
	 ↓
SOC: csi2_dphy
	 ↓
SOC: mipi1_csi2(CSI-2 Host)
	 ↓								 DMA(CMA)
SOC: rkcif_mipi_lvds1(VICAP / CIF) → 输出到DDR
	 ↓
SOC: RK-ISP子系统
	 ↓ (处理后)
  输出到DDR(DMA)

比如:直接控制不走ISP

不同平台有不一样的链路,所以需要一个专门的框架去做抽象管理。

ls /dev/media*
media-ctl -p -d /dev/media0

1 概述

V4L2 是 Linux 内核中视频设备驱动的标准接口规范,广泛用于摄像头、电视调谐器、视频采集卡等设备。

  • 编译内核开启支持

  • 修改设备树

  • 构建根文件系统配置工具集

2 虚拟机中测试

2.1 安装

apt install v4l-utils

2.2 查看设备列表

v4l2-ctl --list-devices
HikCamera: UVC Camera (usb-0000:02:03.0-1):
        /dev/video0
        /dev/video1
        /dev/media0
  1. 不同功能通道

    • video0:主视频流(如MJPG、YUYV格式的实时预览)
    • video1:可能是元数据通道(如时间戳、IMU数据)、静态抓拍通道,或者同一个物理摄像头的另一个实例(例如支持同时输出高低两种分辨率)。
  2. UVC扩展单元 某些UVC摄像头内部有多个逻辑子设备(如视频处理单元、静态图像捕获单元),内核驱动可能为每个子设备单独创建一个/dev/video节点。

  3. /dev/media0

    Media Controller设备节点,用于管理摄像头内部的复杂管线。

    • 作用:当V4L2设备内部有多个模块(如传感器、ISP、缩放器)时,/dev/media0允许用户空间程序查看和配置这些模块之间的连接关系(称为“pipeline”)。
    • 典型场景:UVC摄像头如果实现了媒体控制器API,你会看到这个节点。它本身不直接传输视频,而是通过media-ctl工具来配置内部路由,然后再通过/dev/video0采集数据。

2.3 设备详细

v4l2-ctl -d /dev/video0 --all
Driver Info:
        Driver name      : uvcvideo
        Card type        : HikCamera: UVC Camera
        Bus info         : usb-0000:02:03.0-1
        Driver version   : 5.15.199
....

2.4 设备支持的分辨率

v4l2-ctl -d /dev/video0 --list-formats-ext
ioctl: VIDIOC_ENUM_FMT
        Type: Video Capture

        [0]: 'YUYV' (YUYV 4:2:2)
                Size: Discrete 256x392
                        Interval: Discrete 0.040s (25.000 fps)
              	....
        [1]: 'MJPG' (Motion-JPEG, compressed                
        		Size: Discrete 120x160
                        Interval: Discrete 0.040s (25.000 fps)
                ....        
        [2]: 'H264' (H.264, compressed)
                Size: Discrete 240x320
                        Interval: Discrete 0.033s (30.000 fps)

2.5 获取一帧数据

v4l2-ctl -d /dev/video0 --set-fmt-video=width=256,height=392,pixelformat=YUYV --stream-mmap --stream-count=1 --stream-to=frame.raw
v4l2-ctl -d /dev/video0 --set-fmt-video=width=256,height=392,pixelformat=MJPG --stream-mmap --stream-count=1 --stream-to=frame.jpg

2.6 设置分辨率

v4l2-ctl -d /dev/video0 --set-fmt-video=width=800,height=600,pixelformat=UYVY

像素格式有哪些?

常见的像素格式主要分为原始像素格式(Raw Formats)和视频编码格式。你的提问里YUV和RGB是存储色彩的原始数据,而H.264则是用来压缩打包这些数据的“压缩包格式”。

  • 原始像素格式 (Raw Formats):这类格式直接描述了每个像素点的颜色数据,不经过压缩。它们又主要分为两种:
格式大类代表格式内存占用 (1080p每帧)主要优缺点数据排列方式
RGB类RGB24, BGR24, RGBA32~6.2MB数据量大,逻辑直观,适合直接在屏幕上显示和处理。每个像素固定存储红、绿、蓝三原色的数据。
YUV类YUYV, NV12, I420, YV12约3~4.1MB数据量较小,将亮度和颜色分离,利于压缩和兼容黑白显示,是视频领域的“通行证”。每个像素都有亮度(Y)值,而色度(U/V)值则由相邻像素共享。

YUV的采样方式(如4:4:4、4:2:2、4:2:0)决定了色度信息的保留程度。常见YUV格式的区别如下:

格式名称采样方式存储类型特点
YUYV4:2:2打包 (Packed)每两个像素共享一对UV,顺序交错排列。
UYVY4:2:2打包 (Packed)与YUYV类似,但顺序变为U、Y、V、Y。
NV124:2:0半平面 (Semi-Planar)一个Y平面,一个UV交错存储的平面,在Windows、Android和硬件编解码中很常见。
I4204:2:0平面 (Planar)三个平面顺序存储:全部Y,然后全部U,最后全部V。在FFmpeg等软件中广泛使用。
YV124:2:0平面 (Planar)与I420类似,但存储顺序是Y,然后V,最后U。
  • 视频编码格式 (Video Codecs)
    • 特点:通过复杂的算法(如帧内、帧间预测)对原始像素数据进行高倍率压缩。
    • 代表:H.264、H.265、MJPEG等。它们是处理视频文件或通过网络传输时的格式。其中,H.264 是一种高效的视频编码标准,常与原始像素格式(如YUYV)搭配使用,先将数据压缩成H.264再传输。

🔬 YUYV 的深层解读与数据帧结构

1. YUYV 为什么重要?

YUYV 在摄像头和视频领域扮演着“通用语言”的角色。它数据量适中(YUV 4:2:2采样),能提供不错的图像质量,因此被很多USB摄像头(遵循UVC标准)默认支持。

2. YUYV 的数据帧长什么样?

YUYV是一种打包(Packed)格式,意味着它的Y、U、V分量是交错存储在一起的。

采样方式与数据组成:

  • 采样方式:YUV 4:2:2,意味着在水平方向上,每2个像素点共享一对UV色度信息,但每个像素都保留自己独立的Y亮度信息。
  • 数据组成:一个像素对 (4字节) = Y0 (1字节) + U (1字节) + Y1 (1字节) + V (1字节)。所以,如果图像分辨率是 W x H,那么一帧完整画面的数据量约为 W \* H \* 2 字节

3. 数据排列顺序

假设你有一张4x4像素的图片,YUYV数据在内存中的排列顺序如下图所示,一个格子代表1个字节:

字节偏移Byte 01234567
数据内容Y00U00Y01V00Y02U01Y03V01
所属像素像素(0,0)(0,0)&(0,1)像素(0,1)(0,0)&(0,1)像素(0,2)(0,2)&(0,3)像素(0,3)(0,2)&(0,3)
  • Y后面跟行号和列号,例如Y00表示第0行第0列的Y值。
  • 可以看到,U和V分量总是被相邻的两个Y像素所共享。

🆚 与其他像素格式的数据帧对比

YUYV与其他常见格式的主要区别在于采样方式排列方式

对比项YUYV (YUV 4:2:2, Packed)NV12 (YUV 4:2:0, Semi-Planar)RGB24 (RGB, Packed)H.264 (压缩格式)
采样方式YUV 4:2:2YUV 4:2:0N/A (每个像素包含完整R,G,B)基于视频编码标准
存储类型打包 (Packed)半平面 (Semi-Planar)打包 (Packed)码流格式 (Bitstream)
数据排列交错排列:Y U Y V先存全部Y,再存交错UV顺序排列:R G BB G R包含NAL单元等复杂结构
每像素占用16 bits = 2 bytes12 bits = 1.5 bytes24 bits = 3 bytes高度可变,通常远小于原始格式
内存布局示例[Y0 U0 Y1 V0] [Y2 U1 Y3 V1][Y0 Y1 Y2 Y3 ...] [U0 V0 U1 V1 ...][R0 G0 B0] [R1 G1 B1] [R2 G2 B2] ...无法直接查看像素,需要解码器解析
优点图像质量较好,兼容性好压缩率高,是流媒体和硬件编解码的主流格式色彩表现直接、完整压缩率极高,适合网络传输和存储
缺点数据量较大垂直方向的色彩细节有损失数据量巨大编解码复杂,有算力开销

💡 YUYV 与 H.264 的本质区别

最后,澄清一个最核心的误解:YUYV 和 H.264 是完全不同层次的概念,它们是“原材料”与“压缩包”的关系。

  • YUYV原始像素格式(原材料)。摄像头内部CMOS传感器捕获的原始图像数据,可以直接用来描述图像。
  • H.264视频编码标准(压缩算法/压缩包)。为了高效存储或通过网络传输,用H.264算法对“原材料”(如YUYV数据)进行复杂压缩,得到的数据流就是H.264格式。

整个过程通常是:CMOS传感器捕获光信号 → 转换成原始YUYV格式数据(或其他原始像素格式)→ 送入编码器(如H.264编码器)进行压缩 → 输出 H.264格式的码流。对你来说,选择哪种格式,是在“画质好、体积大”的原始数据和“体积小、但编解码有开销”的压缩数据间做权衡。

描述了摄像头的图像采集能力——能输出哪些像素格式、每种格式下能达到哪些分辨率、当前配置是什么,以及如何试探和更改这些参数。

支持的像素格式(枚举)

  • 通过 VIDIOC_ENUM_FMT 列出所有可用的像素格式(如 YUYV、MJPEG、H264 等),并打印 FourCC 码和描述信息。
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char *dev = "/dev/video0";
static void fmtdesc(int fd)
{
    struct v4l2_fmtdesc fmt;
    printf("\n====== 枚举支持的像素格式 ======\n");
	memset(&fmt, 0, sizeof(fmt)); // 用 memset 将所有字段清零,避免残留数据影响 ioctl。
	fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置 fmt.type 为 V4L2_BUF_TYPE_VIDEO_CAPTURE (摄像头设备)

	for (fmt.index = 0; ; fmt.index++) { 
		if (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) < 0)
			break;
		printf("  [%d] 0x%08x  %s\n",
		       fmt.index,
		       fmt.pixelformat,
		       fmt.description);
	}
	printf("\n");
}
int main(void)
{
    int fd = open(dev, O_RDWR);
    if (fd < 0)
    {
        printf("open %s failed\n", dev);
        return -1;
    }
	fmtdesc(fd);
    close(fd);
    return 0;
}
  • 结果

    root@wyl:~/c-std/for-linux/099_other/v4l2# ./002_v4l2_fmt_type 
    ====== 枚举支持的像素格式 ======
      [0] 0x56595559  YUYV 4:2:2
    
/*
 * V4L2 像素格式与分辨率枚举示例
 *
 * 本例演示如何通过 V4L2 ioctl 接口:
 *   1. 枚举摄像头设备支持的所有像素格式(VIDIOC_ENUM_FMT)
 *   2. 对每种像素格式,枚举其支持的分辨率/帧尺寸(VIDIOC_ENUM_FRAMESIZES)
 *
 * 典型的 V4L2 枚举模式:
 *   设置结构体 -> 设置 index -> 调用 ioctl -> index++ 直到 ioctl 返回负值
 */

#include <sys/ioctl.h>   /* ioctl() */
#include <fcntl.h>       /* open() */
#include <linux/videodev2.h> /* V4L2 相关结构体与 ioctl 宏 */
#include <unistd.h>      /* close() */
#include <stdio.h>       /* printf() */
#include <stdlib.h>      /* EXIT_FAILURE 等 */
#include <string.h>      /* memset() */

/* 默认摄像头设备节点,可通过修改此变量更换设备 */
static char *dev = "/dev/video0";

/*
 * enum_framesizes - 枚举指定像素格式支持的帧尺寸(分辨率)
 * @fd:            打开的 video 设备文件描述符
 * @pixelformat:   要查询的像素格式 FourCC 码(如 V4L2_PIX_FMT_YUYV)
 *
 * 使用 VIDIOC_ENUM_FRAMESIZES ioctl 查询驱动报告的分辨率信息。
 * v4l2_frmsizeenum.type 有三种可能,分别对应不同的查询方式:
 *
 *   V4L2_FRMSIZE_TYPE_DISCRETE   离散列表
 *     驱动提供一个分辨率列表。需要递增 frmsize.index 反复调用,
 *     每次返回一个 (width, height) 对,直到 ioctl 返回负值(-1)表示枚举结束。
 *     常见于硬件编码器或老式 sensor。
 *
 *   V4L2_FRMSIZE_TYPE_STEPWISE   步进范围
 *     驱动报告一个矩形范围 (min_width~max_width, min_height~max_height)
 *     以及水平和垂直的步进值 (step_width, step_height)。
 *     实际可用分辨率为范围内按步进递增的所有组合。
 *     一次调用即可获得全部信息,无需循环。
 *
 *   V4L2_FRMSIZE_TYPE_CONTINUOUS 连续范围
 *     驱动报告 min~max 范围,此范围内任意分辨率都支持。
 *     实际上与 STEPWISE 使用相同的 union stepwise 字段,但步进视为 1。
 *     一次调用即可获得全部信息,无需循环。
 *
 * 返回值:无(结果直接 printf 打印)
 */
static void enum_framesizes(int fd, unsigned int pixelformat)
{
    struct v4l2_frmsizeenum frmsize;

    printf("       分辨率:\n");

    /*
     * 离散列表需要多次调用。index 从 0 开始递增,
     * 直到 ioctl 返回负值(表示没有更多分辨率了)。
     * 对于 STEPWISE 和 CONTINUOUS,第一次调用就已经拿到结果,
     * 第二次调用 ioctl 就会返回负值,因此也会直接退出循环。
     */
    for (int i = 0; ; i++) {
        memset(&frmsize, 0, sizeof(frmsize));
        frmsize.pixel_format = pixelformat;
        frmsize.index = i;

        /* VIDIOC_ENUM_FRAMESIZES 根据 pixel_format + index 查询分辨率 */
        if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) < 0)
            break;

        switch (frmsize.type) {
        case V4L2_FRMSIZE_TYPE_DISCRETE:
            /* 离散分辨率:每次返回一个具体的宽高值 */
            printf("          [%d] %dx%d\n", i,
                   frmsize.discrete.width,
                   frmsize.discrete.height);
            break;

        case V4L2_FRMSIZE_TYPE_STEPWISE:
            /*
             * 步进范围:min~max 范围内按 step 递增。
             * 例如 min_width=320, max_width=1920, step_width=160 表示
             * 支持 320, 480, 640, ..., 1920 这些宽度值。
             * 一次调用即得全部信息,return 退出函数,不再继续循环。
             */
            printf("          stepwise: %dx%d ~ %dx%d  step %dx%d\n",
                   frmsize.stepwise.min_width,  frmsize.stepwise.min_height,
                   frmsize.stepwise.max_width,  frmsize.stepwise.max_height,
                   frmsize.stepwise.step_width, frmsize.stepwise.step_height);
            return;

        case V4L2_FRMSIZE_TYPE_CONTINUOUS:
            /*
             * 连续范围:min~max 之间任意分辨率都支持。
             * 实际上驱动通常会限制一定的粒度,这里只是"名义上连续"。
             * 一次调用即得全部信息,return 退出函数。
             */
            printf("          continuous: %dx%d ~ %dx%d\n",
                   frmsize.stepwise.min_width,  frmsize.stepwise.min_height,
                   frmsize.stepwise.max_width,  frmsize.stepwise.max_height);
            return;
        }
    }
}

/*
 * enum_format_with_sizes - 枚举所有像素格式并打印其分辨率
 * @fd: 打开的 video 设备文件描述符
 *
 * 分两步:
 *   1. 使用 VIDIOC_ENUM_FMT 遍历驱动所有支持的像素格式
 *   2. 对每种格式调用 enum_framesizes() 获取分辨率
 *
 * VIDIOC_ENUM_FMT 使用说明:
 *   - 设置 fmt.type 为 V4L2_BUF_TYPE_VIDEO_CAPTURE(捕获设备)
 *   - 从 fmt.index = 0 开始递增调用 ioctl
 *   - 每次返回一种像素格式的 pixelformat(FourCC 码)和 description(可读名称)
 *   - 当 ioctl 返回负值时表示所有格式已枚举完毕
 *   - 每次调用前必须使用 memset 清零结构体,避免内核解析残留数据
 *
 * 为什么需要先清零结构体:
 *   V4L2 ioctl 采用"结构体传入+传出"模式。内核在解析输入字段时,
 *   如果结构体中有未初始化的字段(例如 padding 字节),可能导致
 *   内核在不同内核版本间行为不一致。memset 清零确保所有字段初始
 *   为 0,保证兼容性。
 */
static void enum_format_with_sizes(int fd)
{
    struct v4l2_fmtdesc fmt;

    printf("====== 枚举像素格式及支持的分辨率 ======\n");

    /* memset 清零:避免结构体中的残留数据影响 ioctl 行为 */
    memset(&fmt, 0, sizeof(fmt));

    /* 设置缓冲区类型为视频捕获,其他类型还有 V4L2_BUF_TYPE_VIDEO_OUTPUT 等 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    /*
     * index 从 0 开始递增枚举。驱动会按顺序返回每种格式,
     * 当枚举完所有格式后,ioctl 返回负值(errno 通常为 EINVAL)。
     * 这种"递增 index 直到失败"的模式在 V4L2 枚举接口中非常常见
     * (格式、分辨率、帧率、controls 等都采用此模式)。
     */
    for (fmt.index = 0; ; fmt.index++) {
        if (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) < 0)
            break;

        /* fmt.pixelformat 是 FourCC 码(如 0x56595559 即 "YUYV") */
        printf("  [%d] 0x%08x  %s\n",
               fmt.index,
               fmt.pixelformat,
               fmt.description);

        /* 对每种格式查询其支持的分辨率 */
        enum_framesizes(fd, fmt.pixelformat);
    }
    printf("\n");
}

int main(void)
{
    int fd;

    /*
     * 打开 V4L2 设备节点。
     * O_RDWR:读写模式,既可用于查询(G_FMT/ENUM_FMT)也可用于设置(S_FMT)。
     * 部分设备可能只需要 O_RDONLY,但 O_RDWR 兼容性最好。
     */
    fd = open(dev, O_RDWR);
    if (fd < 0) {
        perror("open");  /* perror 会打印 "open: <系统错误信息>" */
        return -1;
    }

    /* 枚举所有像素格式及其支持的分辨率 */
    enum_format_with_sizes(fd);

    close(fd);
    return 0;
}

结果

root@wyl:~/c-std/for-linux/099_other/v4l2# ./002_v4l2_fmt_pix
====== 枚举像素格式及支持的分辨率 ======
  [0] 0x56595559  YUYV 4:2:2
       分辨率:
          [0] 96x200
          [1] 200x96
          [2] 8x8448
          [3] 8448x8
          [4] 4x2321
          [5] 96x100
          [6] 4x2637
          [7] 4x14733
          [8] 4x5188
          [9] 5188x4
          [10] 8x8642
          [11] 8642x8
          [12] 96x96
          [13] 240x240
/*
 * V4L2 三级枚举示例:像素格式 -> 分辨率 -> 帧率 (FPS)
 *
 * 演示三个 ioctl 的嵌套枚举:
 *   VIDIOC_ENUM_FMT         枚举像素格式
 *   VIDIOC_ENUM_FRAMESIZES  枚举每种格式的分辨率
 *   VIDIOC_ENUM_FRAMEINTERVALS 枚举每个分辨率的帧间隔(帧率)
 *
 * 帧率 = denominator / numerator(例如 1/30 就是 30fps)
 */

#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static char *dev = "/dev/video0";

/*
 * enum_frameintervals - 枚举指定格式+分辨率下的帧间隔(帧率)
 * @fd:        设备描述符
 * @fourcc:    像素格式 FourCC
 * @w:         宽度
 * @h:         高度
 *
 * 使用 VIDIOC_ENUM_FRAMEINTERVALS ioctl。
 * v4l2_frmivalenum.type 也有三种类型,用法与帧尺寸枚举类似:
 *
 *   V4L2_FRMIVAL_TYPE_DISCRETE
 *     离散帧率列表,需递增 index 多次调用。每次返回一个分数 (numerator/denominator),
 *     帧率 = denominator / numerator (单位 fps)。
 *     例如 numerator=1, denominator=30 表示 30fps。
 *
 *   V4L2_FRMIVAL_TYPE_STEPWISE
 *     步进范围,一次调用获得 min~max/step 的帧率范围。
 *
 *   V4L2_FRMIVAL_TYPE_CONTINUOUS
 *     连续范围,一次调用获得 min~max 的帧率范围,区间内任意帧率都支持。
 */
static void enum_frameintervals(int fd, unsigned int fourcc, int w, int h)
{
    struct v4l2_frmivalenum frmi;

    for (int index = 0; ; index++) {
        memset(&frmi, 0, sizeof(frmi));
        frmi.index        = index;
        frmi.pixel_format = fourcc;
        frmi.width        = w;
        frmi.height       = h;

        if (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &frmi) < 0)
            break;

        switch (frmi.type) {
        case V4L2_FRMIVAL_TYPE_DISCRETE:
            /* 离散帧率:转换分数为浮点 fps */
            printf("              [%d] %.2f fps\n", index,
                   (float)frmi.discrete.denominator / frmi.discrete.numerator);
            break;
        case V4L2_FRMIVAL_TYPE_STEPWISE:
            /* 步进帧率:min ~ max fps,步进 step fps */
            printf("              [%d] %.2f ~ %.2f fps  step %.3f\n", index,
                   (float)frmi.stepwise.min.denominator / frmi.stepwise.min.numerator,
                   (float)frmi.stepwise.max.denominator / frmi.stepwise.max.numerator,
                   (float)frmi.stepwise.step.denominator / frmi.stepwise.step.numerator);
            return;
        case V4L2_FRMIVAL_TYPE_CONTINUOUS:
            /* 连续帧率:min ~ max fps */
            printf("              [%d] %.2f ~ %.2f fps  (continuous)\n", index,
                   (float)frmi.stepwise.min.denominator / frmi.stepwise.min.numerator,
                   (float)frmi.stepwise.max.denominator / frmi.stepwise.max.numerator);
            return;
        }
    }
}

/*
 * enum_framesizes - 枚举像素格式下的分辨率及对应帧率
 * @fd:        设备描述符
 * @fourcc:    像素格式 FourCC
 */
static void enum_framesizes(int fd, unsigned int fourcc)
{
    struct v4l2_frmsizeenum frmsize;

    /* 先获知帧尺寸类型(DISCRETE/STEPWISE/CONTINUOUS) */
    memset(&frmsize, 0, sizeof(frmsize));
    frmsize.pixel_format = fourcc;
    frmsize.index = 0;

    if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) < 0)
        return;

    switch (frmsize.type) {
    case V4L2_FRMSIZE_TYPE_DISCRETE:
        /* 离散分辨率:递增 index 枚举每一项 */
        for (int index = 0; ; index++) {
            memset(&frmsize, 0, sizeof(frmsize));
            frmsize.pixel_format = fourcc;
            frmsize.index = index;
            if (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &frmsize) < 0)
                break;
            printf("      [%d] %dx%d\n", index,
                   frmsize.discrete.width, frmsize.discrete.height);
            /* 进一步枚举该分辨率的帧率 */
            enum_frameintervals(fd, fourcc,
                                frmsize.discrete.width,
                                frmsize.discrete.height);
        }
        break;

    case V4L2_FRMSIZE_TYPE_STEPWISE:
        printf("      stepwise: %dx%d ~ %dx%d  step %dx%d\n",
               frmsize.stepwise.min_width,  frmsize.stepwise.min_height,
               frmsize.stepwise.max_width,  frmsize.stepwise.max_height,
               frmsize.stepwise.step_width, frmsize.stepwise.step_height);
        break;

    case V4L2_FRMSIZE_TYPE_CONTINUOUS:
        printf("      continuous: %dx%d ~ %dx%d\n",
               frmsize.stepwise.min_width,  frmsize.stepwise.min_height,
               frmsize.stepwise.max_width,  frmsize.stepwise.max_height);
        break;
    }
}

/*
 * enum_all - 三级枚举:格式 -> 分辨率 -> 帧率
 */
static void enum_all(int fd)
{
    struct v4l2_fmtdesc fmt;

    printf("====== 枚举像素格式/分辨率/帧率 ======\n");
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    for (fmt.index = 0; ; fmt.index++) {
        if (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) < 0)
            break;
        printf("  [%d] 0x%08x  %s\n",
               fmt.index, fmt.pixelformat, fmt.description);
        enum_framesizes(fd, fmt.pixelformat);
    }
    printf("\n");
}

int main(void)
{
    int fd = open(dev, O_RDWR);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    enum_all(fd);

    close(fd);
    return 0;
}

结果

root@wyl:~/c-std/for-linux/099_other/v4l2# ./002_v4l2_fmt_fps
====== 枚举像素格式/分辨率/帧率 ======
  [0] 0x56595559  YUYV 4:2:2
      [0] 96x200
              [0] 25.00 fps
      [1] 200x96
              [0] 25.00 fps
      [2] 8x8448
              [0] 25.00 fps
      [3] 8448x8
              [0] 25.00 fps
      [4] 4x2321
              [0] 25.00 fps
      [5] 96x100
              [0] 25.00 fps
      [6] 4x2637
              [0] 25.00 fps
      [7] 4x14733
              [0] 25.00 fps
      [8] 4x5188
              [0] 25.00 fps
      [9] 5188x4
              [0] 25.00 fps
      [10] 8x8642
              [0] 25.00 fps
      [11] 8642x8
              [0] 25.00 fps
      [12] 96x96
              [0] 25.00 fps
      [13] 240x240
              [0] 25.00 fps

V4L2 控件本质上是一个 key-value 配置接口,驱动暴露一组 id → value 的映射,用户层通过 ioctl 读写。

对应V4L2设备,以摄像头为例:

​ 有控件的说法。很多V4L2设备高度抽象了一些控件。直接拿来用就好。

​ 比如:亮度、对比度控件。

1. 数据类型(能存什么值)

类型说明例子
INTEGER整数,范围 min~max,步进 step亮度 0~255 step 1
BOOLEAN0/1自动白平衡开关
MENU索引 → 字符串电源频率 0=50Hz 1=60Hz
INTEGER_MENU索引 → 整数某驱动自定义模式 ID
BUTTON写入即触发一个动作执行一次白平衡
INTEGER6464位整数大范围计数器
STRING字符串设备序列号
BITMASK位掩码各功能使能位

2. 属性标志(能怎么用)

当前文件 print_ctrl_flags() 已经列的:

标志含义
DISABLED控件被禁用,不可用
READ_ONLY只读,只能查询不能设置
WRITE_ONLY只写(通常是触发类)
VOLATILE值会随硬件自动变化(如自动曝光时 actual exposure)
HAS_PAYLOAD携带复杂数据结构(H.264/HDR 元数据等)
EXECUTE_ON_WRITE写入即执行(BUTTON 类型必须有此标志)
MODIFY_LAYOUT改变此控件会影响图像布局(比如改变裁剪窗口)

枚举设备支持的控件

流程

  1. 配置
  2. 获取一帧
/* 初始化 V4L2:打开设备、设置 YUYV 格式、申请 mmap 缓冲区、开始流 */
int camera_init(const char *dev, int w, int h)
{
    struct v4l2_format fmt = {0};
    struct v4l2_requestbuffers req = {0};
    struct v4l2_buffer vbuf = {0};
    enum v4l2_buf_type type;

    cam_fd = open(dev, O_RDWR);
    if (cam_fd < 0) { perror("open"); return -1; }

    /* 协商 YUYV 格式 */
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width       = w;
    fmt.fmt.pix.height      = h;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
    fmt.fmt.pix.field       = V4L2_FIELD_NONE;
    if (ioctl(cam_fd, VIDIOC_S_FMT, &fmt) < 0) {
        perror("VIDIOC_S_FMT"); close(cam_fd); return -1;
    }

    img_w = fmt.fmt.pix.width;
    img_h = fmt.fmt.pix.height;
    printf("camera: %dx%d YUYV\n", img_w, img_h);

    /* 申请 V4L2 循环缓冲区(内存由驱动管理) */
    req.count  = BUF_NUM;
    req.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (ioctl(cam_fd, VIDIOC_REQBUFS, &req) < 0) {
        perror("VIDIOC_REQBUFS"); close(cam_fd); return -1;
    }

    for (int i = 0; i < BUF_NUM; i++) {
        memset(&vbuf, 0, sizeof(vbuf));
        vbuf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        vbuf.memory = V4L2_MEMORY_MMAP;
        vbuf.index  = i;
        if (ioctl(cam_fd, VIDIOC_QUERYBUF, &vbuf) < 0) {
            perror("VIDIOC_QUERYBUF"); close(cam_fd); return -1;
        }
        buffers[i].length = vbuf.length;
        buffers[i].start  = mmap(NULL, vbuf.length, PROT_READ | PROT_WRITE,
                                  MAP_SHARED, cam_fd, vbuf.m.offset);
        if (buffers[i].start == MAP_FAILED) {
            perror("mmap"); close(cam_fd); return -1;
        }
    }

    /* 所有缓冲入队,准备采集 */
    for (int i = 0; i < BUF_NUM; i++) {
        memset(&vbuf, 0, sizeof(vbuf));
        vbuf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        vbuf.memory = V4L2_MEMORY_MMAP;
        vbuf.index  = i;
        ioctl(cam_fd, VIDIOC_QBUF, &vbuf);
    }

    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(cam_fd, VIDIOC_STREAMON, &type) < 0) {
        perror("VIDIOC_STREAMON"); close(cam_fd); return -1;
    }

    return 0;
}
/*
 * 捕获一帧:DQBUF 取一帧 → 直接转换到 rgb_buf → QBUF 归还
 * 不经过中间缓冲,mmap 数据直转 ARGB8888
 */
int camera_capture_frame(uint8_t *rgb_buf, int *size)
{
    struct v4l2_buffer vbuf = {0};
    fd_set fds;
    struct timeval tv;

    FD_ZERO(&fds);
    FD_SET(cam_fd, &fds);
    tv.tv_sec  = 0;
    tv.tv_usec = 200000;

    int ret = select(cam_fd + 1, &fds, NULL, NULL, &tv);
    if (ret <= 0) return -1;

    vbuf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    vbuf.memory = V4L2_MEMORY_MMAP;
    if (ioctl(cam_fd, VIDIOC_DQBUF, &vbuf) < 0) return -1;

    /* 从 mmap 缓冲直接转换到输出缓冲,节省一次 memcpy */
    yuyv_to_argb8888(buffers[vbuf.index].start, rgb_buf, img_w, img_h);
    ioctl(cam_fd, VIDIOC_QBUF, &vbuf);

    if (size) *size = img_w * img_h * 4;
    return 0;
}
/*
 * YUYV → ARGB8888 转换(BT.601 近似)
 * 输入 4 字节 = 2 像素 (Y0 U Y1 V)
 * 输出每像素 4 字节 (B G R A)
 */
static void yuyv_to_argb8888(const uint8_t *src, uint8_t *dst, int w, int h)
{
    for (int i = 0; i < w * h / 2; i++) {
        int y0 = src[0], u = src[1] - 128, y1 = src[2], v = src[3] - 128;
        src += 4;

        int r = (298 * y0 + 409 * v + 128) >> 8;
        int g = (298 * y0 - 100 * u - 208 * v + 128) >> 8;
        int b = (298 * y0 + 516 * u + 128) >> 8;
        dst[0] = (uint8_t)(b < 0 ? 0 : (b > 255 ? 255 : b));
        dst[1] = (uint8_t)(g < 0 ? 0 : (g > 255 ? 255 : g));
        dst[2] = (uint8_t)(r < 0 ? 0 : (r > 255 ? 255 : r));
        dst[3] = 0xFF;
        dst += 4;

        r = (298 * y1 + 409 * v + 128) >> 8;
        g = (298 * y1 - 100 * u - 208 * v + 128) >> 8;
        b = (298 * y1 + 516 * u + 128) >> 8;
        dst[0] = (uint8_t)(b < 0 ? 0 : (b > 255 ? 255 : b));
        dst[1] = (uint8_t)(g < 0 ? 0 : (g > 255 ? 255 : g));
        dst[2] = (uint8_t)(r < 0 ? 0 : (r > 255 ? 255 : r));
        dst[3] = 0xFF;
        dst += 4;
    }
}

V4L2 学习笔记(热成像专题)

设备信息

  • 设备: YXG309T 红外热成像传感器(识别为 uvcvideo HikCamera: UVC Camera
  • USB: ID 2bdf:0102 HIK HikCamera
  • UVC Extension Unit: Unit=10, 23个 Selector, 512字节
  • 分辨率: 256x196(固定,S_FMT 全部 EIO),实际热成像区 256x192

核心发现:YUYV = 16-bit 温度数据容器

YUYV 格式在这里不是视频格式,而是 16-bit 原始温度数据的传输容器。

  • 每 4 字节(Y0 U0 Y1 V0)实际是两个连续像素的 低字节 + 高字节
  • U/V 通道携带的是高字节数据,不是颜色信息
  • 同一份数据:映射伪彩色 = 热成像图,换算公式 = 温度值
  • 不需要额外的数据通道,不需要第二个设备节点

帧结构 (256 x 196 = 100352 bytes)

┌─────────────────────────────────────┐
│  行 0~191: 256x192 热成像像素数据    │ ← 16-bit LE, 每像素2字节
│  ┌─────────────────────────────┐    │
│  │  最热点 H: 红圈标记          │    │
│  │  最冷点 L: 蓝圈标记          │    │
│  │  中心 +: 十字准心            │    │
│  └─────────────────────────────┘    │
│  行 192: 元数据块 1                 │ ← 状态/配置信息
│  行 193: 元数据块 2                 │ ← 可能含 min/max/校准值
│  行 194~195: 填充                  │ ← 全 0
└─────────────────────────────────────┘

温度换算

T(°C) = raw_value / scale + offset

// 当前估算: scale=200, offset=0  (需校准)
// 实测范围: raw=4872~5369 → ~24.4°C~26.8°C

已完成示例

001 — 查询设备能力 VIDIOC_QUERYCAP

  • 文件: 001_v4l2_info.c
  • 打开 /dev/videoX,查询 v4l2_capability
  • 热成像设备显示为 uvcvideo 驱动(UVC 兼容模式)

002 — 格式枚举和协商

  • 文件: 002_v4l2_fmt.c
  • ENUM_FMT / ENUM_FRAMESIZES / G_FMT / TRY_FMT / S_FMT
  • 热成像设备 S_FMT 全部返回 EIO(固定格式),需用 G_FMT 获取

003 — 控件调节

  • 文件: 003_v4l2_ctrl.c
  • QUERYCTRL / QUERYMENU / G_CTRL / S_CTRL
  • 热成像设备 无任何 V4L2 控件(无亮度/对比度等)

004 — 输入/输出选择

  • 文件: 004_v4l2_input.c
  • ENUMINPUT / G_INPUT / S_INPUT / ENUMOUTPUT / ENUMSTD
  • 热成像设备通常只有 1 个输入源

005 — read() 方式采集

  • 文件: 005_v4l2_read.c
  • UVC 热成像设备 不支持 read(),需用 mmap

006 — mmap Streaming I/O ⭐

  • 文件: 006_v4l2_mmap.c
  • V4L2 核心流程:
    S_FMT → REQBUFS → QUERYBUF+mmap → QBUF×N → STREAMON
                                                ↕
                                      DQBUF → 处理 → QBUF 归还
                                                ↕
                                            循环采集
                                                ↓
                                      STREAMOFF → munmap → REQBUFS(0)
    
  • 实测: 4 缓冲区, ~25fps, 每帧 100352 bytes
  • 热成像设备的唯一采集方式

013 — 热成像温度数据提取

  • 文件: 013_v4l2_thermal.c
  • 从 mmap 帧中提取 256x192 的 16-bit 温度原始值
  • 输出: thermal_raw.pgm(16-bit), thermal_8bit.pgm(灰度图), thermal_temps.csv(温度矩阵)
  • 打印最热点/最冷点位置及温度

014 — Framebuffer 实时显示

  • 文件: 014_v4l2_fb.c
  • 实时热成像显示,火焰伪彩色
  • HUD 信息: 中心温度、最高温(红标)、最低温(蓝标)
  • 右侧色标条,中心十字准心
  • /dev/fb0 800x600 32bpp,2x 缩放

XU 自定义协议探测

  • Extension Unit ID=10,GUID: {a29e7641-de04-47e3-8b2b-f4341aff003b}
  • 23 个 control selector(1~23)
  • Selectors 1/2/3/5/10: 支持 GET_CUR + SET_CUR(可读写)
  • Selectors 4/6: 仅 GET_CUR(只读)
  • 可能用途: 分辨率切换、帧率控制、校准参数等
  • 文件: 007_check_xu.c

待完成(标准 V4L2)

007 — poll() 非阻塞采集

  • poll()select() 等待帧就绪
  • 结合 DQBUF 实现超时机制
  • 适合多路复用场景(同时采集视频 + 处理用户输入)

008 — userptr 模式

  • 应用程序自己分配内存,通过 USERPTR 传递给驱动
  • 对比 mmap:优缺点、适用场景
  • 了解 DMABUF(零拷贝共享)

009 — 裁剪和缩放 VIDIOC_SELECTION

  • VIDIOC_G_SELECTION / VIDIOC_S_SELECTION
  • target: CROP(裁剪)/ COMPOSE(缩放)
  • 原生分辨率下裁剪 ROI 再采集

010 — V4L2 Events

  • VIDIOC_SUBSCRIBE_EVENT — 订阅事件
  • VIDIOC_DQEVENT — 取出事件
  • 典型事件:失焦、断开连接、控件变化

011 — Memory-to-Memory 设备

  • M2M 设备模型,硬件编解码器(H264、JPEG)
  • 同时维护 capture 和 output 两个队列

012 — V4L2 subdev

  • v4l2-subdev 子设备接口,Sensor / ISP 级别控制
  • 通过 padpad-level format 操作

TODO / 可继续方向

  • 校准温度公式: 用已知温度物体测 raw 值,算出真实 scale/offset
  • 解析行 192~193 元数据: 找出校准参数、帧统计信息
  • 通过 XU 修改分辨率: 研究 unit=10 的 selector 协议
  • 007 — poll() 非阻塞采集
  • 008 — userptr 模式
  • 015 — 伪彩色增强: 自适应动态范围、多色表切换
  • 016 — 帧处理: 多帧降噪、温度追踪、ROI 测温

常用调试命令

v4l2-ctl -d /dev/video0 --all          # 查看完整设备信息
v4l2-ctl -d /dev/video0 --list-formats  # 列出格式
v4l2-ctl -d /dev/video0 --list-ctrls    # 列出控件
lsusb -d 2bdf:0102 -v                   # USB 描述符
cat /sys/class/video4linux/video0/name  # 设备名称

编译方法

gcc -o 00X_v4l2_xxx 00X_v4l2_xxx.c -lm
sudo ./00X_v4l2_xxx        # framebuffer 显示需要 sudo

信号

触发流程

  • 进入系统调用,用户态变成内核态
  • 是对应触发信号

内核态在返回用户态时,看到有信号标记,然后修改栈和指令寄存器让cpu去执行对应函数。

sleep(1)这是一个系统调用,进入内核后休眠。Ctrl+C 按下时:

1. 内核标记信号 pending
2. sleep 因为信号被中断,返回 EINTR
3. 内核在返回用户态之前,看到有 pending signal
4. 内核在用户栈上压入一个伪造的返回地址(指回原代码)
5. 修改栈/指令寄存器,让 CPU 执行 on_signal
6. on_signal 返回时,内核帮你 sigreturn,恢复原来的上下文
7. 代码继续跑 while(running) 检查条件

1

#include <stdio.h>
#include <signal.h>
#include <sys/time.h>
static volatile int running = 1; // static 文件作用域,volatile 编译器不会对它做任何优化
static void on_sig(int s)
{
    (void)s;
    printf("触发系统信号,信号类型为SIGINT或SIGTERM");
    running = 0;
}

int main(void)
{
    signal(SIGINT, on_sig); //绑定信号
    signal(SIGTERM, on_sig);//绑定信号
    while(running){
        printf("运行中-----");
        usleep(5000);
    }
}

SIGINT(终端 Ctrl+C)和 SIGTERM(kill 命令)捕获后将 running 置 0,主循环退出。

  • SIGUSR1SIGUSR2 这是两个预留的用户自定义信号,编号通常为 10 和 12(不同体系可能略有差异),你可以像处理 SIGINT 一样为它们注册处理函数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

static void my_handler(int sig) {
    printf("捕获到信号 %d\n", sig);
}

int main() {
    // 注册 SIGUSR1
    signal(SIGUSR1, my_handler);
    // 注册 SIGUSR2
    signal(SIGUSR2, my_handler);

    while (1) {
        printf("等待信号...\n");
        sleep(1);
    }
    return 0;
}

触发方式

kill(pid, SIGUSR1);   // 向 pid 进程发送 SIGUSR1
raise(SIGUSR1);			//向当前进程发送

#或者
kill -USR1 <进程ID> #向SIGUSR1发送信号
kill -10 <进程ID>   # 部分系统 SIGUSR1 编号为 10
  • 实时信号(Realtime Signals) 范围从 SIGRTMINSIGRTMAX(通常是 34~64),数量较多,支持队列化和按优先级递送,适合更复杂的自定义场景。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void rt_handler(int sig, siginfo_t *info, void *ctx) {
    (void)ctx;
    printf("收到实时信号 %d,携带数值: %d\n", sig, info->si_value.sival_int);
}

int main() {
    struct sigaction act;
    act.sa_sigaction = rt_handler;
    act.sa_flags = SA_SIGINFO;
    sigemptyset(&act.sa_mask);
    sigaction(SIGRTMIN, &act, NULL);

    union sigval val;
    // 连续发送 5 个信号,附带不同数值
    for (int i = 1; i <= 5; i++) {
        val.sival_int = i * 10;
        sigqueue(getpid(), SIGRTMIN, val);
    }

    sleep(3); // 等待异步处理完成
    return 0;
}

输出会依次打印 10, 20, 30, 40, 50(保证顺序,不会丢失)。

内核源码添加驱动

新增panel-lh24030c50.c文件

配置makefile

  • 路径:/kernel/drivers/gpu/drm/panel/Makefile

  • 新增一行

    obj-$(CONFIG_DRM_PANEL_LH24030C50) += panel-lh24030c50.o
    
    • 当内核配置系统检测到 CONFIG_DRM_PANEL_LH24030C50 被赋值为 y(内建)或 m(模块)时,该行会生效。
    • .c 文件编译为对应的 .o 目标文件,并最终链接进内核镜像(y)或生成独立的 .ko 内核模块文件(m)。

配置Kconfig

  • 路径:/kernel/drivers/gpu/drm/panel/Kconfig

  • 新增

    config DRM_PANEL_LH24030C50
    	tristate "LH24030C50 RGB panel"
    	depends on OF && SPI
    	depends on BACKLIGHT_CLASS_DEVICE
    	help
    	  Say Y here if you want to enable support for the LH24030C50
    	  RGB panel module driven by an ST7789V-compatible controller.
    
    • tristate:表示该选项支持三种状态——Y(内建)、M(模块)、N(不编译)。对应Makefile中的 obj-*
    • depends on OF && SPI依赖约束。只有启用了设备树(OF)和SPI总线支持时,该选项才会出现在菜单中。这防止了非硬件平台误选,也保证了编译时能引用到SPI子系统的头文件和符号。
    • depends on BACKLIGHT_CLASS_DEVICE:强制依赖背光类设备。因为面板需要背光调节功能,若内核未开启背光支持,该驱动编译会因找不到 struct backlight_device 等定义而失败。
    • help:给开发者或用户看的说明文本,描述该驱动适用的硬件型号。

内核配置

开启内核配置

kernel_defconfig

CONFIG_DRM_PANEL_LH24030C50=y #与上面makefile中的 CONFIG_DRM_PANEL_LH24030C50 一致
CONFIG_BACKLIGHT_CLASS_DEVICE=y
CONFIG_BACKLIGHT_GPIO=y

关闭内核配置

#CONFIG_FB_TFT=y
#CONFIG_DRM_PANEL_SITRONIX_ST7789V=y
#CONFIG_FB_TFT_ST7735R=y
#CONFIG_FB_TFT_ST7789V=y

也许要修改

一些fragment

rv1106-evb.config

CONFIG_BACKLIGHT_GPIO=y

频率过高

rv1106g-luckfox-pico-pro-max.dts

/**********CRU**********/
&cru {
	assigned-clocks = <&cru 3>;
	assigned-clock-rates = <216000000>;
};

aa_rv1106-lcd.dtsi

&vop {
	assigned-clocks = <&cru 201>;
	assigned-clock-parents = <&cru 3>;
	status = "okay";
};
timing0: panel-timing {
	clock-frequency = <7000000>;
	hactive = <240>;
	vactive = <320>;
	hfront-porch = <1>;
	hback-porch = <20>;
	hsync-len = <10>;
	vfront-porch = <8>;
	vback-porch = <2>;
	vsync-len = <6>;
	hsync-active = <0>;
	vsync-active = <0>;
	de-active = <1>;
	pixelclk-active = <0>;
};

rv1106-luckfox-pico-pro-max-ipc.dtsi

注释
/*****************************PINCTRL********************************/
// SPI
// &spi0 {
// 	pinctrl-0 = <&spi0m0_clk &spi0m0_miso &spi0m0_mosi &spi0m0_cs0>;
// 	#address-cells = <1>;
// 	#size-cells = <0>;
// 	spidev@0 {
// 	compatible = "rockchip,spidev";
// 		spi-max-frequency = <50000000>;
// 		reg = <0>;
// 	};

// 	fbtft@0 {
// 		compatible = "sitronix,st7789v";
// 		reg = <0>;
// 		spi-max-frequency = <20000000>;
// 		fps = <30>;
// 		buswidth = <8>;
// 		debug = <0x7>;
// 		led-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_HIGH>;//BL
// 		dc-gpios = <&gpio2 RK_PB1 GPIO_ACTIVE_HIGH>;//DC
// 		reset-gpios = <&gpio1 RK_PC3 GPIO_ACTIVE_LOW>;//RES
// 	};
// };
// SPDX-License-Identifier: GPL-2.0-only
/*
 * LH24030C50 RGB panel driver based on the ST7789V controller.
 */

#include <linux/delay.h>
#include <linux/media-bus-format.h>
#include <linux/of.h>
#include <video/display_timing.h>
#include <video/videomode.h>
#include <video/of_videomode.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/regulator/consumer.h>
#include <linux/spi/spi.h>

#include <video/mipi_display.h>

#include <drm/drm_device.h>
#include <drm/drm_modes.h>
#include <drm/drm_panel.h>

#define ST7789V_COLMOD_RGB_FMT_18BITS		(6 << 4)
#define ST7789V_COLMOD_CTRL_FMT_18BITS		(6 << 0)

#define ST7789V_RAMCTRL_CMD		0xb0
#define ST7789V_RAMCTRL_RM_RGB			BIT(4)
#define ST7789V_RAMCTRL_DM_RGB			BIT(0)
#define ST7789V_RAMCTRL_MAGIC			(3 << 6)
#define ST7789V_RAMCTRL_EPF(n)			(((n) & 3) << 4)

#define ST7789V_RGBCTRL_CMD		0xb1
#define ST7789V_RGBCTRL_WO			BIT(7)
#define ST7789V_RGBCTRL_RCM(n)			(((n) & 3) << 5)
#define ST7789V_RGBCTRL_VSYNC_HIGH		BIT(3)
#define ST7789V_RGBCTRL_HSYNC_HIGH		BIT(2)
#define ST7789V_RGBCTRL_PCLK_HIGH		BIT(1)
#define ST7789V_RGBCTRL_DE_LOW			BIT(0)
#define ST7789V_RGBCTRL_VBP(n)			((n) & 0x7f)
#define ST7789V_RGBCTRL_HBP(n)			((n) & 0x1f)

#define ST7789V_PORCTRL_CMD		0xb2
#define ST7789V_PORCTRL_IDLE_BP(n)		(((n) & 0xf) << 4)
#define ST7789V_PORCTRL_IDLE_FP(n)		((n) & 0xf)
#define ST7789V_PORCTRL_PARTIAL_BP(n)		(((n) & 0xf) << 4)
#define ST7789V_PORCTRL_PARTIAL_FP(n)		((n) & 0xf)

#define ST7789V_GCTRL_CMD		0xb7
#define ST7789V_GCTRL_VGHS(n)			(((n) & 7) << 4)
#define ST7789V_GCTRL_VGLS(n)			((n) & 7)

#define ST7789V_VCOMS_CMD		0xbb

#define ST7789V_LCMCTRL_CMD		0xc0
#define ST7789V_LCMCTRL_XBGR			BIT(5)
#define ST7789V_LCMCTRL_XMX			BIT(3)
#define ST7789V_LCMCTRL_XMH			BIT(2)

#define ST7789V_VDVVRHEN_CMD		0xc2
#define ST7789V_VDVVRHEN_CMDEN			BIT(0)

#define ST7789V_VRHS_CMD		0xc3

#define ST7789V_VDVS_CMD		0xc4

#define ST7789V_FRCTRL2_CMD		0xc6

#define ST7789V_PWCTRL1_CMD		0xd0
#define ST7789V_PWCTRL1_MAGIC			0xa4
#define ST7789V_PWCTRL1_AVDD(n)			(((n) & 3) << 6)
#define ST7789V_PWCTRL1_AVCL(n)			(((n) & 3) << 4)
#define ST7789V_PWCTRL1_VDS(n)			((n) & 3)

#define ST7789V_PVGAMCTRL_CMD		0xe0
#define ST7789V_PVGAMCTRL_JP0(n)		(((n) & 3) << 4)
#define ST7789V_PVGAMCTRL_JP1(n)		(((n) & 3) << 4)
#define ST7789V_PVGAMCTRL_VP0(n)		((n) & 0xf)
#define ST7789V_PVGAMCTRL_VP1(n)		((n) & 0x3f)
#define ST7789V_PVGAMCTRL_VP2(n)		((n) & 0x3f)
#define ST7789V_PVGAMCTRL_VP4(n)		((n) & 0x1f)
#define ST7789V_PVGAMCTRL_VP6(n)		((n) & 0x1f)
#define ST7789V_PVGAMCTRL_VP13(n)		((n) & 0xf)
#define ST7789V_PVGAMCTRL_VP20(n)		((n) & 0x7f)
#define ST7789V_PVGAMCTRL_VP27(n)		((n) & 7)
#define ST7789V_PVGAMCTRL_VP36(n)		(((n) & 7) << 4)
#define ST7789V_PVGAMCTRL_VP43(n)		((n) & 0x7f)
#define ST7789V_PVGAMCTRL_VP50(n)		((n) & 0xf)
#define ST7789V_PVGAMCTRL_VP57(n)		((n) & 0x1f)
#define ST7789V_PVGAMCTRL_VP59(n)		((n) & 0x1f)
#define ST7789V_PVGAMCTRL_VP61(n)		((n) & 0x3f)
#define ST7789V_PVGAMCTRL_VP62(n)		((n) & 0x3f)
#define ST7789V_PVGAMCTRL_VP63(n)		(((n) & 0xf) << 4)

#define ST7789V_NVGAMCTRL_CMD		0xe1
#define ST7789V_NVGAMCTRL_JN0(n)		(((n) & 3) << 4)
#define ST7789V_NVGAMCTRL_JN1(n)		(((n) & 3) << 4)
#define ST7789V_NVGAMCTRL_VN0(n)		((n) & 0xf)
#define ST7789V_NVGAMCTRL_VN1(n)		((n) & 0x3f)
#define ST7789V_NVGAMCTRL_VN2(n)		((n) & 0x3f)
#define ST7789V_NVGAMCTRL_VN4(n)		((n) & 0x1f)
#define ST7789V_NVGAMCTRL_VN6(n)		((n) & 0x1f)
#define ST7789V_NVGAMCTRL_VN13(n)		((n) & 0xf)
#define ST7789V_NVGAMCTRL_VN20(n)		((n) & 0x7f)
#define ST7789V_NVGAMCTRL_VN27(n)		((n) & 7)
#define ST7789V_NVGAMCTRL_VN36(n)		(((n) & 7) << 4)
#define ST7789V_NVGAMCTRL_VN43(n)		((n) & 0x7f)
#define ST7789V_NVGAMCTRL_VN50(n)		((n) & 0xf)
#define ST7789V_NVGAMCTRL_VN57(n)		((n) & 0x1f)
#define ST7789V_NVGAMCTRL_VN59(n)		((n) & 0x1f)
#define ST7789V_NVGAMCTRL_VN61(n)		((n) & 0x3f)
#define ST7789V_NVGAMCTRL_VN62(n)		((n) & 0x3f)
#define ST7789V_NVGAMCTRL_VN63(n)		(((n) & 0xf) << 4)

#define ST7789V_TEST(val, func)			\
	do {					\
		if ((val = (func)))		\
			return val;		\
	} while (0)

struct st7789v {
	struct drm_panel panel;
	struct spi_device *spi;
	struct gpio_desc *reset;
	struct gpio_desc *sclk;
	struct gpio_desc *mosi;
	struct gpio_desc *cs;
	struct regulator *power;
};

enum st7789v_prefix {
	ST7789V_COMMAND = 0,
	ST7789V_DATA = 1,
};

static inline struct st7789v *panel_to_st7789v(struct drm_panel *panel)
{
	return container_of(panel, struct st7789v, panel);
}

static void st7789v_get_rgbctrl_from_dt(struct device *dev, u8 *ctrl,
					u8 *vbp, u8 *hbp)
{
	struct videomode vm;

	*ctrl = ST7789V_RGBCTRL_RCM(2);
	*vbp = 4;
	*hbp = 10;

	if (of_get_videomode(dev->of_node, &vm, 0))
		return;

	if (vm.flags & DISPLAY_FLAGS_VSYNC_HIGH)
		*ctrl |= ST7789V_RGBCTRL_VSYNC_HIGH;
	if (vm.flags & DISPLAY_FLAGS_HSYNC_HIGH)
		*ctrl |= ST7789V_RGBCTRL_HSYNC_HIGH;
	if (vm.flags & DISPLAY_FLAGS_DE_LOW)
		*ctrl |= ST7789V_RGBCTRL_DE_LOW;
	if (vm.flags & DISPLAY_FLAGS_PIXDATA_NEGEDGE)
		*ctrl |= ST7789V_RGBCTRL_PCLK_HIGH;

	*vbp = vm.vback_porch;
	*hbp = vm.hback_porch;
}

static void st7789v_get_active_area_from_dt(struct device *dev, u16 *width,
					    u16 *height)
{
	struct videomode vm;

	*width = 240;
	*height = 320;

	if (of_get_videomode(dev->of_node, &vm, 0))
		return;

	*width = vm.hactive;
	*height = vm.vactive;
}

static u32 st7789v_get_bus_flags_from_dt(struct device *dev)
{
	struct videomode vm;
	u32 bus_flags = DRM_BUS_FLAG_DE_HIGH |
			DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE;

	if (of_get_videomode(dev->of_node, &vm, 0))
		return bus_flags;

	bus_flags = 0;

	if (vm.flags & DISPLAY_FLAGS_DE_LOW)
		bus_flags |= DRM_BUS_FLAG_DE_LOW;
	else
		bus_flags |= DRM_BUS_FLAG_DE_HIGH;

	if (vm.flags & DISPLAY_FLAGS_PIXDATA_NEGEDGE)
		bus_flags |= DRM_BUS_FLAG_PIXDATA_SAMPLE_NEGEDGE;
	else
		bus_flags |= DRM_BUS_FLAG_PIXDATA_SAMPLE_POSEDGE;

	return bus_flags;
}

static void st7789v_spi_bitbang(struct st7789v *ctx, int dc, u8 data)
{
	int i;

	gpiod_set_value_cansleep(ctx->cs, 0);
	udelay(1);
	gpiod_set_value_cansleep(ctx->sclk, 0);
	gpiod_set_value_cansleep(ctx->mosi, dc ? 1 : 0);
	udelay(2);
	gpiod_set_value_cansleep(ctx->sclk, 1);
	udelay(2);
	for (i = 0; i < 8; i++) {
		gpiod_set_value_cansleep(ctx->sclk, 0);
		gpiod_set_value_cansleep(ctx->mosi, (data >> (7 - i)) & 1);
		udelay(2);
		gpiod_set_value_cansleep(ctx->sclk, 1);
		udelay(2);
	}
	gpiod_set_value_cansleep(ctx->cs, 1);
	udelay(1);
}

static int st7789v_write_command(struct st7789v *ctx, u8 cmd)
{
	st7789v_spi_bitbang(ctx, 0, cmd);
	return 0;
}

static int st7789v_write_data(struct st7789v *ctx, u8 data)
{
	st7789v_spi_bitbang(ctx, 1, data);
	return 0;
}

static const struct drm_display_mode default_mode = {
	.clock = 7000,
	.hdisplay = 240,
	.hsync_start = 240 + 10,
	.hsync_end = 240 + 10 + 10,
	.htotal = 240 + 10 + 10 + 38,
	.vdisplay = 320,
	.vsync_start = 320 + 4,
	.vsync_end = 320 + 4 + 4,
	.vtotal = 320 + 4 + 4 + 8,
	.flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC,
};

static int st7789v_get_modes(struct drm_panel *panel,
			     struct drm_connector *connector)
{
	struct drm_display_mode *mode;
	const u32 bus_format = MEDIA_BUS_FMT_RGB666_1X18;
	const struct drm_display_mode *src_mode = &default_mode;
	struct videomode vm;

	if (!of_get_videomode(panel->dev->of_node, &vm, 0)) {
		struct drm_display_mode *dt_mode;

		dev_info(panel->dev,
			 "dt mode hact=%u vact=%u hfp=%u hbp=%u hsync=%u vfp=%u vbp=%u vsync=%u flags=0x%x\n",
			 vm.hactive, vm.vactive,
			 vm.hfront_porch, vm.hback_porch, vm.hsync_len,
			 vm.vfront_porch, vm.vback_porch, vm.vsync_len,
			 vm.flags);

		dt_mode = drm_mode_create(connector->dev);
		if (dt_mode) {
			drm_display_mode_from_videomode(&vm, dt_mode);
			dt_mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
			src_mode = dt_mode;
		}
	} else {
		dev_info(panel->dev,
			 "using default mode hdisplay=%u vdisplay=%u hsync_start=%u hsync_end=%u htotal=%u vsync_start=%u vsync_end=%u vtotal=%u\n",
			 src_mode->hdisplay, src_mode->vdisplay,
			 src_mode->hsync_start, src_mode->hsync_end,
			 src_mode->htotal, src_mode->vsync_start,
			 src_mode->vsync_end, src_mode->vtotal);
	}

	mode = drm_mode_duplicate(connector->dev, src_mode);
	if (!mode) {
		dev_err(panel->dev, "failed to add mode %ux%ux@%u\n",
			src_mode->hdisplay, src_mode->vdisplay,
			drm_mode_vrefresh(src_mode));
		return -ENOMEM;
	}

	drm_mode_set_name(mode);

	mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
	drm_mode_probed_add(connector, mode);

	connector->display_info.width_mm = 43;
	connector->display_info.height_mm = 57;
	drm_display_info_set_bus_formats(&connector->display_info,
					  &bus_format, 1);
	connector->display_info.bus_flags =
		st7789v_get_bus_flags_from_dt(panel->dev);
	dev_info(panel->dev, "connector bus_flags=0x%x bus_format=0x%x\n",
		 connector->display_info.bus_flags, bus_format);

	return 1;
}

static int st7789v_prepare(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);
	u16 width, height;
	u8 rgbctrl, vbp, hbp;
	int ret;

	ret = regulator_enable(ctx->power);
	if (ret)
		return ret;

	gpiod_set_value(ctx->reset, 1);
	msleep(10);
	gpiod_set_value(ctx->reset, 0);
	msleep(150);

	ST7789V_TEST(ret, st7789v_write_command(ctx, MIPI_DCS_EXIT_SLEEP_MODE));

	/* We need to wait 120ms after a sleep out command */
	msleep(120);

	ST7789V_TEST(ret, st7789v_write_command(ctx,
						MIPI_DCS_SET_ADDRESS_MODE));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));

	ST7789V_TEST(ret, st7789v_write_command(ctx,
						MIPI_DCS_SET_PIXEL_FORMAT));
	ST7789V_TEST(ret, st7789v_write_data(ctx, MIPI_DCS_PIXEL_FMT_18BIT));

	st7789v_get_active_area_from_dt(panel->dev, &width, &height);
	dev_info(panel->dev, "active area width=%u height=%u\n", width, height);

	ST7789V_TEST(ret, st7789v_write_command(ctx,
						MIPI_DCS_SET_COLUMN_ADDRESS));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));
	ST7789V_TEST(ret, st7789v_write_data(ctx, (width - 1) >> 8));
	ST7789V_TEST(ret, st7789v_write_data(ctx, (width - 1) & 0xff));

	ST7789V_TEST(ret, st7789v_write_command(ctx,
						MIPI_DCS_SET_PAGE_ADDRESS));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));
	ST7789V_TEST(ret, st7789v_write_data(ctx, (height - 1) >> 8));
	ST7789V_TEST(ret, st7789v_write_data(ctx, (height - 1) & 0xff));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_PORCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xc));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xc));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x33));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x33));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_GCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x35));

	ST7789V_TEST(ret, st7789v_write_command(ctx, 0xb6));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x20));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_VCOMS_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x2b));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_LCMCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x2c));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_VDVVRHEN_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x01));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_VRHS_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x11));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_VDVS_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x20));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_FRCTRL2_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xf));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_PWCTRL1_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xa4));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xa1));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_PVGAMCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xd0));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x06));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x09));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x0b));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x2a));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x3c));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x55));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x4b));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x08));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x16));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x14));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x19));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x20));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_NVGAMCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0xd0));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x06));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x09));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x0b));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x29));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x36));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x54));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x4b));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x0d));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x16));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x14));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x21));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x20));

	ST7789V_TEST(ret, st7789v_write_command(ctx, MIPI_DCS_ENTER_INVERT_MODE));

	st7789v_get_rgbctrl_from_dt(panel->dev, &rgbctrl, &vbp, &hbp);
	dev_info(panel->dev,
		 "init regs madctl=0x%02x colmod=0x%02x ramctrl=0x%02x,0x%02x rgbctrl=0x%02x,0x%02x,0x%02x\n",
		 0x00, MIPI_DCS_PIXEL_FMT_18BIT, 0x11, 0x00,
		 rgbctrl, vbp, hbp);

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_RAMCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x11));
	ST7789V_TEST(ret, st7789v_write_data(ctx, 0x00));

	ST7789V_TEST(ret, st7789v_write_command(ctx, ST7789V_RGBCTRL_CMD));
	ST7789V_TEST(ret, st7789v_write_data(ctx, rgbctrl));
	ST7789V_TEST(ret, st7789v_write_data(ctx, ST7789V_RGBCTRL_VBP(vbp)));
	ST7789V_TEST(ret, st7789v_write_data(ctx, ST7789V_RGBCTRL_HBP(hbp)));

	return 0;
}

static int st7789v_enable(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);
	int ret;

	ST7789V_TEST(ret, st7789v_write_command(ctx, MIPI_DCS_SET_DISPLAY_ON));
	msleep(50);

	return 0;
}

static int st7789v_disable(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);
	int ret;

	ST7789V_TEST(ret, st7789v_write_command(ctx, MIPI_DCS_SET_DISPLAY_OFF));

	return 0;
}

static int st7789v_unprepare(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);
	int ret;

	ST7789V_TEST(ret, st7789v_write_command(ctx, MIPI_DCS_ENTER_SLEEP_MODE));

	regulator_disable(ctx->power);

	return 0;
}

static const struct drm_panel_funcs st7789v_drm_funcs = {
	.disable	= st7789v_disable,
	.enable		= st7789v_enable,
	.get_modes	= st7789v_get_modes,
	.prepare	= st7789v_prepare,
	.unprepare	= st7789v_unprepare,
};

static int st7789v_probe(struct spi_device *spi)
{
	struct st7789v *ctx;
	int ret;

	ctx = devm_kzalloc(&spi->dev, sizeof(*ctx), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;

	spi_set_drvdata(spi, ctx);
	ctx->spi = spi;

	drm_panel_init(&ctx->panel, &spi->dev, &st7789v_drm_funcs,
		       DRM_MODE_CONNECTOR_DPI);

	ctx->power = devm_regulator_get(&spi->dev, "power");
	if (IS_ERR(ctx->power))
		return PTR_ERR(ctx->power);

	ctx->reset = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_LOW);
	if (IS_ERR(ctx->reset)) {
		dev_err(&spi->dev, "Couldn't get our reset line\n");
		return PTR_ERR(ctx->reset);
	}

	ctx->sclk = devm_gpiod_get(&spi->dev, "spi-scl", GPIOD_OUT_LOW);
	if (IS_ERR(ctx->sclk))
		return PTR_ERR(ctx->sclk);
	ctx->mosi = devm_gpiod_get(&spi->dev, "spi-sdi", GPIOD_OUT_LOW);
	if (IS_ERR(ctx->mosi))
		return PTR_ERR(ctx->mosi);
	ctx->cs = devm_gpiod_get(&spi->dev, "spi-cs", GPIOD_OUT_HIGH);
	if (IS_ERR(ctx->cs))
		return PTR_ERR(ctx->cs);

	ret = drm_panel_of_backlight(&ctx->panel);
	if (ret)
		return ret;

	drm_panel_add(&ctx->panel);

	return 0;
}

static int st7789v_remove(struct spi_device *spi)
{
	struct st7789v *ctx = spi_get_drvdata(spi);

	drm_panel_remove(&ctx->panel);

	return 0;
}

static const struct of_device_id st7789v_of_match[] = {
	{ .compatible = "lh,lh24030c50" },
	{ }
};
MODULE_DEVICE_TABLE(of, st7789v_of_match);

static struct spi_driver st7789v_driver = {
	.probe = st7789v_probe,
	.remove = st7789v_remove,
	.driver = {
		.name = "lh24030c50",
		.of_match_table = st7789v_of_match,
	},
};
module_spi_driver(st7789v_driver);

MODULE_AUTHOR("Maxime Ripard <maxime.ripard@free-electrons.com>");
MODULE_DESCRIPTION("LH24030C50 RGB LCD panel driver");
MODULE_LICENSE("GPL v2");

图像硬件加速

关于显示链路。

多线程

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void* print_message(void* arg) {
    (void*)arg;
    while(1)
    {
        printf("Hello from thread\n");
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t threads;
    pthread_create(&threads, NULL, print_message, NULL);
    while(1)
    {
        sleep(1);
        printf("main threads runing\n");
    }
    return 0;
}

创建一个线程:

extern int pthread_create (pthread_t *__restrict __newthread,

			const pthread_attr_t *__restrict __attr,

			void *(*__start_routine) (void *),

			void *__restrict __arg) __THROWNL __nonnull ((1, 3));
  • 创建成功返回0,否则返回错误编号。
  • 新创建的线程ID设置成__newthread指向的内存单元。
  • __attr用于定制各种不同的线程属性。
  • __start_routine现成从这个函数地址开始运行。这个函数

一、线程退出的 3 种方式

方式触发者本质资源回收
线程执行完函数返回线程自己隐式退出,返回值就是 return 的值必须 joindetach
pthread_exit()线程自己主动退出,可指定返回值必须 joindetach
pthread_cancel()其他线程请求取消(异步/延迟)必须 joindetach

⚠️ 无论哪种方式,线程结束后其资源(栈、TLS、内核结构)不会自动完全释放,除非被 joindetach

场景推荐做法
需要线程结果pthread_join()
不需要结果, fire-and-forgetpthread_detach()(创建后立即 detach)
需要中途终止线程pthread_cancel() + pthread_join(),但尽量用条件变量/标志位优雅退出
避免僵尸线程确保每个线程都被 joindetach

1. pthread_exit(void *retval)

关键点:

  • 只能由线程自己调用(你笔记里写了,这是对的)
  • retval 不能指向线程栈上的局部变量(线程退出后栈销毁,指针悬空)
  • 主线程调用 pthread_exit() 不会导致进程退出,只会导致主线程退出,其他线程继续运行
void *my_thread(void *arg)
{
    printf("线程执行---\n");
    int *result = malloc(sizeof(int));
    *result = 42;
    
    pthread_exit(result);  // 主动退出,返回 42
    // 或者直接用 return result; 效果一样
}

int main(void)
{
    pthread_t pthread;
    pthread_create(&pthread, NULL, my_thread, NULL);
    while (1)
    {
        /* code */
        sleep(1);
        printf("主线程---\n");
    }
    return 0;
}

2.pthread_join(pthread_t thread, void **retval)

作用:

  • 阻塞等待指定线程结束
  • 回收线程资源(栈、内核结构)
  • 获取线程返回值(通过 retval 输出参数)

⚠️ 坑:

  • 一个线程只能被 join 一次
  • 如果线程已经被 detach,再 join 会返回 EINVAL
  • join 也不 detach → 线程变成"僵尸线程",资源泄漏
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *my_thread(void *arg)
{
    sleep(3);
    printf("线程停止---\n");
}

int main(void)
{
    pthread_t pthread;
    pthread_create(&pthread,NULL,my_thread,NULL);
    void *result;
    pthread_join(pthread,result);
    printf("主线程\n");
    return 0;
}

3. pthread_detach(pthread_t thread)

作用:

  • 告诉系统:我不关心这个线程的返回值,它结束后自动回收资源
  • detach 后线程变成"分离状态",结束后立即释放资源,不可 join

适用场景:

  • 不需要知道线程结果(如后台守护线程、日志写入线程)
  • 避免忘记 join 导致资源泄漏

⚠️ 坑:

  • detach 后不能再 join
  • 如果线程返回了堆内存指针,detach 后没人收,会内存泄漏
void *my_thread(void *arg)
{
    printf("子线程运行---\n");
    return NULL;
}
int main(void)
{
    pthread_t pthread;
    pthread_create(&pthread,NULL,my_thread,NULL);
    printf("子线程开始运行---\n");
    pthread_detach(pthread);
    printf("不阻塞,继续执行主线程\n");
    sleep(1);
    printf("主线程结束--\n");
    return 0;
}

4. pthread_cancel(pthread_t thread)

关键点:

  • 只是"请求"取消,不是强制杀死
  • 目标线程必须开启取消机制才会响应(默认是开启的)
  • 取消是异步的,目标线程可能在**取消点(cancellation point)**才实际退出

⚠️ 大坑:

  • 异步取消可能导致资源泄漏(锁没释放、内存没 free)
  • 延迟取消更安全,但线程可能卡在非取消点的死循环里,永远退不出
  • 取消后必须 join 来回收资源!
void *my_thread(void *arg)
{
    while (1)
    {
        printf("子线程运行----\n");
        sleep(1);
    }
    return NULL;
}
int main(void)
{
    pthread_t pthread;
    pthread_create(&pthread,NULL,my_thread,NULL);
    printf("3s后请求取消子线程\n");
    sleep(3);
    pthread_cancel(pthread);
    printf("子线程已取消\n");
    sleep(3);
    printf("主线程终止\n");
    return 0;
}

摄像头驱动

MIPI = Mobile Industry Processor Interface,移动产业处理器接口联盟专门为手机、嵌入式、IoT、车载设计低功耗、高速差分串行接口,分很多子协议:

  • CSI:Camera Serial Interface(摄像头)
  • DSI:Display Serial Interface(屏幕)
  • I2C/I3C:低速控制
  • UniPro、CPHY、DigRF 等

MIPI CSI-2:目前主流摄像头传输标准。

MIPI CSI-2 硬件分层(分两部分:DPHY / CPHY)

  1. D-PHY:2 线差分(P/N),最高~2.5Gbps/lane,低成本,绝大多数嵌入式摄像头(GC2145/OV2640/IMX 系列)都用,RV1106 全是 DPHY。
  2. C-PHY:3 线三相信号,速率更高、布线复杂,高端车载 / 手机主摄。
Sensor → MIPI CSI-2差分线 → csi2_dphy1(模拟PHY)
→ mipi1_csi2(CSI2协议解串器)
→ rkcif(图像采集单元)
→ rkisp(图像ISP,降噪、白平衡、曝光)
→ 应用层 /dev/video0

MIPI CSI-3

M-PHY

sensor:mos → ADC → csi → DPHY-TX

SOC:DCPHY-RX → MIPI-CSI → RKCIF → ISP → DRM

硬件层

	MIPI CSI-2 Sensor传感器						  MIPI协议	控制lane发送
感光传感器--> ADC转换 --> Bayer RAW像素(RAW/RGB) --> CSI2协议封装 --> DPHY --> MIPI线 -->SOC

SOC层

		   接收lane
MIPI线 -->  DPHY --> 各种转换 --> CSI2 HOST --> VICAP / CIF --> 各种方式 --> ISP

DPHY:对应设备树 csi2_dphy0,csi2_dphy1,csi2_dphy2

CSI2 HOST:对应设备树 mipi1_csi2,mipi2_csi2

VICAP / CIF:对应设备树 rkcif_mipi_lvds1

v4l2驱动

	V4L2框架		videobuf2内存管理		video设备节点
DMA中断通知 --> 缓冲队列入队/出队 --> /dev/videoX节点 --> 用户空间系统调用

用户层

	系统调用API		应用框架		业务场景
open/ioctl/mmap --> GStreamer/FFmpeg --> 显示画面/编码存储/RTSP推流/AI推理

1

第一部分:图1(端到端数据流全景图)逐级详解

这张图展示了从“光信号”到“应用画面”的物理与逻辑路径。

1. 摄像头硬件端(Sensor Board)

  • 光信号 -> CMOS感光阵列 & ADC:外界光线打到CMOS传感器上,每个像素点根据接收光强产生模拟电压信号。**ADC(模数转换器)**立即将这些模拟信号转换为数字电平,形成原始的Bayer格式数字数据(每个像素点只包含R/G/B中的一种颜色分量)。
  • RAW Bayer 像素数据:这是ISP(图像信号处理器)处理的“原材料”,也是最原始的无损数据,体积较大。
  • MIPI CSI-2 打包器:传感器内部的逻辑电路按照MIPI联盟定义的CSI-2协议,将RAW数据封装成数据包。具体分为**长包(Long Packet)承载像素负载,和短包(Short Packet)**承载帧起始(FS)、帧结束(FE)、行起始(LS)等同步信号。
  • D-PHY 发送器:将打包好的并行数据转换为高速差分串行信号(一般包含1对时钟线和多对数据线,如2-lane或4-lane),通过FPC排线传输给主控SoC。

2. SoC硬件端(接收与路由)

  • D-PHY 接收器:SoC端对应的物理层接口,负责锁定时钟信号,并将高速串行数据重新解串为并行数据。这一步骤如果出现信号完整性(SI)问题,会导致数据错位或丢失。
  • CSI-2 Host 解包器:解析MIPI协议,剥离包头,将有效的像素负载提取出来,并根据虚拟通道(Virtual Channel)ID将数据分发到不同的缓冲区。
  • VICAP(视频捕获控制器):这是RK平台承上启下的核心硬件模块。它负责将Host解包后的数据进行格式重整(如数据对齐、位宽调整),并且内置路由逻辑。它决定了数据是直接通过DMA搬运到内存(Bypass模式),还是送入ISP进行图像增强(ISP路径)。
  • 数据路由选择(关键分叉点)
    • 直通路径(Bypass):适用于AI识别或深度感知场景。VICAP直接将RAW数据或Sensor输出的YUV数据通过**DMA(直接内存访问)**搬运到DDR内存中,应用程序直接拿原始数据做推理。此路径延时最低。
    • ISP处理路径:VICAP将数据流转发给ISP(图像信号处理器)。ISP硬件依次执行**黑电平校正(BLC)、镜头阴影校正(LSC)、去马赛克(Demosaic,将Bayer插值为全彩)、自动白平衡(AWB)、色彩转换(CCM)、Gamma校正、锐化、降噪(NR)以及宽动态(WDR)**等复杂运算。
  • DMA 写入内存:无论是否经过ISP,最终处理完的数据帧都会由DMA控制器写入预先在内存中申请好的物理地址(即videobuf2的缓冲区)。写入完成后,DMA会触发硬件中断通知CPU。

3. Linux内核 V4L2 驱动层(软件调度)

  • videobuf2 缓冲池管理:这是V4L2框架的内存管理核心。它在内核空间维护一个缓冲区队列,通过mmap(内存映射)、DMABUF(跨设备共享)或Userptr(用户指针)三种模式与用户空间交互。DMA将数据写入后,缓冲区状态会从VB2_BUF_STATE_DONE流转为VB2_BUF_STATE_QUEUED,等待用户取走。
  • video_device 设备节点:最终生成用户空间可见的 /dev/videoX 节点。应用程序通过open()打开该节点,通过ioctl(如VIDIOC_QBUF入队、VIDIOC_DQBUF出队)来获得图像帧。
  • Media Controller & v4l2_subdev(控制面):图中虚线表示控制流。v4l2_subdev代表Sensor、ISP等子设备,驱动通过I2C总线下发寄存器配置给Sensor(如修改曝光值、帧率)。Media Controller则负责动态建立Pipeline(如Sensor -> CSI -> ISP -> videoX),确保数据流打通。

4. 用户应用程序层(最终使用)

  • 系统调用(ioctl / mmap / poll):应用通过标准Linux系统调用与 /dev/videoX 交互。pollepoll用于等待帧就绪事件,避免CPU空转。
  • 上层应用框架:实际开发中很少直接写ioctl,而是调用V4L2 Userpace API封装库,或者使用 GStreamerv4l2src 插件、FFmpegavdevice 模块。在Android系统中则对接Camera HAL
  • 显示/编码/推流:拿到内存中的图像数据后,应用将其送去显示(DRM)、硬件编码(如H.264/H.265)或通过网络协议(RTSP/WebRTC)推流。

2.png

第二部分:图2(软件分层架构图)逐层详解

这张图侧重于操作系统内部“代码模块”的层级依赖,适合开发驱动或调试内核问题时查阅。

1. 用户空间(User Space)

  • 应用/算法库:包括跑在ARM上的OpenCV推理程序、Qt显示界面等,它们不直接操作寄存器,只调用标准API。
  • Android Camera HAL / RkAiq库:特指Rockchip提供的3A(AE/AWB/AF)算法动态库。该库在用户空间运行,通过私有的/dev/isp_params节点将3A计算出的参数(如曝光时间、增益、白平衡色温)实时下发给内核驱动,形成闭环控制。

2. 内核空间 - V4L2框架层(Kernel Space)

  • 字符设备层:Linux将硬件抽象为文件,即 /dev/videoX/dev/v4l-subdevX。应用的所有操作入口都在这里。
  • V4L2核心层:这是标准Linux内核提供的通用框架代码。它负责解析ioctl命令码,并路由到对应的驱动回调函数(如.vidioc_streamon.vidioc_s_fmt),同时管理poll事件通知机制。
  • Media Controller(媒体控制器):属于V4L2框架的进阶特性。它不允许应用随意使能硬件模块,必须严格按拓扑图绑定。例如,使用命令 media-ctl -l "'rkisp-isp':0 -> 'rkisp-resizer':0 [1]" 来强制指定数据流下一跳。
  • videobuf2(VB2):独立于V4L2核心的内存管理层。它适配不同的内存类型(连续物理内存或不连续内存),并提供ops回调给驱动,用于通知驱动“该给这块缓冲区填数据了”。

3. 内核空间 - RK平台子设备驱动(Platform Driver)

  • Sensor驱动:通常遵循 v4l2_subdev 标准。核心工作是实现 .s_power(上电时序)、.s_fmt(设置输出分辨率/帧率)以及 s_ctrl(通过I2C写Sensor寄存器)。
  • RK CIF(Camera Interface)驱动:负责管理CSI-2 Host和VICAP硬件。它申请DMA通道,配置MIPI数据通道映射(lane映射),并处理MIPI错误中断(如ECC/CRC校验失败)。
  • RK ISP驱动:管理ISP硬件流水线。它不仅要配置硬件寄存器,还要处理ISP产生的中断(如帧结束中断、3A统计信息Ready中断),并将统计结果(Histogram/AE统计)通过特定的v4l2_event上报给用户空间的RkAiq库。

4. 硬件寄存器层(Hardware Layer)

  • 这是物理上的硬件单元。驱动层通过 writel() / readl() 函数向这些硬件单元的寄存器地址写入控制值(如启动传输、配置DMA地址)。注意:ISP驱动的参数配置和CIF驱动的启动时机必须严格配合,否则会出现“DMA写到错误地址”导致系统内存被踩坏(Memory Corruption)的严重问题。

🔗 数据流关键节点对照总结(排查宝典)

为了帮你更好地将图文对照用于实际Debug,我把关键节点做了串联描述:

关键节点对应图中的模块成功标志(Debug点)常见故障表现
物理信号锁定D-PHY接收器驱动打印 lane ratehsync 成功信息驱动报错 Timeout waiting for PLL lock,无图像
协议解包成功CSI-2 Host/proc/interruptscif 中断计数增加硬件中断不增加,说明MIPI线序或电压不匹配
DMA搬运正确VICAP -> DDRcat /proc/kmsgDMA FIFO overflow 错误画面出现横向撕裂或绿色噪点
缓冲区队列流转videobuf2 -> /dev/videoX应用层poll返回POLLINDQBUFF不阻塞DQBUFF一直阻塞,检查缓冲区是否全部出队(STREAMON未调用)
图像画面显示用户层 GStreamergst-launch-1.0 v4l2src device=/dev/video0 ! ... 出现画面花屏大概率是ISP参数错误或RAW格式匹配不对(如MIPI是RAW10,用户解析成RAW12)

确保有Framebuffer

ls /dev/fb*
#返回/dev/fb0

概念

Framebuffer 就是一块内存区域,里面存放着屏幕上每个像素的颜色值

LCD 控制器会周而复始地从 Framebuffer 中逐一取出每个像素的颜色,通过 RGB 数据线、时序信号(HSYNC、VSYNC、DE、DCLK)发送给 LCD 屏幕,屏幕就显示出图像。

Framebuffer不支持GPU加速,如果要使用GPU加速技术必须使用DRM,或者使用别人封装好的DRM库,比如openGL,Vulkan等

系统调用,打开文件

  • 使用<fcntl.h>头文件进行系统调用打开/dev/fb0
  • 使用<unistd.h>头文件进行系统调用关闭close(fb0);
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define FD "/dev/fb0"
int main(void){
    int fb0 =open(FD,O_RDWR);
    if (fb0 == -1) {
        printf("打开 %s 失败\n",FD);
        return -1;
    }
    printf("打开 %s 成功\n",FD);
    close(fb0);
    return 0;
}
  • 使用 ioctl 获取屏幕参数(分辨率、BPP)
  • 使用<linux/fb.h>拿到基本信息
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/fb.h>
#include <sys/ioctl.h>

int main(void)
{ 
    int fb0 = open("/dev/fb0",O_RDWR);
    if (fb0 == -1) {
        printf("打开失败\n");
        return -1;
    }
    // 2. 获取屏幕参数(分辨率、色深等)
    struct fb_var_screeninfo vinfo;
    struct fb_fix_screeninfo finfo;
    ioctl(fb0, FBIOGET_VSCREENINFO, &vinfo);
    ioctl(fb0, FBIOGET_FSCREENINFO, &finfo);

    // 打印屏幕信息
    printf("分辨率: %d x %d\n", vinfo.xres, vinfo.yres);
    printf("色深: %d 位\n", vinfo.bits_per_pixel);

    close(fb0);
    return 0;
}
  • /usr/include/linux/fb.h中的内容
struct fb_var_screeninfo vinfo; //获取基本的可变显示信息,分辨率,色深等。
struct fb_fix_screeninfo finfo;
  • mmap

mmapmemory map 的缩写,它将内核中的一块内存(这里是 Framebuffer)映射到用户进程的地址空间,使得可以直接像访问普通数组一样读写这块内存,而不需要通过 read/write 系统调用。

参数详解:

	addr: 建议的映射起始地址。通常设为 NULL (或 0),让内核自动选择合适的地址。
	length: 映射区域的长度(字节)。
	prot: 保护标志,决定内存页面的访问权限:
	PROT_READ: 可读
	PROT_WRITE: 可写
	PROT_EXEC: 可执行
	PROT_NONE: 不可访问
	flags: 映射类型标志:
	MAP_SHARED: 共享映射。对内存的修改会写回文件/设备,其他映射该文件的进程可见。(Framebuffer 常用此模式)
	MAP_PRIVATE: 私有映射。对内存的修改不会写回文件,而是创建副本(Copy-on-Write)。
	fd: 文件描述符(通过 open 获得)。如果是匿名映射,设为 -1。
	offset: 文件中的偏移量,必须是页面大小(通常 4KB)的倍数。
返回值:
	成功:返回映射区域的起始地址指针。
	失败:返回 MAP_FAILED (即 (void *)-1)。
在 Framebuffer 中的作用: 它将 /dev/fb0 设备的显存直接映射到用户空间的指针 fb_buf。之后你读写 fb_buf 就像读写普通数组一样,但实际上是直接操作显卡显存。
#include <stdio.h>
#include <sys/fcntl.h>
#include <sys/unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <string.h>

/**
 * @brief 映射关系
 * 
 * @return int 
 */
int main()
{ 
    int fd0 = open("/dev/fb0", O_RDWR);
    if(fd0 == -1)
    {
        printf("open fb0 error\n");
        return -1;
    }
    struct fb_var_screeninfo varinfo;
    ioctl(fd0, FBIOGET_VSCREENINFO, &varinfo);
    struct fb_fix_screeninfo finfo;
    ioctl(fd0, FBIOGET_FSCREENINFO, &finfo);

    //获取帧缓冲区内存大小 8bit的屏幕
    long screensize = varinfo.xres * varinfo.yres * varinfo.bits_per_pixel / 8;
    printf("screensize = %ld\n", screensize);//screensize = 4096000不到4mb

    //处理mmap
    char *fbp = mmap(NULL, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fd0, 0);
    if(fbp == MAP_FAILED)
    {
        printf("mmap error\n");
        return -1;
    }
    printf("fbp = %p\n", fbp);
    printf("finfo.line_length = %d\n", finfo.line_length);
    
    //设置fbp开始到screensize的内存为0 用来画矩形
    memset(fbp, 0, screensize);

    //刷成蓝色
    for (int i = 0; i < screensize; i += 4) { // 32位色:4字节1像素
        fbp[i + 0] = 0xFF;  // 蓝色
        fbp[i + 1] = 0x00;  // 绿色
        fbp[i + 2] = 0x00;  // 红色
        // 透明度Alpha ARGB8888支持,RGB565不支持Alpha不能设置这个
        fbp[i + 3] = 0xFF;  //通常设为255表示不透明 
    }
    sleep(2);

    // 释放映射关系 fbp到screensize的内存
    munmap(fbp, screensize);
    close(fd0);
    
    return 0;
}
  • RGB565和ARGB8888

通过vinfo.bits_per_pixel拿到色深,16位为RGB565,32位为ARGB8888。可以类别为

ARGB8888为:

  • 16进制:0xAARRGGBB
  • 二进制:0bAAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB

RGB565为:

  • 二进制:0bRRRRR GGGGGG BBBBB

简化之前刷屏代码

// 刷成蓝色 (简化版)
uint32_t *fbp32 = (uint32_t *)fbp; // 将 char 指针转为 32位整数指针
long num_pixels = screensize / 4;  // 计算总像素数
    
// 0xFF0000FF 代表: Alpha=0xFF, Red=0x00, Green=0x00, Blue=0xFF (ARGB格式)
// 在小端序机器上,这会在内存中存储为: FF 00 00 FF (即 B=FF, G=00, R=00, A=FF)
uint32_t blue_color = 0xFF0000FF; 

for (long i = 0; i < num_pixels; i++) {
    fbp32[i] = blue_color;
}

命令行操作

hexdump -e '16/1 "%02x " "\n"' /dev/input/event0

通过evtest工具测试输入设备

apt install evtest
root@docker:~/# evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0:      Power Button
/dev/input/event1:      AT Translated Set 2 keyboard
/dev/input/event2:      VirtualPS/2 VMware VMMouse
/dev/input/event3:      VirtualPS/2 VMware VMMouse
/dev/input/event4:      QEMU QEMU USB Tablet
Select the device event number [0-4]: 
可用输入设备列表:
/dev/input/event0: 电源按钮(Power Button)
/dev/input/event1: 标准键盘(AT Translated Set 2 keyboard)
/dev/input/event2: 虚拟机鼠标(VirtualPS/2 VMware VMMouse)
/dev/input/event3: 虚拟机鼠标(VirtualPS/2 VMware VMMouse)
/dev/input/event4: 虚拟机平板/触摸设备(QEMU QEMU USB Tablet)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h>
#include <sys/ioctl.h>

#define INPUT_DEVICE "/dev/input/event4"

#define SCREEN_WIDTH  1280
#define SCREEN_HEIGHT 800

// 全局保存真实触摸范围
int abs_x_min, abs_x_max;
int abs_y_min, abs_y_max;

int main(void)
{
    int fd;
    struct input_event ev;
    struct input_absinfo absinfo;
    // 打开输入设备
    fd = open(INPUT_DEVICE, O_RDONLY);
    if (fd < 0) {
        perror("open error");
        exit(1);
    }
    // 获取 X 轴范围
    ioctl(fd, EVIOCGABS(ABS_X), &absinfo);
    abs_x_min = absinfo.minimum;
    abs_x_max = absinfo.maximum;

    // 获取 Y 轴范围
    ioctl(fd, EVIOCGABS(ABS_Y), &absinfo);
    abs_y_min = absinfo.minimum;
    abs_y_max = absinfo.maximum;
    
    // 打印获取到的范围
    printf("EVDEV ABS_X range: %d - %d\n", abs_x_min, abs_x_max);
    printf("EVDEV ABS_Y range: %d - %d\n", abs_y_min, abs_y_max);

    while (1)
    {
        // 读取一个输入事件
        read(fd, &ev, sizeof(ev));
        // 1. 绝对坐标 X (触摸屏/鼠标)
        if (ev.type == EV_ABS && ev.code == ABS_X) {
            printf("X: %d\t", (ev.value*SCREEN_WIDTH)/abs_x_max);
        }

        // 2. 绝对坐标 Y
        if (ev.type == EV_ABS && ev.code == ABS_Y) {
            printf("Y: %d\n", (ev.value*SCREEN_HEIGHT)/abs_y_max);
        }
        // 5. 触摸屏按下/抬起
        if (ev.type == EV_KEY && ev.code == BTN_TOUCH) {
            printf("触摸状态: %s\n", ev.value ? "按下" : "抬起");
        }

        // 6. 鼠标左键按下/抬起
        if (ev.type == EV_KEY && ev.code == BTN_LEFT) {
            printf("鼠标左键: %s\n", ev.value ? "按下" : "抬起");
        }
    }
    
    close(fd);
    return 0;
}

<fcntl.h>

<unistd.h>

Lvgl

Lvgl控件学习

Display

每一个物理显示设备都有一个Display(lv_display)

lvgl初始化一个物理显示设备需要进行以下操作:

  • 为这个物理显示设备创建一个lv_display_t对象,并设置物理设备的宽高
  • 为这个对象提供绘图函数(硬件驱动)
  • 分配draw_buffers。把这个buffer当成lvgl的草稿纸,lvgl写完草稿了,才会写到屏幕上。使用lvgl画图,控件变化等功能,比如填充(0,0,320,240)这个区域lvgl会现在这个buffer中完成绘制,再调用绘图函数将buffer里的数据同步到显示屏。注意:buffer可能很小,比如才320*10,那么这个buffer,需要调用240/10=24次绘图函数
static void init_lvgl_buf(void)
{
  /*Create a display buffer*/
  static lv_disp_draw_buf_t disp_buf1;//定义一个显示绘图缓冲区结构体,用于管理 LVGL 的“草稿纸”。
  static lv_color_t buf1_1[SDL_HOR_RES * 100];//分配一块内存作为缓冲区
  lv_disp_draw_buf_init(&disp_buf1, buf1_1, NULL, SDL_HOR_RES * 100);//初始化缓冲区结构体

  /*创建 lv_disp_drv_t驱动描述符 */
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
    
  /* 给驱动描述符对象分配内存,设置Display宽高,提供绘图函数*/
  disp_drv.draw_buf = &disp_buf1;
  disp_drv.flush_cb = sdl_display_flush;
  disp_drv.hor_res = SDL_HOR_RES;
  disp_drv.ver_res = SDL_VER_RES;

  /*创建Display对象,会根据前面的驱动描述符对象中的内容创建*/
  lv_disp_t *disp = lv_disp_drv_register(&disp_drv);

}

lv_disp_drv_register做了什么

为什么**lv_scr_act()**能拿到默认显示屏的widgets根节点?

LVGL 中,显示设备通过全局链表默认指针来管理多个显示屏。

  • 全局静态变量:在文件作用域定义链表头(_lv_ll_t)和全局 disp_default 指针,提供模块内部的“上下文”。必然有一个变量存储lv_disp_t
  • 通用链表(_lv_ll_t:LVGL 实现了一个轻量级双向链表,可容纳任意类型的节点,避免为每种对象重写链表操作。
  • 动态内存分配:为每个显示设备分配独立的 lv_disp_t 对象,插入链表后统一管理。
  1. 分配并初始化 lv_disp_t 对象
    • 存储显示驱动参数(分辨率、flush 回调、绘图缓冲区等)。
    • 将该对象挂入全局显示链表,若为第一个显示设备则设为默认显示。
  2. 创建两个屏幕对象(lv_obj_t
    • 活动屏幕(act_scr:当前可见的主屏幕。
    • 上一个屏幕(prev_scr:用于屏幕切换动画的备用屏幕(初始与 act_scr 相同或为 NULL)。
  3. 配置屏幕对象的基本属性
    • 设置屏幕尺寸为显示设备的 hor_res × ver_res
    • 清除可滚动标志(屏幕本身不可滚动)。
    • 标记对象类型为 LV_OBJ_CLASS_SCREEN,使其成为所有其他控件的根容器。
    • 将屏幕的 parent 设为 NULL(顶级对象)。
  4. 将屏幕与显示设备关联
    • disp->act_scr = new_scrdisp->prev_scr = NULL(或 new_scr)。
    • 设置屏幕的 disp 指针指向当前显示设备。
  5. 触发默认主题/样式的加载
    • 如果 LVGL 启用了主题功能,会自动为屏幕应用默认主题(背景色、默认样式等)。
    • 主题可能进一步初始化默认字体、颜色方案等。
  6. 强制刷新整个屏幕(首次重绘)
    • 调用 lv_obj_invalidate(act_scr) 标记整个屏幕为“脏”。
    • 随后 LVGL 的任务调度器会触发一次全屏重绘:在绘图缓冲区中绘制屏幕背景,并通过 flush_cb 发送到物理显示屏。
  7. 使 lv_scr_act() 可用
    • lv_scr_act() 定义为 lv_disp_get_scr_act(lv_disp_get_default()),此时默认显示设备已存在,act_scr 已有效,因此可以返回屏幕根控件指针。

Screen

display对象创建之后会创建4个screen。

  1. 底层(位于活动屏幕下方,透明、不可滚动,但可以点击)
  2. Active Screen
  3. 顶层(位于活动屏幕上方,透明且不可滚动或点击)
  4. 系统层(位于顶层上方,透明且不可滚动或点击)
  • lv_screen_active()
  • lv_layer_top()
  • lv_layer_sys()
  • lv_layer_bottom()

“弹出窗口”添加到 顶层既可以实现弹出框

Active Screen

当前被 LVGL 显示系统认定为“前台”的那张画布

不是什么base widget 不要搞混

Base Widget

通过lv_scr_act() 拿到base widget

所有 Widgets 中最基本的是基础 Widget,所有其他 Widgets 都基于它构建。从面向对象的角度来看,可以将基础 Widget 视作所有其他 Widgets 继承的 Widget 类。

所有的Widget返回的都是lv_obj_t 结构体指针

widgets

所有widget都有

  • 位置

  • 尺寸

  • 父级

  • 样式

  • 事件

  • ....

位置(position)

1. 绝对坐标定位 (Absolute Positioning)

  • lv_obj_set_pos(obj, x, y)

    • 用法:同时设置 X 和 Y 坐标。
    • 示例lv_obj_set_pos(obj1, 100, 100);
    • 特点:最常用,简洁明了。坐标原点通常是父容器的内容区域左上角(扣除 padding 后)。
  • lv_obj_set_x(obj, x) / lv_obj_set_y(obj, y)

    • 用法:单独设置 X 或 Y 坐标。

    • 示例:

      lv_obj_set_x(obj1, 100);
      lv_obj_set_y(obj1, 100);
      
    • 特点:适用于只需要改变一个方向位置的场景,或者动态调整单个轴的位置。

注意:绝对坐标受父容器的 padding(内边距)影响。如果父容器设置了 padding,(0,0) 点实际上是 padding 之后的起始点。


2. 相对对齐定位 (Alignment)

这种方式基于父容器或另一个参考对象的特定锚点(如中心、左上角、右下角等)进行定位,更适合响应式布局或居中显示。

  • lv_obj_set_align(obj, align)

    • 用法:将对象对齐到其父容器的指定位置。

    • 示例:

      lv_obj_set_align(obj1, LV_ALIGN_CENTER); // 居中
      lv_obj_set_align(obj1, LV_ALIGN_TOP_MID); // 顶部中间
      
    • 特点:自动计算位置,即使父容器大小改变,对象仍保持相对位置不变。

  • lv_obj_align(obj, parent, align, x_ofs, y_ofs)

    • 用法:将对象对齐到指定 parent 的某个位置,并允许添加偏移量 (x_ofs, y_ofs)。

    • 示例:

      // 对齐到 base_widget 的中心,并向右偏移 10px,向下偏移 20px
      lv_obj_align(obj1, LV_ALIGN_CENTER, 10, 20);
      
    • 参数说明:

      • align: 对齐方式枚举(如 LV_ALIGN_CENTER, LV_ALIGN_OUT_RIGHT_MID 等)。
      • x_ofs, y_ofs: 在对齐基础上的额外像素偏移。
  • lv_obj_align_to(obj, base, align, x_ofs, y_ofs)

    • 用法:将对象对齐到任意另一个对象 base 的特定位置。

    • 示例:

      // 将 obj2 放在 obj1 的右侧中间,间隔 10px
      lv_obj_align_to(obj2, obj1, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
      
    • 特点:非常强大,用于构建复杂的相对布局(如按钮旁边的提示框)。

父布局自动定位

1. Flex 布局

 // 1. 设置父容器为 Flex 布局,方向为垂直排列
lv_obj_set_flex_flow(base_widget, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(base_widget, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
//会自动排序
lv_obj_t * obj1 = lv_obj_create(base_widget);
lv_obj_set_size(obj1, 100, 100);
lv_obj_t * obj2 = lv_obj_create(base_widget);
lv_obj_set_size(obj1, 100, 100);

2. Grid 布局 (网格)

// 定义列宽:两列,每列 50px
static lv_coord_t col_dsc[] = {50, 50, LV_GRID_TEMPLATE_LAST};
// 定义行高:两行,每行 50px
static lv_coord_t row_dsc[] = {50, 50, LV_GRID_TEMPLATE_LAST};

// 1. 设置父容器的网格描述(重要设置)
lv_obj_set_grid_dsc_array(base_widget, col_dsc, row_dsc);
// 占据第0列,第0行,跨度1列,跨度1行
lv_obj_set_grid_cell(obj1, LV_GRID_ALIGN_START, 0, 1, LV_GRID_ALIGN_START, 0, 1);
    
// 占据第1列,第0行,跨度1列,跨度1行
lv_obj_set_grid_cell(obj2, LV_GRID_ALIGN_START, 1, 1, LV_GRID_ALIGN_START, 0, 1);

3. 简单的自动间距 (Pad)

lv_obj_set_flex_flow(base_widget, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(base_widget, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
//设置子对象之间的间距
lv_obj_set_style_pad_row(base_widget, 40, 0);
//会自动排序 并且间距为40
lv_obj_t * obj1 = lv_obj_create(base_widget);
lv_obj_set_size(obj1, 100, 100);
lv_obj_t * obj2 = lv_obj_create(base_widget);
lv_obj_set_size(obj1, 100, 100);

大小

//设置宽度为100
lv_obj_set_width(w1,100);
//设置高度为50
lv_obj_set_height(w1,50);
//同时设置宽度和高度 100x50
lv_obj_set_size(w1,100,50);

//如果部件没有设置,会有默认的宽高。

样式

3种设置方式

  • lv_obj_set_style_xxx(obj,value ,selector);

    • obj: 指向要设置样式的 LVGL 对象(如 lv_obj_t *, lv_label_t * 等)的指针。

    • value: 要设置的具体属性值。类型取决于具体的属性(例如颜色是 lv_color_t,宽度是 lv_coord_t,不透明度是 lv_opa_t)。

    • selector: 样式选择器,用于指定该样式应用于对象的哪个部分或状态。

      • 0: 表示默认状态(主要部分)。
      • LV_PART_MAIN: 主体部分。
      • LV_PART_SCROLLBAR: 滚动条部分。
      • LV_PART_INDICATOR: 指示器部分(如进度条、滑块)。
      • LV_STATE_PRESSED: 按下状态。
      • LV_STATE_FOCUSED: 聚焦状态。
      • 可以使用位运算组合,例如 LV_PART_MAIN | LV_STATE_PRESSED
lv_obj_t *base_widget = lv_scr_act();

lv_obj_t *label = lv_label_create(base_widget);
lv_label_set_text(label, "This is a label");
lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 0);

lv_obj_set_style_text_color(label, lv_palette_main(LV_PALETTE_RED), 0);
  • lv_obj_add_style(obj, &style, selector)

    • 必须要创建一个lv_style_t变量,并且设置成static
    static lv_style_t label_style;
    
    • 必须初始化,声明变量只分配了内存,但并没有“初始化”其内部逻辑结构
    lv_style_init(&label_style);
    
    • 开始为这个lv_style_t添加样式
    lv_style_set_text_color(&label_style, lv_palette_main(LV_PALETTE_RED));
    
    • 把这个样子附加到控件上,可以多个控件共用一个样式
    lv_obj_add_style(label1,&label_style,0);
    lv_obj_add_style(label2,&label_style,0);
    
  • 主题 / LV_STYLE_DEFAULT,完全不设置,走全局主题或默认值

样式其他函数

lv_style_t * style_def = lv_obj_class_get_style(&lv_label_class, 0);

部件

小部件由一个或多个部件构成。例如,一个按钮只有一个部件,称为 LV_PART_MAIN。然而,一个 Slider (滑动条)(lv_slider) 有 LV_PART_MAINLV_PART_INDICATOR和 LV_PART_KNOB

状态

小部件可以处于以下状态的组合中:

  • LV_STATE_DEFAULT 正常,释放状态
  • LV_STATE_CHECKED: 切换或选中状态
  • LV_STATE_FOCUSED: 通过键盘或编码器聚焦或通过触摸板/鼠标点击
  • LV_STATE_FOCUS_KEY: 通过键盘或编码器聚焦,但不通过触摸板/鼠标
  • LV_STATE_EDITED: 通过编码器编辑
  • LV_STATE_HOVERED: 被鼠标悬停
  • LV_STATE_PRESSED: 正在被按下
  • LV_STATE_SCROLLED: 正在滚动
  • LV_STATE_DISABLED: 禁用

查看小部件状态

如果处于某种状态,会返回true

lv_obj_has_state(widget, LV_STATE_...)

添加或移除状态

lv_obj_add_state(widget, LV_STATE_...);
lv_obj_remove_state(widget, LV_STATE_...);

使用定时器3s后重置按钮状态

static void time_cb(lv_timer_t * timer)
{
    lv_obj_t * w = (lv_obj_t *)timer->user_data;
    lv_obj_clear_state(w, LV_STATE_PRESSED);
}
void lv_demo_widget_part_states(void)
{
    lv_obj_t * base_widget = lv_scr_act();
    lv_obj_t * btn = lv_btn_create(base_widget);
    lv_obj_set_size(btn, 100, 50);
    lv_obj_center(btn);
    // lv_obj_set_align(btn, LV_ALIGN_CENTER);
    lv_obj_add_state(btn, LV_STATE_PRESSED);//按钮按下状态
    
    lv_timer_create(time_cb, 3000, btn);
}

有一些 Widget 属性可以通过lv_obj_add_flag(widget, LV_OBJ_FLAG_...) 和 lv_obj_remove_flag(widget, LV_OBJ_FLAG_...) 来启用或禁用。

一些例子:

/* 隐藏 Widget */
lv_obj_add_flag(widget, LV_OBJ_FLAG_HIDDEN);

/* 使 Widget 不可点击 */
lv_obj_remove_flag(widget, LV_OBJ_FLAG_CLICKABLE);
  • LV_EVENT_VALUE_CHANGED当启用了 LV_OBJ_FLAG_CHECKABLE标志并且 Widget 被点击(在切换到/从选中状态过渡时)

LVGL的定时器执行定时回调时,主线程实际是停止的。

同时有多个定时器到期,它们会按照创建顺序优先级依次执行。

lvgl的所有空间widget都基于lv_obj_t。t表示类型,自定义类型。

lv_btn_create()
lv_lable_create()
//都(继承)lv_obj_t
//lv_obj_t是一个结构体,其实lv_btn_create() lv_lable_create()都是用于创建lv_obj_t结构体
//类似面向对象语言中的子类型转成父类型

widget创建:

lv_obj_t *w1 = lv_obj_create(lv_scr_act());
//这里的lv_scr_act()表示整块屏幕的根父类。
lv_obj_t *w2 = lv_obj_create(w1);
//这里的w2的父类是w1。

//	w2会随w1的位置移动而相对移动。

widget属性:

lv_obj_t *w1 = lv_obj_create(lv_scr_act());
  1. 大小

    //设置宽度为100
    lv_obj_set_width(w1,100);
    //设置高度为50
    lv_obj_set_height(w1,50);
    //同时设置宽度和高度 100x50
    lv_obj_set_size(w1,100,50);
    
    //如果部件没有设置,会有默认的宽高。
    
  2. 位置

    //坐标系跟电脑屏幕的是一样的,左上角为0,0 右下角为x,y。
    //部件默认是在父类对象的左上角
    //设置的是相对位置,不是绝对位置
    
    //设置x轴坐标
    lv_obj_set_x(w1,100);
    //设置y轴坐标
    lv_obj_set_y(w1,100);
    //同时设置x,y轴坐标
    lv_obj_set_pos(w1,100,100);//posation
    
  3. 对齐*

    //1.参照父对象对齐
    lv_obj_set_align(obj,LV_ALIGN_...);
    //参照父对象对齐,在进行偏移
    lv_obj_align(obj,LV_ALIGN_...,x,y);
    
    //2.参照其他对象对齐(无父子关系之间的对象)
    //参照其他对象对齐,再进行偏移
    lv_obj_align_to(obj_to_align,obj_referece,LV_ALIGN_...,x,y)
    
  4. 样式

    static lv_style_t style; //这里如果没有设置static 或者lv_style_t没有全局,会随方法栈丢失
    lv_style_init(&style);//必须先初始化
    //设置边框
    lv_style_set_border_color(&style, lv_color_hex(0x00FF00));
    
    //创建对象
    lv_obj_t *obj = lv_obj_create(lv_scr_act());
    //应用到对象
    lv_obj_add_style(obj, &style, LV_STATE_PRESSED);
    

    上面是最常用的方式,这种方式可以将样式和代码进行分离。单独一个文件存放样式。

    //直接设置
    lv_obj_t *obj2 = lv_obj_create(lv_scr_act());
    lv_obj_set_style_bg_color(obj2, lv_color_hex(0xFF0000),LV_STATE_DEFAULT);
    lv_obj_set_style_bg_opa(obj2, LV_OPA_50,LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(obj2, lv_color_hex(0x00FF00),LV_STATE_PRESSED);
    

    这种方式也有一定的应用。

不同部件的样式
lv_obj_t *slider = lv_slider_create(lv_scr_act());
lv_obj_align(slider, LV_ALIGN_BOTTOM_MID,0,-40);
//设置slider主体颜色
lv_obj_set_style_bg_color(slider, lv_color_hex(0xFF0000),LV_STATE_DEFAULT|LV_PART_INDICATOR);
lv_obj_set_style_bg_color(slider, lv_color_hex(0xFF0000),LV_STATE_DEFAULT|LV_PART_MAIN);
lv_obj_set_style_bg_color(slider, lv_color_hex(0xFF0000),LV_STATE_DEFAULT|LV_PART_KNOB);

5.事件

lv_obj_t *btn = lv_btn_create(lv_scr_act());
static char* my_data = "Hello World";
//不能这么写 LV_EVENT_CLICKED | LV_EVENT_LONG_PRESSED只会触发一个
lv_obj_add_event_cb(btn, lv_event_handler, LV_EVENT_CLICKED, my_data);
//这里的my_data可以传任意类型的指针
lv_obj_add_event_cb(btn, lv_event_handler, LV_EVENT_LONG_PRESSED, my_data);
static void lv_event_handler(lv_event_t *e)
{
  void *user_data = lv_event_get_user_data(e);
  // 根据实际类型进行转换和使用
  char *str = (char*)user_data;
  //判断事件类型
  lv_event_code_t code = lv_event_get_code(e);
  if(code == LV_EVENT_CLICKED)
  {
    //修改寄存器值
    printf("event_code: %d,%s\n",e->code,str);
  }else if( code == LV_EVENT_LONG_PRESSED) {
    //修改寄存器值
    printf("event_code: %d,%s\n",e->code,str);
  }
  //判断是否是父级对象
}

Vscode模拟lvgl环境

下载 https://github.com/Kitware/CMake/releases/download/v3.31.11/cmake-3.31.11-windows-x86_64.msi

如果vscode找不到cmake需要在

.vscode/settings.json文件下配置

{
    "cmake.cmakePath": "E:\\Program Files\\CMake\\bin\\cmake.exe"
}

如果运行后没有反应,将SDL2-2.30.1\x86_64-w64-mingw32\bin\SDL2.dll复制到示例中的bin文件夹(编译后生成)

红外项目

项目主要是通过

回调函数

多线程

在lvgl主线程下开启一个btns线程去poll()循环回调btn_event_cb()。

有事件告诉lvgl去消费按钮输入。

事件驱动

非阻塞

只需要在lvgl的循环中读去一个函数btns_read的返回值,然后判断返回值就能知道是否有输入

移植通用流程

  1. 下载lvgl源码
https://github.com/lvgl/lvgl/
  1. 删除没有用的文件和文件夹

1

  1. 将lv_conf_template.h复制并改名为lv_conf.h

  2. 修改lv_conf.h中的#if 0为#if 1

  3. lvgl代码初始化流程:

    1. 设置分辨率和缓冲区大小。
    #define LV_HOR_RES_MAX 480   // 水平分辨率
    #define LV_VER_RES_MAX 320   // 垂直分辨率
    #define LV_BUF_SIZE (LV_HOR_RES_MAX * 5) // 缓冲区大小(行数)
    
    static lv_disp_draw_buf_t draw_buf;     // LVGL绘制缓冲区 静态分配内存?
    static lv_color_t buf1[LV_BUF_SIZE];    // 第一缓冲区
    static lv_color_t buf2[LV_BUF_SIZE];    // 第二缓冲区(双缓冲)
    
    lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_BUF_SIZE);
    
    1. 给lvgl提供心跳时钟
    lv_tick_inc(1); // 可以在定时中断或者linux定时函数中调用
    
    1. 注册显示驱动,为lvgl提供绘图函数
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.hor_res = 480;//设置LVGL实际分辨率
    disp_drv.ver_res = 320;
    disp_drv.flush_cb = my_disp_flush;  // 注册刷新回调
    disp_drv.draw_buf = &draw_buf;
    lv_disp_drv_register(&disp_drv);
    
    1. 提供绘图函数给lvgl
    void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
    	//调用硬件进行画矩形或者画像素点
        
        // 通知LVGL绘画完成
        lv_disp_flush_ready(disp); 
    }
    
    1. 输入设备(伪代码)
    void evdev_read(lv_indev_drv_t * drv, lv_indev_data_t * data){
    	data->point.x = x;//具体的x坐标
        data->point.y = y;//具体的y坐标
        data->state = type;//按下的类型
    }
    static lv_indev_drv_t indev_drv;
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_POINTER;
    indev_drv.read_cb = evdev_read;
    lv_indev_drv_register(&indev_drv);
    
    1. 在main里面调用lv_init(),在while(1)调用lv_timer_handler()。

    lv_timer_handler()LVGL调度函数,发现需要重绘,安排刷新,发现LVGL定时器完成处理定时回调,分发用户输入事件的回调。

MCU移植

ESP(arduino)

具体移步到

099_lvgl初始化基于esp8266.md

ESP(idf)

待写

常规mcu

待写

Linux移植

具体移步到

099_lvgl移植到imx6ull.md

099_lvgl移植linux-通用.md

LVGL(Light and Versatile Graphics Library,轻量多功能图形库)是目前嵌入式领域最主流的开源图形库之一。它主要用于为资源受限的微控制器(MCU)和低性能微处理器(MPU)创建现代、美观且流畅的图形用户界面(GUI)。

下面从几个核心维度来介绍它:

1. 核心特点

  • 免费开源:采用 MIT 许可证,非常宽松,可以免费用于商业项目,无需公开代码。
  • 硬件要求低:最低仅需 几十 KB 的 RAM 和几百 KB 的 Flash 即可运行,非常适合单片机。
  • 现代化 UI 组件:内置超过 30 种基础控件,如按钮、滑块、图表、仪表、键盘、列表、图片、文本区域等,并支持动画、透明度、阴影和抗锯齿字体。
  • 强大的绘图能力:支持多种边框、渐变、圆角、阴影、不透明度等风格设置,无需额外图片就能实现毛玻璃、3D 感等视觉效果。
  • 事件驱动:采用类似 Qt 或网页前端的信号/槽机制,通过回调函数响应用户的点击、拖拽等输入。
  • 支持多种输入设备:可同时驱动触摸屏、鼠标、键盘、编码器、实体按钮等多种输入。
  • 多语言与 UTF-8:支持中、日、韩等语言显示,只需将字库文件包含进来即可。
  • 无需操作系统:可以跑在裸机程序上,也可以方便地移植到 FreeRTOS、RT-Thread 等实时系统上。

2. 基本工作流程

使用 LVGL 开发界面的大致步骤:

  1. 初始化:调用 lv_init() 并初始化显示驱动和输入设备驱动(如触摸屏)。
  2. 创建对象:在屏幕上创建各种控件(如 lv_btn_create 创建按钮,lv_label_create 创建文字标签)。
  3. 设置属性:调整控件的位置、大小、颜色、文字内容、样式等。
  4. 添加事件:为控件绑定回调函数,例如“点击按钮时 LED 闪烁”。
  5. 循环运行:在主循环中周期性调用 lv_timer_handler() 处理 LVGL 的内部任务(如动画、事件响应),间隔建议为 5-10 毫秒。

3. 硬件与性能

  • 典型配置
    • Cortex-M3/M4 单片机(如 STM32F4 系列)或以上。
    • 约 64 KB RAM、200 KB Flash 可以跑基础界面。
    • 带帧缓冲区的显示屏(SPI、8080、RGB 接口均可)。
  • 性能优化:支持将部分运算或绘图交给 GPU、DMA2D 等硬件加速单元;支持双缓冲或部分刷新以减少内存占用。

4. 生态与工具

  • SquareLine Studio:官方推出的拖拽式 UI 设计器(类似 Qt Designer),可大幅提升开发效率。社区版有限制,付费版可商用。
  • 模拟器:可以在 Windows、Linux、macOS 上用 VS Code、Code::Blocks、Qt Creator 等直接模拟运行 LVGL 代码,无需硬件。
  • 大量移植示例:官方和社区提供了适配常见开发板(STM32、ESP32、NXP i.MX RT、Raspberry Pi)和显示屏驱动芯片的范例。
  • 绑定语言:除了 C 语言主库,还有 MicroPython、Arduino 语言、Rust 等绑定,方便不同背景的开发者。

5. 与其他 GUI 方案对比

方案特点适合场景对比 LVGL
emWin老牌商业库,稳定高可靠性商业产品需付费,界面风格较老旧
TouchGFX效果华丽,但硬件要求高STM32 高性能单片机与 STM32 绑定较紧,占用资源更多
Qt for MCU与 Qt 生态兼容想复用 Qt 技能的开发者商业授权,硬件要求比 LVGL 高
uGFX轻量,商业化收费极小资源设备社区活跃度和文档不如 LVGL

image-20250812232709242

示例代码

#include <Arduino_GFX_Library.h>
#include <lvgl.h>
#include <Ticker.h>
//定时器
Ticker lvglTicker;

// 定时器回调函数
void lvglTickCallback() {
  lv_tick_inc(1);  // 每毫秒增加一次
}

// ESP12E 引脚定义
#define TFT_CS   15   // GPIO15
#define TFT_DC   4    // GPIO4
#define TFT_RST  2    // GPIO2
#define TFT_BL   5    // GPIO5(背光控制)

// 使用硬件SPI
Arduino_DataBus *bus = new Arduino_ESP8266SPI(TFT_DC, TFT_CS);
Arduino_GFX *gfx = new Arduino_ILI9488_18bit(bus, TFT_RST); // 使用18位模
/* LVGL配置 */
#define LV_HOR_RES_MAX 480   // 水平分辨率
#define LV_VER_RES_MAX 320   // 垂直分辨率
#define LV_BUF_SIZE (LV_HOR_RES_MAX * 5) // 缓冲区大小(行数)

static lv_disp_draw_buf_t draw_buf;     // LVGL绘制缓冲区
static lv_color_t buf1[LV_BUF_SIZE];    // 第一缓冲区
static lv_color_t buf2[LV_BUF_SIZE];    // 第二缓冲区(双缓冲)
void initGFX(){
    // 复位显示屏
  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, LOW);
  delay(100);
  digitalWrite(TFT_RST, HIGH);
  delay(200);

  // 初始化背光
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  // 保持常亮

  // 初始化显示屏
  if (!gfx->begin(27000000)) { // 27MHz SPI速度
    Serial.println("Display init failed!");
    while(1); // 停止执行
  }
  // 设置横屏模式
  gfx->setRotation(1); // 1表示90度旋转,通常是横屏
  Serial.println("Display init success!");
}
/* 显示刷新回调函数 */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);
  //绘图
  //开始绘制以area->x1, area->y1为起点,颜色为color_p(这里包含了x1,y1,w,h的所有颜色值),终点为w,h的矩形
  gfx->draw16bitRGBBitmap(
    area->x1,        // x起始位置
    area->y1,        // y起始位置
    (uint16_t *)color_p, // 数据指针
    w,               // 宽度
    h                // 高度
  );
  lv_disp_flush_ready(disp); // 通知LVGL刷新完成
}
void lvgl_init(){
  lv_init();
  lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_BUF_SIZE);
  /* 注册显示驱动 */
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  disp_drv.hor_res = LV_HOR_RES_MAX;//设置LVGL display的分辨率
  disp_drv.ver_res = LV_VER_RES_MAX;
  disp_drv.flush_cb = my_disp_flush;  // 注册刷新回调
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);
}
lv_obj_t *label;
void setup(void) {
  initGFX();
  // testGFX();
  lvgl_init();
  // 启动定时器,每1ms调用一次
  lvglTicker.attach_ms(1, lvglTickCallback);
    
  label = lv_label_create(lv_scr_act());
}

void loop()
{
  
  static char buffer[32];
  sprintf(buffer, "Hello World! counter = %d", counter);
  lv_label_set_text(label, buffer);
  counter++;

  lv_timer_handler();

  Serial.printf("Counter: %d\n", counter);
}

1.初始化显示器。

​ GFX初始化主要是为了给lvgl提供一个绘图函数,lvgl需要一个绘图函数。这时候lvgl会接管调用显示驱动。也就是我们主动调用绘图函数在屏幕上打点画图:(交给lvgl去做了)

//GFX 自己在屏幕上绘图
gfx->fillScreen(BLACK);//将屏幕设置成黑色
gfx->setTextColor(WHITE, BLACK);//字体颜色白色,背景黑色
gfx->setTextSize(2, 2);//字体大小
gfx->setCursor(30, 140);//所在位置
gfx->println("ILI9488 3.5\"");//开始打印字体在屏幕上

​ 可以给lvgl提供一个打点函数,也可以提供一个画矩形的函数

void Arduino_GFX::writePixel(int16_t x, int16_t y, uint16_t color)
void Arduino_GFX::draw16bitRGBBitmap(int16_t x, int16_t y,
                                     uint16_t *bitmap, int16_t w, int16_t h)

2.设置lvgl绘制缓冲区

2.1. 为什么需要设置绘制缓冲区?

LVGL是一个嵌入式图形库,通常运行在资源有限的微控制器上。它采用了一种部分刷新的策略,即每次只刷新屏幕的一部分,而不是整个屏幕(全屏刷新会消耗大量资源且速度慢)。

​为了高效地管理图形绘制,LVGL使用一个或多个缓冲区(称为绘制缓冲区)来暂存即将绘制到屏幕上的像素数据。这样,图形绘制操作可以在内存中完成,然后一次性将数据发送到显示设备,提高效率。

​如果没有缓冲区,每次绘制操作(比如画一个点)都可能直接操作显示设备(通过接口如SPI、并行接口等),这样效率极低,因为每次操作都会有通信开销。

2.2.缓冲区的作用

临时画布:存储即将渲染到屏幕的像素数据。
​图形加速:在内存中高效完成复杂绘制(如透明度混合、渐变)。
​同步控制:协调绘制与屏幕刷新时序,确保画面稳定。
​工作原理:LVGL在buf1渲染时,GPU同时从buf2读取数据到屏幕,交替使用避免视觉撕裂
​缓冲区大小:480*5=2400像素,每次渲染5行(平衡性能与内存)
static lv_disp_draw_buf_t draw_buf;     // LVGL绘制缓冲区
static lv_color_t buf1[LV_BUF_SIZE];    // 第一缓冲区
static lv_color_t buf2[LV_BUF_SIZE];    // 第二缓冲区(双缓冲)

3.lvgl初始化

void lvgl_init(){
  lv_init();  // 核心库初始化
  lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_BUF_SIZE);  // 双缓冲配置
  
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);  // 驱动结构体初始化
  disp_drv.hor_res = 480;       // 设置水平分辨率
  disp_drv.ver_res = 320;       // 设置垂直分辨率
  disp_drv.flush_cb = my_disp_flush;  // 注册刷新回调
  disp_drv.draw_buf = &draw_buf;      // 绑定缓冲区
  lv_disp_drv_register(&disp_drv);    // 注册驱动
}
组件作用代码示例
lv_init()初始化LVGL内核数据结构(内存管理/定时器等)lv_init()
绘制缓冲区双缓冲机制防止撕裂lv_disp_draw_buf_init(&draw_buf, buf1, buf2, 2400)
显示驱动结构体承载显示设备的配置参数lv_disp_drv_t disp_drv
分辨率设置定义物理屏幕尺寸disp_drv.hor_res = 480
刷新回调注册连接LVGL渲染引擎与硬件驱动程序disp_drv.flush_cb = my_disp_flush
驱动注册将配置注入LVGL系统lv_disp_drv_register(&disp_drv)

4.回调函数

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
  // 1. 计算刷新区域尺寸
  uint32_t w = area->x2 - area->x1 + 1;
  uint32_t h = area->y2 - area->y1 + 1;
  
  // 2. 调用硬件加速绘图
  //开始绘制以area->x1, area->y1为起点,颜色为color_p(这里包含了x1,y1,w,h的所有颜色值),终点为w,h的矩形
  gfx->draw16bitRGBBitmap(area->x1, area->y1, (uint16_t *)color_p, w, h); 
  
  // 3. 通知LVGL刷新完成
  lv_disp_flush_ready(disp);
}
  1. 接收LVGL传递的矩形区域(area)和像素数据(color_p)
  2. 通过draw16bitRGBBitmap将RGB565数据写入显存
  3. 必须调用lv_disp_flush_ready()解除LVGL阻塞

​ 简单来说就是,lvgl要开始画图了。画的区域是x,y,w,h这个区域,颜色值为color。然后传递给GFX硬件绘图驱动去绘图。接着绘图完成之后调用lv_disp_flush_ready()解除LVGL阻塞。

5.*手动给lvgl定时器时钟。更新这个内部时钟计数器

#include <Ticker.h>
Ticker lvglTicker;
// 定时器回调函数
void lvglTickCallback() {
  lv_tick_inc(1);  // 每毫秒增加一次 
}
// 启动定时器,每1ms调用一次
lvglTicker.attach_ms(1, lvglTickCallback);

lv_tick_inc(1);

​ 这个必须要的,如果不给lvgl更新时钟计数器,lvgl所有工作都不能正常运行。

​ /手动提供LVGL tick //可以使用其他时钟源在#define LV_TICK_CUSTOM 0中开启

lv_timer_handler()

​ LVGL调度函数,发现需要重绘,安排刷新,发现LVGL定时器完成处理定时回调,分发用户输入事件的回调。

前置条件

  • 会使用Linux FrameBuffer

前往:上级目录的 Linux-Framebuffer编程.md

  • 会使用/dev/input/event*

前往:上级目录的 Linux-input-event.md

流程

  1. 下载lvgl源码
https://github.com/lvgl/lvgl/
  1. 删除没有用的文件和文件夹

1

  1. 将lv_conf_template.h复制并改名为lv_conf.h

  2. 修改lv_conf.h:

    1. lv_conf.h中的**#if 0**为#if 1,
    2. 修改lv_conf.h中LV_COLOR_DEPTH 色深
    3. 修改lv_conf.h中LV_MEM_CUSTOM开启自动分配内存
  3. 在src同级目录新建porting文件夹用来存放lvgl整合触摸和显示代码

LVGL v8.3.11 移植到正点原子 i.MX6ULL 指南

本文档记录了将 LVGL v8.3.11 移植到正点原子 i.MX6ULL Linux 开发板的详细步骤和配置说明。


1. 环境准备

  • 开发板: 正点原子 i.MX6ULL (ALPHA/MINI)
  • 操作系统: Linux (Ubuntu 18.04+ 推荐)
  • 交叉编译器: arm-linux-gnueabihf-gcc
  • 依赖库: 标准 C 库、pthread、math

2. 项目瘦身 (可选但推荐)

为了保持工程清爽,删除了以下不必要的文件和文件夹:

  • demos/, docs/, examples/, scripts/, tests/
  • .github/, env_support/
  • CMakeLists.txt, Kconfig, SConscript 等非 Makefile 构建文件

1


3. 核心配置 (lv_conf.h)

基于 lv_conf_template.h 创建 lv_conf.h 并进行了以下关键配置:

  • 启用内容: #if 1
  • 颜色深度: #define LV_COLOR_DEPTH 16 (匹配 i.MX6ULL 的 16 位 Framebuffer)
  • 内存管理:
    • #define LV_MEM_CUSTOM 1 (直接使用 Linux 的 malloc/free)
  • 系统 Tick:
    • #define LV_TICK_CUSTOM 1
    • 实现 custom_tick_get() 函数,通过 gettimeofday 获取毫秒级时间。
  • 日志系统:
    • #define LV_USE_LOG 1
    • #define LV_LOG_PRINTF 1 (通过 printf 输出调试信息)

4. 驱动实现 (porting/)

4.1 显示驱动 (Linux Framebuffer)

  • 文件: porting/fbdev.c, porting/fbdev.h
  • 设备: /dev/fb0
  • 实现:
    • fbdev_init(): 打开设备并使用 mmap 映射显存。
    • fbdev_flush(): 将 LVGL 渲染的缓存拷贝到显存对应区域。

4.2 输入驱动 (Linux evdev)

  • 文件: porting/evdev.c, porting/evdev.h
  • 设备: 默认 /dev/input/event1 (根据实际情况修改 EVDEV_NAME)
  • 实现:
    • evdev_read(): 解析触摸屏产生的 input_event 数据,转换为 LVGL 的坐标和按键状态。

5. 编译系统 (Makefile)

创建了支持交叉编译的 Makefile,关键配置如下:

  • 编译器: CC = arm-linux-gnueabihf-gcc
  • 包含路径: -I./
  • 编译标准: -std=gnu99 (解决 for 循环中定义变量的编译报错)
  • 链接库: -lm -lpthread
  • 核心规则: 包含 lvgl.mk 以自动引入 LVGL 源码。

6. 使用说明

6.1 编译

在项目根目录下执行:

make clean
make -j4

生成可执行文件 lvgl_demo

6.2 运行

lvgl_demo 拷贝到开发板后运行:

chmod +x lvgl_demo
./lvgl_demo

7. 常见问题排查

  1. 编译报错 error: ‘for’ loop initial declarations...:
    • 确保 Makefile 中 CFLAGS 包含 -std=gnu99
  2. 头文件找不到 lvgl/lvgl.h:
    • 由于项目瘦身,直接引用 #include "lvgl.h" 即可。
  3. 运行显示 cannot open framebuffer device:
    • 检查设备节点是否存在,或使用 sudo 运行程序。
  4. 触摸无响应:
    • 运行 cat /proc/bus/input/devices 确认触摸屏对应的 event 编号,并修改 porting/evdev.c 中的 EVDEV_NAME

前言

本篇文章使用STM32CubeIDE进行开发,移植LVGL到STM32F103C8T6。F103C8T6刚好卡在LVGL能用的范围。

**开源地址:**https://gitee.com/wei-yuliu/stm32-f103-c8-t6-lvgl-stm32-cube-ide.git

一年前尝试过移植LVGL到STM32F103C8T6,各种问题,一直失败,放弃了。最近突然想起这件事,发现其实不难(兴许是当时能力还不够)。文章可能会有很多问题讲述不是很清楚,也可能有很多错误,欢迎指正!

挖个坑:

移植到Linux-x86:文章还没写

移植到Linux-ARM:文章还没写

学到什么

  • 使用STM32CubeIDE/CubeMX配置单片机的时钟,GPIO,外设,中断等。
  • 使用STM32Hal库代码编写,函数调用。
  • STM32CubeIDE/CubeMX项目配置,会合理工具使用会让编程事半功倍。
  • 屏幕厂商提供的示例,移植驱动到自己开发板。
  • 懂得LVGL移植的那点东西:
    • 裁剪&配置(lv_conf.h);
    • 提供时钟心lv_tick_inc(1)跳给lvgl;
    • 提供绘图函数LCD_DrawArea()给lvgl;
    • 时不时问问lvgl是否要刷新了lv_task_handler();
    • 写lvgl应用(main.c的例子);
  • 为后续Linux驱动开发,应用开发提供思维。我认为linux驱动和应用本质是硬件驱动提供接口给应用调用。你能移植设备厂商的代码了,跟应用开发人员(也还是你- -)规定一下接口需要什么参数,剩下的就不需要驱动工程师考虑的问题了,到应用开发人员头疼了(也还是你- -)。linux一堆规定,规定驱动要怎么写(mmp),规定提供给应用开发人员的接口(读写文件),这里就不深究了,后面移植到linux的时候再细细品味。

LVGL

下载

https://github.com/lvgl/lvgl/releases

stm32-lvgl-1


裁剪

stm32-lvgl-2

stm32-lvgl-3


STM32Cube IDE配置

项目配置

stm32-lvgl-4

stm32-lvgl-5

stm32-lvgl-6

stm32-lvgl-7

stm32-lvgl-8

stm32-lvgl-9

生成的目录结构

stm32-lvgl-10

配置LVGL目录

stm32-lvgl-11

stm32-lvgl-12

stm32-lvgl-13

移植屏幕厂商驱动测试

  • 移植屏幕驱动最重要的一点是:给LVGL提供绘图函数。

  • 由于每个人使用的屏幕不同,这里不写代码了,可以前往开原仓库中查看参考,地址:

画矩形函数:

void LCD_DrawArea(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *colors);
  • 在Core文件夹新增Hardware文件夹
  • 新增lcd.h和lcd.c存放移植屏幕厂商代码

LVGL移植代码编写

裁剪lvgl功能

修改后的lv_conf.h文件可以前往:https://gitee.com/wei-yuliu/stm32-f103-c8-t6-lvgl-stm32-cube-ide.git,直接改一下LV_HOR_RES_MAX,LV_VER_RES_MAX就能用了。

  • 详细说明

在lv_conf.h文件(原lv_conf_template.h)做如下修改:

  1. #if 0改成#if 1:这个是开启lvgl,如果为0为不使用lvgl,为0整个.h文件失效

  2. 屏幕大小:LV_HOR_RES_MAX,LV_VER_RES_MAX设置成自己屏幕大小

  3. 颜色翻转:如果是SPI屏要设置LV_COLOR_16_SWAP为1

  4. 屏幕旋转(0,90°,180°,270°)缓冲区:LV_DISP_ROT_MAX_BUF调小一点,10u变成1u,测试没有用到屏幕旋转功能,LV_DISP_ROT_MAX_BUF很小都没事。

  5. LV_MEM_SIZE:内存池,32 * 1024(32KB)改成2 * 1024(2KB),RAM占用2KB,C8T6只有20KB,所以我们这里调小一点。这里就不得不说LVGL内存的两种分配方式了:

    1. 手动分配LV_MEM_CUSTOM为0时,一个大小固定的数组(2KB)
    2. 自动分配LV_MEM_CUSTOM为1时,由malloc分配,单片机不怎么使用。移植LVGL到linux这种带操作系统才会用。
  6. 功能裁剪

功能模块修改后说明
LV_USE_ANIMATION0禁用动画
LV_USE_SHADOW0禁用阴影绘制
LV_USE_PATTERN0禁用矩形图案填充
LV_USE_VALUE_STR0禁用矩形上的数值字符串绘制
LV_USE_BLEND_MODES0禁用混合模式(仅正常混合)
LV_USE_OPA_SCALE0禁用整体透明度缩放
LV_USE_IMG_TRANSFORM0禁用图像旋转和缩放
LV_USE_API_EXTENSION_V60禁用 v6 API 兼容
LV_USE_API_EXTENSION_V70禁用 v7 API 兼容
LV_USE_DEBUG0完全禁用调试断言
  1. 对象裁剪

    1. 保留对象:

      • LV_USE_BTN → 1
      • LV_USE_CONT → 1
      • LV_USE_LABEL → 1
    2. 裁剪掉的对象:

      对象修改后控件名称
      LV_USE_ARC0Arc(圆弧)
      LV_USE_BAR0Bar(进度条)
      LV_USE_BTNMATRIX0Button Matrix(按钮矩阵)
      LV_USE_CALENDAR0Calendar(日历)
      LV_USE_CANVAS0Canvas(画布)
      LV_USE_CHECKBOX0Checkbox(复选框)
      LV_USE_CHART0Chart(图表)
      LV_USE_CPICKER0Color Picker(颜色选择器)
      LV_USE_DROPDOWN0Dropdown List(下拉列表)
      LV_USE_GAUGE0Gauge(仪表盘)
      LV_USE_IMG0Image(图像)
      LV_USE_IMGBTN0Image Button(图像按钮)
      LV_USE_KEYBOARD0Keyboard(虚拟键盘)
      LV_USE_LED0LED(指示灯)
      LV_USE_LINE0Line(线条)
      LV_USE_LIST0List(列表)
      LV_USE_LINEMETER0Line Meter(线性仪表)
      LV_USE_OBJMASK0Object Mask(对象遮罩)
      LV_USE_MSGBOX0Message Box(消息框)
      LV_USE_PAGE0Page(页面)
      LV_USE_SPINNER0Spinner(加载动画)
      LV_USE_ROLLER0Roller(滚轮选择器)
      LV_USE_SLIDER0Slider(滑动条)
      LV_USE_SPINBOX0Spinbox(数值调节框)
      LV_USE_SWITCH0Switch(开关)
      LV_USE_TEXTAREA0Textarea(文本区域)
      LV_USE_TABLE0Table(表格)
      LV_USE_TABVIEW0Tabview(选项卡视图)
      LV_USE_TILEVIEW0Tileview(磁贴视图)
      LV_USE_WIN0Window(窗口)

配置LVGL

  • 在Core/Hardware文件夹下新增lvglc.h和lvglc.c
/*
 * lvglc.h
 */
#ifndef HARDWARE_LVGLC_H_
#define HARDWARE_LVGLC_H_
#include "lcd.h" //引入lcd驱动
#include "lvgl.h" //引入lvgl

void lvgl_init(void);
void lvgl_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p);
#endif
/*
 * lvglc.c
 */
#include "lvglc.h"

#define LV_BUF_SIZE (LCD_WIDTH * 5) // 缓冲区大小(行数)
static lv_disp_buf_t draw_buf;     // LVGL绘制缓冲区
static lv_color_t buf1[LV_BUF_SIZE];    // 第一缓冲区
/**
 * lvgl初始化函数
 *
 */
void lvgl_init() {
  lv_init();
  lv_disp_buf_init(&draw_buf, buf1, NULL, LV_BUF_SIZE);

  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  disp_drv.hor_res = LCD_WIDTH; //屏幕宽度
  disp_drv.ver_res = LCD_HEIGHT; //屏幕高度
  disp_drv.flush_cb = lvgl_disp_flush;//刷新回调
  disp_drv.buffer = &draw_buf; //缓冲区
  lv_disp_drv_register(&disp_drv);
}

/* 显示刷新回调函数 */
void lvgl_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {

  //绘图矩形这里调用的就是lcd驱动,也就是说,lcd只需要给lvgl提供一个绘图函数就可以了。这也就是为什么lvgl能移植到各种地方的原因之一
  LCD_DrawArea(area->x1, area->y1, area->x2, area->y2, (uint16_t*)color_p);

  lv_disp_flush_ready(disp); // 通知LVGL刷新完成
}

给lvgl提供时钟

  • stm32f103c8t6需要配置时钟和定时器为lvgl提供时钟
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) //定时中断,在main.c中直接定义这个函数,按需修改
{
	if(htim->Instance==TIM2)//如果是定时器2
		lv_tick_inc(1); //给lvgl提供时钟
	}
}

使用lvgl!

int main(void)
{
	//其他代码
	//....
	//....
	
	//启动定时器
	HAL_TIM_Base_Start_IT(&htim2);
	LCD_Init();//lcd初始化
  	lvgl_init();//lvgl初始化,lvglc.c中的函数
  	
  	lv_obj_t *btn = lv_btn_create(lv_scr_act(), NULL);  //创建按钮
  	lv_obj_set_size(btn, 120, 50);               // 宽度120像素,高度50像素
  	lv_obj_align(btn, NULL, LV_ALIGN_CENTER, 0, 0); // 居中显示
  	//设置按钮背景颜色
  	lv_obj_set_style_local_bg_color(btn, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, lv_color_hex(0x0000FF));
    //在while中调用lv_task_handler()
    while(1)
    {
        lv_task_handler();
        HAL_Delay(5);
    }
}

代码优化

  • 不开启代码优化是绝对超出flash的!

properties -> c/c++build -> settings -> optimization level设置成**-Os**

stm32-lvgl-14

编译烧录

stm32-lvgl-16

LVGL 通用移植指南 (MCU & Linux)

本指南总结了将 LVGL 移植到任何嵌入式系统(无论是裸机 MCU 还是运行 Linux 的 SoC)的通用流程。


1. 核心源码集成

无论是什么平台,第一步都是将 LVGL 源码加入工程。

  • 获取源码: 从 GitHub 克隆或下载特定版本的 LVGL。
  • 添加路径: 确保编译器能够搜索到 lvgl.h 所在的根目录。
  • 编译配置:
    • Linux/Makefile: 包含 lvgl.mk
    • MCU/IDE: 手动将 src 目录下所有源文件加入编译列表,并添加头文件包含路径。

2. 配置文件 (lv_conf.h)

这是 LVGL 的“大脑”,决定了库的功能和行为。

  1. 创建文件: 复制 lv_conf_template.h 并更名为 lv_conf.h
  2. 基本配置:
    • #if 0 修改为 #if 1
    • 颜色深度: 设置 LV_COLOR_DEPTH (通常 MCU 为 16,Linux 为 32)。
    • 内存管理:
      • MCU: 设置 LV_MEM_SIZE 分配静态堆。
      • Linux/OS: 建议设置 LV_MEM_CUSTOM 1 直接使用系统的 malloc/free
  3. Tick 接口:
    • LVGL 需要一个毫秒级的计时器来处理动画和刷新。
    • 方式 A (推荐): 开启 LV_TICK_CUSTOM 1 并提供一个获取系统时间的函数。
    • 方式 B: 在硬件中断(如 SysTick)中手动调用 lv_tick_inc(1)

3. 显示驱动移植 (Display Interface)

LVGL 不直接操作硬件,它通过你提供的回调函数进行绘图。

  1. 初始化硬件: 初始化 LCD 控制器、SPI/I2C 总线或映射 Framebuffer。
  2. 注册驱动:
    • 定义 lv_disp_draw_buf_t 并分配显存缓冲区(可以是单缓冲或双缓冲)。
    • 定义 lv_disp_drv_t 结构体。
    • 实现 flush_cb 回调函数: 这是核心,LVGL 会传入坐标和像素数据,你需要将其拷贝到屏幕上。
    • 调用 lv_disp_drv_register()

4. 输入设备移植 (Input Interface)

  1. 初始化硬件: 初始化触摸芯片、按键 GPIO 或打开 evdev 设备。
  2. 注册驱动:
    • 定义 lv_indev_drv_t 结构体。
    • 设置设备类型(LV_INDEV_TYPE_POINTER 触摸/鼠标,或 LV_INDEV_TYPE_KEYPAD 按键)。
    • 实现 read_cb 回调函数: LVGL 会定期调用此函数,你需要返回当前坐标和按键状态(按下或释放)。
    • 调用 lv_indev_drv_register()

5. 主循环与任务处理

LVGL 是非阻塞的任务系统,需要你在主循环中不断轮询。

int main() {
    lv_init();               // 1. 初始化内核
    hal_init();              // 2. 初始化你的显示和输入驱动
    
    ui_init();               // 3. 创建你的 UI 界面

    while(1) {
        lv_timer_handler();  // 4. 处理定时器、刷新屏幕、读取输入
        usleep(5000);        // 5. 适当延时 (MCU 使用 Delay)
    }
}

6. MCU 与 Linux 的主要区别

特性MCU (裸机/RTOS)Linux (SoC)
显存通常在内部 RAM 或外部 SDRAM通过 /dev/fb0 进行内存映射 (mmap)
输入SPI/I2C 读取触摸 IC 寄存器读取 /dev/input/eventX 标准接口
内存静态数组分配动态内存分配 (malloc)
文件系统FatFS / LittleFS标准 POSIX (fopen/fread)
多线程需注意临界区保护需注意互斥锁 (Mutex)

7. 移植清单 (Checklist)

  • lv_conf.h 是否已启用并配置了颜色深度?
  • lv_tick_incLV_TICK_CUSTOM 是否正常工作?
  • flush_cb 是否能将像素正确显示在屏幕上?
  • read_cb 是否能正确上报坐标?
  • 主循环是否在持续调用 lv_timer_handler()

lvgl配置时钟有两种方式:

  • lv_tick_inc(uint time);

    比较简单一般用在mcu

  • lv_conf.h文件中定义 LV_TICK_CUSTOM为1

    一般用在linux和RTOS。需要给LV_TICK_CUSTOM_SYS_TIME_EXPR提供一个函数

    #define LV_TICK_CUSTOM 1
    #if LV_TICK_CUSTOM
        #define LV_TICK_CUSTOM_INCLUDE <stdint.h>
        extern uint32_t custom_tick_get(void);
        #define LV_TICK_CUSTOM_SYS_TIME_EXPR (custom_tick_get())
    #endif  
    

    linux

    uint32_t custom_tick_get(void)
    {
        struct timeval tv;
        gettimeofday(&tv, NULL);
        static uint64_t start = 0;
        if (!start) start = tv.tv_sec*1000ULL + tv.tv_usec/1000;
        return (uint32_t)(tv.tv_sec*1000ULL + tv.tv_usec/1000 - start);
    }
    

ov5640摄像头,头文件

#ifndef OV5640_H
#define OV5640_H

#include <stdint.h>
#include <stdbool.h>

/* Camera image resolution */
#define CAMERA_WIDTH  640
#define CAMERA_HEIGHT 480

/**
 * Initialize the OV5640 camera via V4L2.
 * @param device The V4L2 device path (e.g., "/dev/video1").
 * @return 0 on success, -1 on failure.
 */
int ov5640_init(const char * device);

/**
 * Capture a frame from the camera.
 * @param buf Pointer to the buffer where the frame data will be stored.
 *            The buffer must be at least CAMERA_WIDTH * CAMERA_HEIGHT * 2 bytes (for RGB565).
 * @return 0 on success, -1 on failure.
 */
int ov5640_capture_frame(void * buf);

/**
 * Deinitialize the camera.
 */
void ov5640_uninit(void);

#endif /* OV5640_H */

ov5640摄像头,c文件

#include "ov5640.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <errno.h>

#define BUFFER_COUNT 4

struct buffer {
    void *start;
    size_t length;
};

static int cam_fd = -1;
static struct buffer *buffers = NULL;
static unsigned int n_buffers = 0;

int ov5640_init(const char *device) {
    struct v4l2_capability cap;
    struct v4l2_format fmt;
    struct v4l2_requestbuffers req;
    struct v4l2_buffer buf;
    unsigned int i;

    /* 1. Open device */
    cam_fd = open(device, O_RDWR);
    if (cam_fd == -1) {
        perror("Opening video device");
        return -1;
    }

    /* 2. Check capability */
    if (ioctl(cam_fd, VIDIOC_QUERYCAP, &cap) == -1) {
        perror("Querying Capabilities");
        goto error;
    }

    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) {
        fprintf(stderr, "%s is no video capture device\n", device);
        goto error;
    }

    /* 3. Set format (RGB565) */
    memset(&fmt, 0, sizeof(fmt));
    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width = CAMERA_WIDTH;
    fmt.fmt.pix.height = CAMERA_HEIGHT;
    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565; // LVGL native format
    fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;

    if (ioctl(cam_fd, VIDIOC_S_FMT, &fmt) == -1) {
        perror("Setting Pixel Format");
        goto error;
    }

    /* 4. Request buffers */
    memset(&req, 0, sizeof(req));
    req.count = BUFFER_COUNT;
    req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;

    if (ioctl(cam_fd, VIDIOC_REQBUFS, &req) == -1) {
        perror("Requesting Buffers");
        goto error;
    }

    /* 5. Map buffers */
    buffers = calloc(req.count, sizeof(*buffers));
    for (n_buffers = 0; n_buffers < req.count; ++n_buffers) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = n_buffers;

        if (ioctl(cam_fd, VIDIOC_QUERYBUF, &buf) == -1) {
            perror("Querying Buffer");
            goto error;
        }

        buffers[n_buffers].length = buf.length;
        buffers[n_buffers].start = mmap(NULL, buf.length,
                                        PROT_READ | PROT_WRITE, MAP_SHARED,
                                        cam_fd, buf.m.offset);

        if (buffers[n_buffers].start == MAP_FAILED) {
            perror("mmap");
            goto error;
        }
    }

    /* 6. Queue buffers */
    for (i = 0; i < n_buffers; ++i) {
        memset(&buf, 0, sizeof(buf));
        buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index = i;
        if (ioctl(cam_fd, VIDIOC_QBUF, &buf) == -1) {
            perror("Queueing Buffer");
            goto error;
        }
    }

    /* 7. Start streaming */
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (ioctl(cam_fd, VIDIOC_STREAMON, &type) == -1) {
        perror("Starting Streaming");
        goto error;
    }

    return 0;

error:
    ov5640_uninit();
    return -1;
}

int ov5640_capture_frame(void *dest_buf) {
    struct v4l2_buffer buf;
    memset(&buf, 0, sizeof(buf));
    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    /* 1. Dequeue buffer */
    if (ioctl(cam_fd, VIDIOC_DQBUF, &buf) == -1) {
        if (errno == EAGAIN) return 0; // No frame yet
        perror("Dequeueing Buffer");
        return -1;
    }

    /* 2. Copy data to user buffer (assuming it's RGB565) */
    memcpy(dest_buf, buffers[buf.index].start, CAMERA_WIDTH * CAMERA_HEIGHT * 2);

    /* 3. Re-queue buffer */
    if (ioctl(cam_fd, VIDIOC_QBUF, &buf) == -1) {
        perror("Re-queueing Buffer");
        return -1;
    }

    return 0;
}

void ov5640_uninit(void) {
    if (cam_fd != -1) {
        enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        ioctl(cam_fd, VIDIOC_STREAMOFF, &type);
        for (unsigned int i = 0; i < n_buffers; ++i)
            munmap(buffers[i].start, buffers[i].length);
        free(buffers);
        close(cam_fd);
        cam_fd = -1;
    }
}

lvgl调用摄像头

static void camera_timer_cb(lv_timer_t * timer)
{
    lv_obj_t * canvas = (lv_obj_t *)timer->user_data;
    void * buf = lv_canvas_get_img(canvas)->data;
    ov5640_capture_frame(buf);
    lv_obj_invalidate(canvas);
}

if (ov5640_init("/dev/video1") == 0) {
    /* Create a canvas for camera display */
    static lv_color_t cbuf[CAMERA_WIDTH * CAMERA_HEIGHT];
    lv_obj_t * canvas = lv_canvas_create(lv_scr_act());
    lv_canvas_set_buffer(canvas, cbuf, CAMERA_WIDTH, CAMERA_HEIGHT, LV_IMG_CF_TRUE_COLOR);
    lv_obj_center(canvas);
    lv_obj_set_style_border_width(canvas, 2, 0);
    lv_obj_set_style_border_color(canvas, lv_palette_main(LV_PALETTE_RED), 0);
    /* Create a timer to update the camera feed */
    lv_timer_create(camera_timer_cb, 50, canvas); // 20 FPS
} else {
    printf("Failed to initialize camera /dev/video1\n");
}

Mcu

Sdcc

常用方式(确定版本)

  • 去官网下载

  • 上传到服务器

  • 设置环境变量

  • export PATH=$PATH:/root/tool/gcc/bin
    export PATH=$PATH:/root/tool/sdcc/bin
    source /etc/profile  
    //说明
    PATH 								环境变量
    = 									赋值
    $PATH 								当前PATH的值
    :/root/tool/arm-linux-gnueabihf 	追加一行
    

简单方式(apt安装)

apt install gcc
apt install sdcc

确定版本安装

  • 去到apt包管理官网https://packages.ubuntu.com/

  • 搜索,安装

  • apt install gcc=11.4.0
    

Windows安装

直接下载.exe文件进行安装

Stm32 notes

声明

讲解stm32c8t6点灯程序从c语言到bin烧录文件的整体流程。

第一部分,比较重要,说清楚被开发工具隐藏的c语言到bin文件流程,包括交叉编译器、c语言预处理,编译,汇编,链接,各个阶段生成的文件。

第二部分,将生成的bin反向推理中断向量,代码编反汇编。

第三部分,通过bootloader+app讲解中断偏移等内容

第一部分

在window下使用交叉编译工具

  • 安装
https://developer.arm.com/downloads/-/gnu-rm
  • 配置环境变量,配置到windows环境
D:\Program Files (x86)\GNU Arm Embedded Toolchain\10 2021.10\bin
  • 由于windows执行makefile比较困难,需要一步一步编译每个.c文件,再做链接,要么就要写shell脚本,比较麻烦。现在只举一些例子。以下是点灯程序,包含main.c:对应的c文件,以及startup.s:对应的汇编启动代码,还有link.ld链接脚本。

main.c

// 寄存器地址(STM32F103 固定)
#define RCC_BASE  0x40021000
#define GPIOA_BASE 0x40010800

#define RCC_APB2ENR *(volatile unsigned int *)(RCC_BASE + 0x18)
#define GPIOA_CRL   *(volatile unsigned int *)(GPIOA_BASE + 0x00)
#define GPIOA_ODR   *(volatile unsigned int *)(GPIOA_BASE + 0x0C)

void main(void)
{
  // 1. 使能 GPIOA 时钟
  RCC_APB2ENR |= 1 << 2;

  // 2. 配置 PA0 推挽输出
  GPIOA_CRL &= ~(0xF << 0);
  GPIOA_CRL |=  (0x3 << 0);

  // 3. 点灯 PA0
  GPIOA_ODR |= 1 << 0;

  // 死循环
  while(1);
}

startup.s

.equ _estack, 0x20005000

.section .isr_vector
.global _vector_table
_vector_table:
  .word _estack
  .word Reset_Handler
  .word 0
  .word 0
  .word 0
  .word 0
  .word 0

.section .text.Reset_Handler
Reset_Handler:
  bl main
  b .

link.ld


MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
  RAM   (rw)  : ORIGIN = 0x20000000, LENGTH = 20K
}


SECTIONS
{
  .text :
  {
    *(.isr_vector)     
    *(.text)           
  } > FLASH            

  .data :
  {
    *(.data)
  } > RAM AT > FLASH
}

将上面的三个文件放到同一个目录下,依次执行以下shell命令

1.将startup.s汇编文件编译成目标文件

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c startup.s -o startup.o

2.将main.c编译成目标文件

arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c main.c -o main.o

3.使用链接文件链接生成elf文件

arm-none-eabi-ld startup.o main.o -T link.ld -o main.elf

4.elf文件转成hex文件,elf文件转成bin文件

arm-none-eabi-objcopy -O ihex main.elf main.hex
arm-none-eabi-objcopy -O binary main.elf main.bin

步骤解析:

1.将汇编文件编程成目标文件过程。

2.main.c编译成目标文件,这个步骤依赖的头文件,如果是stdio.h这种已经有的会放在arm-none-eabi\include目录下,编译器会自己引进来。如果是自己写的比如,iic.c和iic.h,main.c引入了iic.h,这时候就需要添加**-T ./**命令。把对应目录下的头文件引进来。

下载

  • 参考当前目录下sdcc/gcc-sdcc安装.md文件

  • 由于windows执行makefile比较困难,需要一步一步编译每个.c文件,再做链接,要么就要写shell脚本,比较麻烦。现在只举一些例子。

基本使用

main.c

// 51 单片机 P1 口点灯(P1.0 接 LED)
#define FOSC 11059200L
#include <8052.h>

// 简单延时函数
void delay(void)
{
    unsigned int i;
    for(i=0; i<60000; i++);
}

void main(void)
{
    while(1)
    {
        P1 = 0xFE;   // P1.0 输出低电平 → 点灯
        delay();
        
        P1 = 0xFF;   // 灭灯
        delay();
    }
}

编译

# 1. 编译 + 链接(SDCC 一步完成)
sdcc -mmcs51 --model-small -I. main.c -o main.ihx
# 2. 转成标准 hex
packihx main.ihx > main.hex
# 或者直接复制main.ihx改成main.hex
# 3. 转成纯二进制 bin , sdcc不能直接转,直接烧录hex就可以了

1. 51 架构 (8051)

51 单片机不属于 ARM 这种“通用处理器”生态,其工具链相对专有或开源。

  • SDCC (Small Device C Compiler): 这是目前 51 架构最主流的开源免费工具链。它是一套针对 8 位微控制器的优化 C 编译器,支持 MCS51 (即 51 内核)、Z80、STM8 等。配合 SDCC 本身,加上 stcgal 等烧录工具,可以组成完整的开源开发环境。
  • Keil C51 (商业): 这是 51 架构事实上的“工业标准”,由 ARM 公司(原 Keil)提供。虽然它是商业软件,但在 51 开发中使用率极高,代码密度和稳定性表现优秀。Arm GNU Toolchain 并不能直接编译 51 内核代码

2. RISC-V 架构 (RV)

RISC-V 作为开源指令集,其 GNU 工具链非常完善,且由基金会和各大厂商积极维护。

  • RISC-V GNU Toolchain (riscv-gnu-toolchain): 这是 RISC-V 的官方 GNU 工具链,功能上类似 Arm GNU Toolchain。它包含 riscv64-unknown-elf-gcc(用于裸机)和 riscv64-linux-gnu-gcc(用于 Linux 系统)等。
    • 源码:托管在 GitHub 的 riscv-collab/riscv-gnu-toolchain
    • 预编译包:许多 Linux 发行版(如 Ubuntu/Debian)可以直接通过 apt install gcc-riscv64-linux-gnugcc-riscv64-unknown-elf 安装。
  • 厂商定制版本: 和 ARM 一样,芯片厂商也会提供定制化的工具链。
    • SiFive:提供针对其 CoreIP 优化的 Freedom Studio(基于 Eclipse 和 GNU 工具链)。
    • 沁恒 (WCH):其 RISC-V 系列 MCU(如 CH32V系列)虽也可用标准 GNU 工具链,但官方推荐使用 MounRiver Studio(集成了定制版 GCC 和调试工具)。

3. ARM架构

ARM Cortex-M/R/A很多交叉编译器分别编译不同的系列,常用的Arm GNU Toolchain (arm-none-eabi-gcc)

4. X86架构

x86架构要根据编译的系统做调整,也就是说编译到不同的os平台,比如linux,mac,windows。

如果在windows上编译windwos应用一般使用VS的MSVC,在linux编译windwos使用MinGW-w64/w32工具链。

至于mac,更多时候是在mac是编程自己的应用。

event1

1)事件定义

首先得有一个统一的事件表示。不用搞多复杂,一个类型加一个数据就够了:

typedef struct {
    uint16_t type;    /* 事件类型 */
    uint16_t param;   /* 附带参数 */
} event_t;

事件类型用枚举来管理:

enum {
    EVT_NONE = 0,
    EVT_KEY_PRESS,
    EVT_KEY_RELEASE,
    EVT_UART_RX,
    EVT_TIMER_TICK,
    EVT_ADC_DONE,
    // ...
};

2)事件队列

队列本质上就是一个环形缓冲区,中断里往里塞事件,主循环里取出来处理:

#define EVT_QUEUE_SIZE  32

static event_t evt_queue[EVT_QUEUE_SIZE];
static volatile uint8_t head = 0;
static volatile uint8_t tail = 0;

/* 中断中调用:投递事件 */
void event_post(uint16_t type, uint16_t param)
{
    uint8_t next = (head + 1) % EVT_QUEUE_SIZE;
    if (next != tail) {          /* 队列没满 */
        evt_queue[head].type  = type;
        evt_queue[head].param = param;
        head = next;
    }
}

/* 主循环中调用:取出事件 */
bool event_get(event_t *evt)
{
    if (tail == head)
        return false;            /* 队列为空 */

    *evt = evt_queue[tail];
    tail = (tail + 1) % EVT_QUEUE_SIZE;
    return true;
}

3)事件分发

取出事件之后,怎么交给对应的处理函数?最简单的做法是用一张函数指针表:

typedef void (*event_handler_t)(uint16_t param);

/* 处理函数注册表 */
static event_handler_t handler_table[EVT_MAX] = { NULL };

void event_register(uint16_t type, event_handler_t handler)
{
    if (type < EVT_MAX)
        handler_table[type] = handler;
}

void event_dispatch(event_t *evt)
{
    if (evt->type < EVT_MAX && handler_table[evt->type]) {
        handler_table[evt->type](evt->param);
    }
}

把这三部分组合起来,主循环就变得非常清爽了:

int main(void)
{
    system_init();

    /* 注册各模块的事件处理函数 */
    event_register(EVT_KEY_PRESS,  on_key_press);
    event_register(EVT_UART_RX,    on_uart_receive);
    event_register(EVT_TIMER_TICK, on_timer_tick);
    event_register(EVT_ADC_DONE,   on_adc_done);

    event_t evt;
    while (1) {
        if (event_get(&evt)) {
            event_dispatch(&evt);
        } else {
            __WFI();  /* 没事件就睡觉,省电 */
        }
    }
}
/* 中断里只投递事件 */
void USART1_IRQHandler(void)
{
    uint8_t data = USART1->DR;
    event_post(EVT_UART_RX, data);  // 投递完就走
}

单片机?无非操作寄存器罢了

1. 一切动作,无非操作寄存器

操作 SFR(特殊功能寄存器)就能让 IO 口拉高拉低。 操作 SFR 就能把中断总闸合上。 操作 SFR 就能让定时器开始哒哒哒地数数。 把 IO 引脚的手柄一高一低地掰出节奏,那就是 IIC,就是 SPI,就是 PWM。 ……

什么串口、什么 I2C、什么 SPI、什么 PWM 输出,拆穿了就是照着协议,在正确的时间点去写那几个寄存器、读那几个寄存器。

举个最俗的例子:串口。 忽略掉所有细节,你看: 把 SCON 配好模式,把定时器 1 的 TH1、TL1 装好初值,波特率就定下来了; 再往 PCON 里把 SMOD 那一位拨一下,波特率还能翻倍; 然后往 SBUF 这个寄存器里扔一个字节,硬件自己就按约定的节拍,把起始位、数据位、校验位、停止位一帧一帧颠出去。 另一边,从 SBUF 里读一个字节,电脑发过来的数据就拿到了。 “通过设置几个寄存器的值,不就设置好了?” 对,就是这个道理。

但是,“细节完全不能忽略”,我们学的,恰恰就是怎么去翻手册找到这些寄存器,搞清楚协议是什么样子,以及把这些寄存器在正确的时间点摆弄成什么值


2. 你写的 C,怎么变成芯片里的动作(以51为例)

2.1 写 C

你想点个灯:

P0_1 = 1;   // 实际操作的是 P0 寄存器的第1位

你眼里是一个变量赋值。这行 C,经过下面的流水线,最终就会变成驱动一颗 LED 亮起的物理电流。

2.2 编译、汇编、链接 → .hex

Keil C51 或者 SDCC 这些编译器,帮你把 C 变成了汇编,再把汇编变成机器码,链接成 .hex 文件。 不知道里面发生了什么,不知道就不知道呗,又不影响写 C。没错,写应用层的时候可以这么任性,为什么单片机现在入门如此简单,就是因为有了c,有编译器帮你去做这些复杂的操作。 但如果哪天你被一个硬件 fault 卡住,想抠那几微秒的时序,就会发现,这个黑箱里的每一步都是你的救命稻草。

2.3 烧录

.hex 文件里是一条一条的机器指令,被烧录器逐字节写进 Flash。

flash
地址     		指令         真实存储
0x0000  [jump 0x0080]     0001 0080  <-假设
0x0004  [mov xx, xx]      0002 xxxx  <-假设
0x0008  [ ]                          <-假设
...     ....

2.4 上电执行

一上电,芯片内部的硬件逻辑“咔嗒”一下,把 PC(程序计数器)归零。 PC=0,CPU 从 Flash 的 0 地址拿出第一条指令,塞进译码器,译码器认出来这是什么活,执行完毕,PC 自动指向下一条指令的地址(51 指令不定长,有 1 字节、2 字节、3 字节,PC 有时候+1有时候+2,或者+3)。 就这么一条一条地从 Flash 里取指令,一条一条地执行,直到断电。


3. 这不巧了吗?计算机组成原理那本书全活了

上电后,PC=0,从 Flash 地址 0 处取第一条指令(针对51,复位向量就在 0x0000)。 指令五花八门: 有操作 RAM 的,把数据搬来搬去; 有操作 SFR 的,把 IO 口拉高拉低,把中断闸门拉开合上; 有跳转的,把 PC 直接踹到别的地址; 还有空转的 NOP,纯粹耗掉一个机器周期。 这不就是你前面那句话——“无非是操作寄存器”吗?

那 CPU 又是个什么玩意?为什么能看懂指令?为什么能算加法? 大学教过啊:译码器把一大串 0/1 翻译成“哦,这是要把某个寄存器的第几位拉高”;ALU 在那吭哧吭哧做加减乘除与或非。

CPU 怎么从 Flash 取指令?怎么操作 RAM?怎么操作 SFR? Flash 是一栋房子,RAM 是另一栋房子,SFR 又是一排控制柜,CPU 自个儿是个总调度室。 房子之间怎么通信?拉一根电话线嘛——这就是总线。 把地址线、数据线、控制线捆在一起,CPU 往地址线上放门牌号,数据线上就传来对应的内容。 单片机这个小区里房子可多了:I2C、SPI 这些是出小区的省道,通向板子上的传感器、存储器; 小区内部的电话线也有讲究,有些用铜线就够(低速总线 APB),有些得上光纤(高速总线 AHB)。高低速总线就分出来了。

那数据传输怎么搞? 让 CPU 从 A 房子搬数据,再搬到 B 房子?行,但 CPU 被这种纯体力活占着太浪费。 于是引出一个专门干这种活儿的搬运工——DMA。CPU 交代一句:“把 UART 接收到的这 256 个字节搬到 RAM 里”,DMA 就闷头搬完,不劳 CPU 再费神。

什么 RAM?SRAM、DRAM。什么 Flash?NOR Flash、NAND Flash。教材里分过类,在这里全都对得上号。 就连中断,也不过是硬件在某个事件发生时,硬生生把 PC 当前值压栈,然后把 PC 强制指到一个预设的中断入口地址,CPU 就跑去执行中断函数了——计算机组成原理里的“中断响应周期”,在你这片 51 上原原本本地发生。

真棒,大学都学过的。知行合一,知道计算机里面有什么,单片机怎么把这个过程实现,怎么使用这些知识。


4. 知不知道又怎样?抽象叠着抽象

可是,上面这些东西,你不知道,也不妨碍你敲出能跑的 C 代码。

你写 EA = 1;,开启了总中断。 你写 P0 = 0xFF;,把 P0 口的 8 个 IO 全拉高了。 你写 int a = 0; a++;,本质上就是在操作 RAM 里的某个寄存器(存储单元),C 编译器帮你决定用哪个地址,帮你生成操作它的指令。 你全程都在读写寄存器,只不过 C 语言给你披了一层温柔的外衣。

学计算机的,最会玩抽象。 从物理电平 → 链路帧 → IP 包 → TCP 流 → HTTP 报文,一层一层往上叠,底层难用是吧?那我再给你抽象一层。

51 抽象层次还不算高,你 EA = 1 一眼还能看出是去摸一个叫 EA 的开关。 到了 STM32,HAL 库直接挡在你和寄存器之间: HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); 这背后是 HAL 库帮你找到 GPIOA 的基地址,算出偏移,定位到输出数据寄存器,然后往那个 32 位寄存器里写了个 (1<<5)。 抽象!你只需告诉管家“把书房的灯打开”,管家自己就去掰开关了。


5. 抽象下面是什么?从 EA = 1 一路拆到晶体管

那如果我们偏偏要撕开这层抽象呢?

EA = 1; 这个 C 语句,在 8051 的语境下,EA 是个特殊功能寄存器中的一位——总中断使能位。 它在字节地址 0xA8(即 IE 寄存器)的第7位。 注意:这里有个大坑。 如果你想用字节操作开总中断,应该写 IE = 0x80;,也就是把 0xA8 这个字节地址里的最高位置 1。 如果你写 EA = 1;,编译器可能会聪明地把它翻译成位操作指令,而 8051 有一个独立的位寻址空间,其中位地址 0xAF 才是 EA。 你绝对不能写成“操作位地址 0xA8”,那个位地址对应的是 IE 寄存器的第 0 位 EX0(外部中断0使能),你这样一搞,总中断没开,外部中断 0 倒给点着了。 所以,严谨地说: EA=1 → 汇编是 SETB 0xAF,机器码 0xD2 0xAFIE=0x80 → 汇编是 MOV 0xA8, #0x80,机器码 0x75 0xA8 0x80。 两者都能开总中断,但走的路不同,寻址方式不同。这就是细节。

好,我们把 SETB 0xAF 这条汇编指令翻成机器码。 查 8051 指令集:SETB bit 的指令编码是 0xD2,后面跟一个直接位地址。 位地址 0xAF,二进制就是 1010 1111。 所以整条指令的机器码是: 1101 0010 1010 1111 (0xD2 0xAF)

这两个字节被烧录进 Flash 的某个地址,比如 0x0000 和 0x0001。 上电,PC=0,从 Flash 地址 0 取出 1101 0010,塞进译码器。 译码器一看:“1101 0010?这是 SETB 指令,后面还得再取一个字节。” PC 自动加 1,从地址 1 取出 1010 1111,译码器明白:“要去位寻址空间里,把位地址 0xAF 拉高。” 内部硬件选中位地址 0xAF 所对应的那个锁存器,把它电平拉到高。 这个锁存器恰恰好连着中断控制逻辑的总闸门,闸门一开,整个中断系统就激活了。 从你敲下 EA=1 到中断闸门打开,多米诺骨牌一张没塌,全按你设计的顺序倒下。


6. 那到底要操作哪些寄存器?怎么查?

这时候你问:“操作哪些寄存器?” 答:翻数据手册。一本几百页的 PDF,拆穿了就是一本开关面板说明书

以经典 51 为例,这几个面板你必须摸透:

6.1 IO 口面板

P0, P1, P2, P3。 写 1 就是输出高电平(同时引脚内部弱上拉),写 0 就是输出低电平。想读引脚状态?直接读这个寄存器就行。

6.2 中断调度面板

IE(中断使能寄存器,字节地址 0xA8):

  • EA (IE.7):总闸
  • ET0 (IE.1):定时器0中断分闸
  • EX0 (IE.0):外部中断0分闸 …… IP(中断优先级寄存器,字节地址 0xB8): 哪个中断优先级高,在这里调。 TCON(定时器/计数器控制寄存器,字节地址 0x88): 里面有定时器的启停开关(TR0、TR1),还有外部中断的触发方式选择(IT0、IT1)。

6.3 定时器车间

TMOD(定时器模式寄存器,不可位寻址): 决定定时器0/1是当定时器还是计数器,以什么方式工作(13位/16位/8位自动重装)。 TCON:启停控制,溢出标志也在这儿。 TH0/TL0, TH1/TL1:装初值的地方。 想用定时器产生精确的时间间隔?在这几个开关和计数器里填好数就行。

6.4 串口收发室

SCON(串口控制寄存器,字节地址 0x98): 选模式(8位/9位),允许接收(REN),发送/接收中断标志。 PCON(电源控制寄存器,字节地址 0x87): SMOD 位控制波特率是否加倍,其他位还有掉电模式、空闲模式。 SBUF(串口数据缓冲器,字节地址 0x99): 写 SBUF = 发;读 SBUF = 收。地址虽是一个,物理上是两个独立的寄存器,发收分离,硬件自己判别。

6.5 复用说明(以增强型51或ARM为例)

经典 51 的引脚功能是硬件绑死的:P3.0/P3.1 固定是 RXD/TXD,一打开串口,这两个脚自动切换到第二功能,不用额外设置。 但后来的 STC15/STC8 或者 STM32,有了专门的引脚功能切换寄存器(如 P_SW1AFIO),你才能“操作 SFR 就能设置引脚复用”。 这个细节知道就好,别在新手期对着 AT89C51 手册找复用寄存器。

查这些开关的方法极简单: 打开手册的 “Special Function Registers” 表格,或看每个外设章节的寄存器描述,一张表告诉你寄存器名、地址、每一位叫什么、写0写1分别是什么意思。 你要做的,就是对着那张表把二进制拼出来。


7. 从51跳到ARM,无非开关面板变大了

有了 51 的底子,去碰 STM32,你会恍然发现,所有套路一模一样,只是:

  • 寄存器变成了 32 位,一口气能控制 16 个 IO 口的模式、上下拉、输出速度。
  • 每个外设前面多了一道时钟闸门:你得先往 RCC 相关寄存器里写个 1,把外设的钟敲响,它才开始工作。51 不用,51 的外设钟大多默认是通的,或者跟主时钟捆在一起。
  • 中断控制器变得更复杂,NVIC,还要设优先级分组,但灵魂还是那套:使能位、标志位、优先级位。

STM32HAL 库呢? 它就是在这一大片寄存器面板上,又给你糊了一层墙纸。 你调用 HAL_UART_Transmit(&huart1, pData, Size, Timeout),HAL 在后头帮你查状态寄存器,帮你把一字节塞进数据寄存器,帮你等到发送完成。 “不直接操作寄存器”,不代表寄存器不存在,只是有人替你动了手。 当你要调一个极限性能的中断驱动,或者被一个 bug 卡住时,你还是得掀开墙纸,看背后的寄存器波形。


8. 最后的最后:把整个流程串成一张图

单片机 = 一堆房子(外设、存储器)挂在总线走廊上,每个房子都有门牌号(地址),门牌号后面是一排开关(寄存器)。 CPU 这个快递员,按照 Flash 里的一张张指令单(机器码),到各个房子取货(读寄存器)、送货(写寄存器)。偶尔让 DMA 这小哥代劳搬大件。

你写的每一行 C: P0_1 = 0; → 编译器把它变成 CLR 0x91(位地址),机器码 0xC2 0x91 → 烧进 Flash → PC 指向它 → 译码器认出“清0位” → 硬件把位地址 0x91(P0.1)锁存器拉低 → P0.1 引脚输出低电平 → LED 灭。 全过程无魔法,全是大学里教过的硬核知识。

什么译码器、ALU、PC、栈、哈佛结构、冯诺依曼结构,全在你手里这块几块钱的芯片里原原本本跑着。 你问我怎么学单片机? 无非就是:翻开手册 → 找到对应的寄存器 → 根据协议要求算出要写的值 → 写进去 → 世界就动起来了。 这份“罢了”的背后,正是你从底层到应用层自由穿行的底气。

函数入栈

栈帧概念:

某个寄存器入栈(一般是sfr特殊功能寄存器)

SOC开发笔记

Embedded linux imx6ull

Driver program

注意:

  • linux模块开发必须依赖源码,并且这个源码的linux版本必须与烧录到板子的源码版本一致,也就说整个uboot->linux内核->rootfs->模块开发。一开始就要确定各自的版本,直接拿芯片原厂提供的进行修改最好。

  • 必须要配置linux内核顶层Makefile的交叉编译器目标架构

#在大概254行

#ARCH		?= $(SUBARCH)
#CROSS_COMPILE	?= $(CONFIG_CROSS_COMPILE:"%"=%)

ARCH		?= arm
CROSS_COMPILE	?= arm-linux-gnueabihf-

Linux三种驱动

  • 字符设备驱动

  • 块设备驱动

  • 网络设备驱动

字符设备驱动介绍

字符设备就是一个一个字节按照字节流的方式进行读写。

第一个驱动

  • 先编译linux内核。/root/tool/linux_driver,执行正点原子提供的脚本imx6ull_alientek_emmc.sh
  • 在aaa_linux_driver文件夹下新建chrdevbase.c文件
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>

#define CHRDEVBASE_MAJOR	200				/* 主设备号 */
#define CHRDEVBASE_NAME		"chrdevbase" 	/* 设备名     */

static char readbuf[100];		/* 读缓冲区 */
static char writebuf[100];		/* 写缓冲区 */
static char kerneldata[] = {"kernel data!"};

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase open!\r\n");
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	
	/* 向用户空间发送数据 */
	memcpy(readbuf, kerneldata, sizeof(kerneldata));
	retvalue = copy_to_user(buf, readbuf, cnt);
	if(retvalue == 0){
		printk("kernel senddata ok!\r\n");
	}else{
		printk("kernel senddata failed!\r\n");
	}
	
	//printk("chrdevbase read!\r\n");
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	/* 接收用户空间传递给内核的数据并且打印出来 */
	retvalue = copy_from_user(writebuf, buf, cnt);
	if(retvalue == 0){
		printk("kernel recevdata:%s\r\n", writebuf);
	}else{
		printk("kernel recevdata failed!\r\n");
	}
	
	//printk("chrdevbase write!\r\n");
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase release!\r\n");
	return 0;
}

/*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	
	.open = chrdevbase_open,
	.read = chrdevbase_read,
	.write = chrdevbase_write,
	.release = chrdevbase_release,
};

/*
 * @description	: 驱动入口函数 
 * @param 		: 无
 * @return 		: 0 成功;其他 失败
 */
static int __init chrdevbase_init(void)
{
	int retvalue = 0;

	/* 注册字符设备驱动 */
	retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
	if(retvalue < 0){
		printk("chrdevbase driver register failed\r\n");
	}
	printk("chrdevbase init!\r\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
	printk("chrdevbase exit!\r\n");
}

/* 
 * 将上面两个函数指定为驱动的入口和出口函数 
 */
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);

/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");

  • 编写Makefile,这一步很重要,KERNELDIR内核目录配置很重要
KERNELDIR := /root/tool/linux_driver
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
  • 复制到开发板
//ssh连接开发板,在开发板中执行以下命令
sshpass -p 20000316 scp root@192.168.0.254:/root/tool/linux_driver/aaa_driver/chrdevbaseApp /home/root/linux_kernel_driver/chrdevbaseApp
  • 加载.ko文件先更新依赖库
depmod
modprobe chrdevbase.ko
  • 或者不用更新依赖库
insmod chrdevbase.ko
  • 创建设备节点文件

​ 这个文件就是操作这个模块的点,应用程序通过节点文件对设备进行操作。

mknod /dev/chrdevbase c 200 0

其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个 字符设备,“200”是设备的主设备号,“0”是设备的次设备号

  • 对设备进行读写
./chrdevbaseApp /dev/chrdevbase 1
  • 写在
rmmod chrdevbase.ko

绪论

由于linux不能直接使用物理内存,linux有mmu(memory manager unit)。

MMU完成虚拟空间到无论空间的内存映射,有内存保护功能,不能直接透过操作系统进行物理地址映射。

MMU同样映射DDR,IO寄存器:

​ ARM、RISC-V 这类嵌入式 CPU,物理地址空间是统一编址的(统一编址 = 内存映射 IO,MMIO)

  • 一部分物理地址 → 对应 DDR 内存
  • 另一部分物理地址 → 对应 片上外设寄存器(GPIO、UART、I2C、时钟控制器……)

CPU 只看物理地址

  • 物理地址空间 = DDR + 各种外设寄存器

  • MMU 只做虚拟地址 → 物理地址翻译

  • 只要物理地址落在外设区间,CPU 就直接访问硬件寄存器,不经过 DDR

ioremap 和 iounmap

ioremap 函数用于获取指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在 arch/arm/include/asm/io.h 文件中

iounmap 函数释放掉 ioremap 函数所做的映射

Linux kernel

下载linux内核

安装 apt-get install lzop

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j8 //all表示编译所有 
//如果没有zImage使用以下命令编译
make zImage ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- all -j8

linux内核的xxx_deconfig文件在/arch/arm/configs/imx_v7_defconfig

编译DTB设备树文件(前面已经编译了)不需要执行

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- ./arch/arm/boot/dts/imx6ull-14x14-emmc-7-1024x600-c.dtb

Linux启动测试

先把zImage和dtb文件拷贝到tftp文件夹

cp /root/tool/linux_kernel/zdyz/arch/arm/boot/dts/imx6ull-14x14-emmc-7-1024x600-c.dtb /srv/tftp/
cp /root/tool/linux_kernel/zdyz/arch/arm/boot/zImage /srv/tftp/

在uboot命令行下执行指令

tftp 80800000 zImage
tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
bootz 80800000 - 83000000

Linux内核来源

  1. www.kernel.org 内核官网,有linux所有版本。
  2. SOC厂商在linux内核官网找一个版本,在这个版本下操作。
  3. 板子厂商会从SOC厂商拿到soc厂商维护的内核在进行修改。

NXP官方linux内核编译

上传到ubuntu
修改顶层Makefile
ARCH		?= $(SUBARCH)
CROSS_COMPILE	?= $(CONFIG_CROSS_COMPILE:"%"=%)
改成
ARCH		?= arm
CROSS_COMPILE	?= arm-linux-gnueabihf-

执行以下指令

make clean
make imx_v7_mfg_defconfig
make -j8

添加自己开发版

  • arch/arm/configs的imx_v7_mfg_defconfig文件进行复制改成aa_wyl_defconfig

  • 将aa_wyl_defconfig文件中"CONFIG_ARCH_MULTI_V6=y"注释掉

  • 执行make aa_wyl_defconfig
    

添加设备树文件

cp arch/arm/boot/dts/imx6ul-14x14-evk.dts arch/arm/boot/dts/imx6ull_wyl_defconfig.dts

修改设备树的Makefile

arch/arm/boot/dts/Makefile

找到CONFIG_SOC_IMX6ULL配置添加自己的dtb

dtb-$(CONFIG_SOC_IMX6ULL) += \
	imx6ull_wyl_defconfig.dtb \

自己板子编译

make distclean
make aa_wyl_defconfig
make menuconfig
make all -j8
或者
make zImage all -j8

问题修改

/usr/bin/ld: scripts/dtc/dtc-parser.tab.o  CC      arch/arm/kernel/asm-offsets.s
:(.bss+0x50): multiple definition of `yylloc'; scripts/dtc/dtc-lexer.lex.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status

进入到scripts/dtc/dtc-lexer.l和dtc-lexer.lex.c_shipped

YYLTYPE yylloc;
改成
extern YYLTYPE yylloc;

完成

arch/arm/boot/zImage
arch/arm/boot/dts/imx6ull_wyl_defconfig.dtb

测试

复制zImage和dtb文件到tftp目录

tftp 80800000 zImage
tftp 83000000 imx6ull_wyl_defconfig.dtb
bootz 80800000 - 83000000

CPU主频修改

aa_wyl_defconfig文件中

CONFIG_CPU_FREQ_DEFAULT_GOV_ONDEMAND=y
CONFIG_CPU_FREQ_GOV_POWERSAVE=y
CONFIG_CPU_FREQ_GOV_USERSPACE=y
CONFIG_CPU_FREQ_GOV_INTERACTIVE=y

第 1 行,配置 ondemand 为默认调频策略。

第 2 行,使能 powersave 策略。

第 3 行,使能 userspace 策略。

第 4 行,使能 interactive 策略。

第 1 行注释掉,然后在 4 行后面添加: CONFIG_CPU_FREQ_GOV_ONDEMAND=y

EMMC改成8数据线

网络驱动修改

在linux中,所有的驱动修改都涉及到设备树,一般改动只会改动设备树文件内容。

源码

pinctrl_spi4: spi4grp {
			fsl,pins = <
				MX6UL_PAD_BOOT_MODE0__GPIO5_IO10	0x70a1
				MX6UL_PAD_BOOT_MODE1__GPIO5_IO11	0x70a1
				MX6UL_PAD_SNVS_TAMPER7__GPIO5_IO07	0x70a1
				MX6UL_PAD_SNVS_TAMPER8__GPIO5_IO08	0x80000000
			>;
		};

改成

pinctrl_spi4: spi4grp {
			fsl,pins = <
				MX6UL_PAD_BOOT_MODE0__GPIO5_IO10	0x70a1
				MX6UL_PAD_BOOT_MODE1__GPIO5_IO11	0x70a1  
			>;
		};

源码

spi4 {
		compatible = "spi-gpio";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_spi4>;
		pinctrl-assert-gpios = <&gpio5 8 GPIO_ACTIVE_LOW>;
		status = "okay";
		gpio-sck = <&gpio5 11 0>;
		gpio-mosi = <&gpio5 10 0>;
		cs-gpios = <&gpio5 7 0>;
		num-chipselects = <1>;
		#address-cells = <1>;
		#size-cells = <0>;

		gpio_spi: gpio_spi@0 {
			compatible = "fairchild,74hc595";
			gpio-controller;
			#gpio-cells = <2>;
			reg = <0>;
			registers-number = <1>;
			registers-default = /bits/ 8 <0x57>;
			spi-max-frequency = <100000>;
		};
	};

改成

spi4 {
		compatible = "spi-gpio";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_spi4>;
		status = "okay";
		gpio-sck = <&gpio5 11 0>;
		gpio-mosi = <&gpio5 10 0>;
		num-chipselects = <1>;
		#address-cells = <1>;
		#size-cells = <0>;

		gpio_spi: gpio_spi@0 {
			compatible = "fairchild,74hc595";
			gpio-controller;
			#gpio-cells = <2>;
			reg = <0>;
			registers-number = <1>;
			registers-default = /bits/ 8 <0x57>;
			spi-max-frequency = <100000>;
		};
	};

源码

&iomuxc {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_hog_1>;
	imx6ul-evk {
		/*省略其他*/


		pinctrl_enet1: enet1grp {
			fsl,pins = <
				MX6UL_PAD_ENET1_RX_EN__ENET1_RX_EN	0x1b0b0
				MX6UL_PAD_ENET1_RX_ER__ENET1_RX_ER	0x1b0b0
				MX6UL_PAD_ENET1_RX_DATA0__ENET1_RDATA00	0x1b0b0
				MX6UL_PAD_ENET1_RX_DATA1__ENET1_RDATA01	0x1b0b0
				MX6UL_PAD_ENET1_TX_EN__ENET1_TX_EN	0x1b0b0
				MX6UL_PAD_ENET1_TX_DATA0__ENET1_TDATA00	0x1b0b0
				MX6UL_PAD_ENET1_TX_DATA1__ENET1_TDATA01	0x1b0b0
				MX6UL_PAD_ENET1_TX_CLK__ENET1_REF_CLK1	0x4001b031
			>;
		};

		pinctrl_enet2: enet2grp {
			fsl,pins = <
				MX6UL_PAD_GPIO1_IO07__ENET2_MDC		0x1b0b0
				MX6UL_PAD_GPIO1_IO06__ENET2_MDIO	0x1b0b0
				MX6UL_PAD_ENET2_RX_EN__ENET2_RX_EN	0x1b0b0
				MX6UL_PAD_ENET2_RX_ER__ENET2_RX_ER	0x1b0b0
				MX6UL_PAD_ENET2_RX_DATA0__ENET2_RDATA00	0x1b0b0
				MX6UL_PAD_ENET2_RX_DATA1__ENET2_RDATA01	0x1b0b0
				MX6UL_PAD_ENET2_TX_EN__ENET2_TX_EN	0x1b0b0
				MX6UL_PAD_ENET2_TX_DATA0__ENET2_TDATA00	0x1b0b0
				MX6UL_PAD_ENET2_TX_DATA1__ENET2_TDATA01	0x1b0b0
				MX6UL_PAD_ENET2_TX_CLK__ENET2_REF_CLK2	0x4001b031
			>;
		};
}

NXP BSP项目

Rootfs

将正点原子出厂的uboot和linux内核进行编译

注意linux内核编译时出现yylloc错误要修改

进入到scripts/dtc/dtc-lexer.l和dtc-lexer.lex.c_shipped

YYLTYPE yylloc;
改成
extern YYLTYPE yylloc;
  • uboot烧录进板子。

  • zImage放到tftp目录

  • imx6ull-alientek-emmc.dtb放到tftp目录

测试

//zImage改了名,网线插到2口(FEC1)

tftp 80800000 zImage-alientek
tftp 83000000 imx6ull-alientek-emmc.dtb
bootz 80800000 - 83000000

修改Makefile,添加交叉编译器

CROSS_COMPILE必须使用绝对路径

ARCH ?= arm
CROSS_COMPILE ?= /root/tool/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-

添加中文支持

libbb/printable_string.c文件

//printable_string 方法

//注释掉
if (c >= 0x7f)
	break;
	
//即
if (c < ' ')
	break;
/* if (c >= 0x7f)
	break; */
//ENABLE_UNICODE_SUPPORT 判断项

//注释掉
if (c < ' ' || c >= 0x7f)

//添加
if( c < ' ')
//即
while (1) {
	unsigned char c = *d;
	if (c == '\0')
		break;
	if (c < ' ' || c >= 0x7f)
		*d = '?';
	d++;
}

libbb/unicode.c文件

*d++ = (c >= ' ' && c < 0x7f) ? c : '?';
//改成
*d++ = (c >= ' ') ? c : '?';
//即
while ((int)--width >= 0) {
	unsigned char c = *src;
	if (c == '\0') {
		do
			*d++ = ' ';
		while ((int)--width >= 0);
		break;
	}
	// *d++ = (c >= ' ' && c < 0x7f) ? c : '?';
	*d++ = (c >= ' ') ? c : '?';
	src++;
}
/* if (c < ' ' || c >= 0x7f) */
//改成
if(c < ' ')
//即
while (*d) {
	unsigned char c = *d;
	// if (c < ' ' || c >= 0x7f)
	if(c < ' ')
		*d = '?';
	d++;
}

配置busybox

①、defconfig,缺省配置,也就是默认配置选项。

②、allyesconfig,全选配置,也就是选中 busybox 的所有功能

③、allnoconfig,最小配置。

默认使用defconfig

make defconfig
make menuconfig

在图形配置界面做以下配置

静态编译 busybox 还是动态编译

选中为静态编译,我们不选中

Location: 
 -> Settings 
	-> Build static binary (no shared libs)

选中

Location: 
  -> Settings 
	-> vi-style line editing commands

使能 busybox 的 unicode 编码以支持中文

Location: 
 -> Settings
   -> Support Unicode //选中
	 -> Check $LC_ALL, $LC_CTYPE and $LANG environment variables //选中

不选中

Location: 
  -> Linux Module Utilities
	-> Simplified modutils

选中

Location: 
  -> Linux System Utilities 
 	-> mdev (16 kb) //确保下面的全部选中,默认都是选中

编译报错问题

系统时间问题解决在.config文件中注释掉这两个

#CONFIG_RDATE=y
#CONFIG_DATE=y

编译

make
//清除配置 make distclean

make后---命令行会问要不要打开CONFIG_DATE,输入n后回车,再次询问要不要打开CONFIG_RDATE,输入n后回车

make install CONFIG_PREFIX=/root/tool/rootfs/maked-zdyz

编译完成,在/root/tool/rootfs/maked-zdyz有相应文件

添lib加库文件

  • 移动/root/tool/rootfs/maked-zdyz所有文件到/srv/nfs文件夹下
cp -rf /root/tool/rootfs/maked-zdyz/* /srv/nfs
  • 在/root/tool/rootfs/maked-zdyz新建lib文件夹

  • 复制交叉编译器里面的库文件

cd /root/tool/arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib
cp *so* *.a /srv/nfs/lib/ -d   “-d”表示拷贝符号链接

rm /srv/nfs/lib/ld-linux-armhf.so.3  软链接删除
cp /root/tool/arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/ld-linux-armhf.so.3 /srv/nfs/lib/ 

cd /root/tool/arm-linux-gnueabihf/arm-linux-gnueabihf/lib
cp *so* *.a /srv/nfs/lib/ -d

mkdir /srv/nfs/usr/lib
cd /root/tool/arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/lib
cp *so* *.a /srv/nfs/usr/lib -d
  • 查看文件系统大小
cd /srv/nfs
du ./lib ./usr/lib/ -sh

//
root@wyl:/srv/nfs# du ./lib ./usr/lib/ -sh
57M     ./lib
67M     ./usr/lib/
  • 创建其他文件夹
mkdir dev proc mnt sys tmp root

挂载测试

nfs配置命令

root=/dev/nfs nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>] ip=<client-ip>:<server-ip>:<gw-ip>:<netmask>:<hostname>:<device>:<autoconf>:<dns0-ip>:<dns1-ip

服务器 IP 地址,也就是存放根文件系统主机的 IP 地址,那就是 Ubuntu 的 IP 地址。

根文件系统的存放路径,/srv/nfs

NFS 的其他可选选项,一般不设置

客户端 IP 地址,也就是我们开发板的 IP 地址,Linux 内核启动以后就会使用 此 IP 地址来配置开发板。

服务器 IP 地址

网关地址192.168.0.1

子网掩码,255.255.255.0。

客户机的名字,一般不设置,此值可以空着。

设备名,也就是网卡名,一般是 eth0,eth1

自动配置,一般不使用,所以设置为 off。

DNS0 服务器 IP 地址,不使用

DNS1 服务器 IP 地址,不使用

在uboot命令行输入

setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.0.254:/srv/nfs,proto=tcp rw ip=192.168.0.155:192.168.0.254:192.168.0.1:255.255.255.0::eth1:off'//设置 bootargs
saveenv //保存环境变量

或者

setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.0.254:/srv/nfs,v3,tcp ip=dhcp'

启动linux

tftp 80800000 zImage-alientek
tftp 83000000 imx6ull-alientek-emmc.dtb
bootz 80800000 - 83000000

Uboot

uboot开源项目,一个bootloader。高度抽象的一个bootloader,对比单片机而言,通常要自己手写bootloader。

芯片厂商会从uboot官网下载某一个版本的uboot,然后加入自己芯片的驱动。

开发版的厂商会从芯片厂商下载uboot,再度进行修改。

编译正点原子提供的版级uboot

  • 找到uboot源码
  • 传到vm虚拟机的目录下,解压。
  • 确定DDR3和EMMC大小
  • 编译过程(注意必须要有gcc环境)
sudo apt install build-essential
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_ddr512_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

可以在顶层makefile中配置交叉编译器和编译的架构

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

uboot编译注意点

首次编译生成的u-boot.binu-boot.imx,一个u-boot.bin是uboot的代码,直接烧录是用不了的。对于imx6ull来说,BootROM启动必须读取flash头部第一个扇区4KB里面的IVT,DCD,DATA等内容然后初始化ddr。随后才会跳转到uboot,u-boot.imx是u-boot.bin已经添加了头部信息的可直接运行文件。

首先,defconfig配置文件,必须要和板子上的ddr,flash对应。比如emmc_defconfig说明要用emmc,nand_defconfig说明要用到nand。

help命令或者?

**bootz命令

bootz 的内部工作流程大致分为以下几个阶段:

1. 内核映像格式检查

U-Boot 会检查指定地址处是否为一个有效的 zImage。zImage 具有特殊的头部结构(通常包含自解压代码),其前 40 字节中有一个魔数(例如 0x016f2818)用于标识 ARM Linux 内核。若格式错误,命令会报错退出。

2. 处理 initrd(可选)

如果提供了 initrd 地址和大小,U-Boot 会将 initrd 的信息记录到内存中的启动参数区域(或通过设备树传递给内核)。内核启动后会挂载 initrd 作为初始根文件系统。

3. 处理设备树(FDT)

U-Boot 会对提供的 DTB 进行必要的处理:

  • 如果 DTB 地址不在内存中(例如是 flash 地址),可能需要先复制到 RAM。
  • 修正 DTB:U-Boot 会将内存节点(/memory)、启动参数(chosen/bootargs)等信息写入 DTB。这依赖于 U-Boot 的配置(CONFIG_OF_LIBFDT)。
  • 验证 DTB 格式(魔数、版本等)。

如果未指定 FDT 地址,U-Boot 会依次查找:

  • 环境变量 fdtaddr
  • 环境变量 fdtcontroladdr(如果存在) 如果都找不到,则不会传递设备树,内核将回退到使用 ATAGS 启动(老式方式)。

4. 准备启动参数

U-Boot 会根据内核启动约定,将必要的信息传递给内核。在 ARM 体系结构中,启动时寄存器状态约定为:

  • r0 = 0
  • r1 = 机器类型编号(如果使用 ATAGS,不使用设备树时有效)
  • r2 = ATAGS 或 DTB 的物理地址

当使用设备树时,U-Boot 会将设备树地址放入 r2,并将 r1 通常设置为 0(表示使用设备树)。具体的寄存器使用可参考 Linux 内核文档(Documentation/arm/Booting)。

5 .跳转到内核

U-Boot 找到 zImage 的入口点(通常就是加载地址),清理缓存并执行跳转指令,将控制权交给内核的自解压代码。后续由 zImage 完成自解压和最终内核的引导。

分析编译后的uboot

api文件夹

和硬件没有关系的API函数

arch

u-boot.lds链接脚本

NXP

下载NXP提供的uboot
选择xxx_defconfig文件
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ul_14x14_evk_emmc_defconfig
编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- V=1 -j8
清除
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean

移植

1.1添加板子默认配置文件

​ 复制nxp官方mx6ul_14x14_evk_emmc_defconfig重命名为自己的xxx_deconfig

mx6ul_14x14_evk_emmc_defconfig

CONFIG_ARM=y
CONFIG_ARCH_MX6=y
CONFIG_TARGET_MX6UL_14X14_EVK=y
CONFIG_SYS_EXTRA_OPTIONS="IMX_CONFIG=board/freescale/mx6ul_14x14_evk/imximage.cfg,MX6UL_EVK_EMMC_REWORK"
CONFIG_CMD_GPIO=y
CONFIG_CMD_DHCP=y
CONFIG_CMD_PING=y

在uboot根目录下的/board/freescale/mx6ullevk/imximage.cfg文件下配置了裸机的头文件

xxx_defconfig

CONFIG_ARM=y
CONFIG_ARCH_MX6=y
CONFIG_TARGET_MX6ULL_WYL=y //改动
CONFIG_SYS_EXTRA_OPTIONS="IMX_CONFIG=board/freescale/wyl_mx6ullevk/imximage.cfg,MX6UL_EVK_EMMC_REWORK"//改动
CONFIG_CMD_GPIO=y
CONFIG_CMD_DHCP=y
CONFIG_CMD_PING=y
1.2添加对应头文件
  • ​ 在uboot根目录下/include/configs/mx6ullevk.h定义了配置信息

  • ​ 复制mx6ullevk.h文件修改名为wyl_mx6ullevk.h

  • ​ 修改wyl_mx6ullevk.h里面内容

//头文件修改
#ifndef __WYL_MX6ULLEVK_CONFIG_H
#define __WYL_MX6ULLEVK_CONFIG_H
1.3添加对应的板级文件夹
  • ​ 每个板子都有对应的文件,就是板级文件,现在将6ull evk的板级文件夹board/freescale/mx6ullevk复制命名为wyl_mx6ullevk

  • ​ 重命名wyl_mx6ullevk文件夹中mx6ullevk.c文件为 wyl_mx6ullevk.c

  • ​ 修改wyl_mx6ullevk文件夹下的Makefile

obj-y  := mx6ullevk.o
//修改为
obj-y  := wyl_mx6ullevk.o
  • ​ 修改wyl_mx6ullevk文件夹下的imximage.cfg
PLUGIN	board/freescale/mx6ullevk/plugin.bin 0x00907000
//修改为
PLUGIN	board/freescale/wyl_mx6ullevk/plugin.bin 0x00907000
  • ​ 修改wyl_mx6ullevk文件夹下的Kconfig
if TARGET_MX6ULL_14X14_EVK || TARGET_MX6ULL_9X9_EVK
//修改为
if TARGET_MX6UL_WYL


config SYS_BOARD
	default "mx6ullevk"
//修改为
config SYS_BOARD
	default "wyl_mx6ullevk"
	
	
config SYS_CONFIG_NAME
	default "mx6ullevk"
//修改为
config SYS_CONFIG_NAME
	default "wyl_mx6ullevk"
  • 修改wyl_mx6ullevk文件夹下MAINTAINERS文件
MX6ULLEVK BOARD
M:	Peng Fan <peng.fan@nxp.com>
S:	Maintained
F:	board/freescale/wyl_mx6ullevk/
F:	include/configs/wyl_mx6ullevk.h
F:	configs/aa_wyl_mx6ull_emmc_defconfig

1.4修改uboot配置界面
  • ​ 去到arch/arm/cpu/mx6/Kconfig文件,在207行,加入以下
config TARGET_MX6ULL_WYL
	bool "Support wyl_mx6ullevk"
	select MX6ULL
	select DM
	select DM_THERMAL
加入下面这行
source "board/freescale/wyl_mx6ullevk/Kconfig"

确定IO初始化

board/freescale/wyl_mx6ullevk/wyl_mx6ullevk.c中的定义lcd_pads[]中

注释复位

//MX6_PAD_SNVS_TAMPER9__GPIO5_IO09 | MUX_PAD_CTRL(NO_PAD_CTRL)

/*
	gpio_direction_output(IMX_GPIO_NR(5, 9) , 0);
	udelay(500);
	gpio_direction_output(IMX_GPIO_NR(5, 9) , 1);
*/

LCD参数

在board/freescale/wyl_mx6ullevk/wyl_mx6ullevk.c中的displays[]

主要是理解arch/arm/include/asm/imx-common/video.h中的display_info_t结构体,以及这个结构体下的include/linux/fb.h中fb_videomode结构体(RGB LCD参数)

struct fb_videomode {
	const char *name;	/* optional */
	u32 refresh;		/* optional */
	u32 xres;
	u32 yres;
	u32 pixclock;
	u32 left_margin;
	u32 right_margin;
	u32 upper_margin;
	u32 lower_margin;
	u32 hsync_len;
	u32 vsync_len;
	u32 sync;
	u32 vmode;
	u32 flag;
};

修改LCD参数

struct display_info_t const displays[] = {{
	.bus = MX6UL_LCDIF1_BASE_ADDR,
	.addr = 0,
	.pixfmt = 24,
	.detect = NULL,
	.enable	= do_enable_parallel_lcd,
	.mode	= {
		.name			= "TFT7016",
		.xres           = 1024,
		.yres           = 600,
		.pixclock       = 19531,
		.left_margin    = 140,
		.right_margin   = 160,
		.upper_margin   = 20,
		.lower_margin   = 12,
		.hsync_len      = 20,
		.vsync_len      = 3,
		.sync           = 0,
		.vmode          = FB_VMODE_NONINTERLACED
} } };

修改头文件

修改include/configs/wyl_mx6ullevk.h中的panel(有两处)

panel=TFT7016

如果有问题黑屏

这是因为之前有将环境变量保存到 EMMC 中,uboot 启动以后会先从 EMMC 中读取环境 变量,如果 EMMC 中没有环境变量的话才会使用 mx6ull_alientek_emmc.h 中的默认环境变量。 如果 EMMC 中的环境变量 panel 不等于 TFT7016,那么 LCD 显示肯定不正常,我们只需要在 uboot 中修改 panel 的值为 TFT7016 即可。

setenv panel TFT7016
saveenv
reset

驱动修改

修改include/configs/wyl_mx6ullevk.h文件

①、修改 ENET1 网络 PHY 的地址。

②、修改 ENET2 网络 PHY 的地址。

③、使能 REALTEK 公司的 PHY 驱动

在第325行,以下是源文件

#ifdef CONFIG_CMD_NET
#define CONFIG_CMD_PING
#define CONFIG_CMD_DHCP
#define CONFIG_CMD_MII
#define CONFIG_FEC_MXC
#define CONFIG_MII
#define CONFIG_FEC_ENET_DEV		1

#if (CONFIG_FEC_ENET_DEV == 0)
#define IMX_FEC_BASE			ENET_BASE_ADDR
#define CONFIG_FEC_MXC_PHYADDR          0x2
#define CONFIG_FEC_XCV_TYPE             RMII
#elif (CONFIG_FEC_ENET_DEV == 1)
#define IMX_FEC_BASE			ENET2_BASE_ADDR
#define CONFIG_FEC_MXC_PHYADDR		0x1
#define CONFIG_FEC_XCV_TYPE		RMII
#endif
#define CONFIG_ETHPRIME			"FEC"

#define CONFIG_PHYLIB
#define CONFIG_PHY_MICREL
#endif
修改后
#ifdef CONFIG_CMD_NET
#define CONFIG_CMD_PING
#define CONFIG_CMD_DHCP
#define CONFIG_CMD_MII
#define CONFIG_FEC_MXC
#define CONFIG_MII
#define CONFIG_FEC_ENET_DEV		1

#if (CONFIG_FEC_ENET_DEV == 0)
#define IMX_FEC_BASE			ENET_BASE_ADDR
#define CONFIG_FEC_MXC_PHYADDR          0x2
#define CONFIG_FEC_XCV_TYPE             RMII
#elif (CONFIG_FEC_ENET_DEV == 1)
#define IMX_FEC_BASE			ENET2_BASE_ADDR
#define CONFIG_FEC_MXC_PHYADDR		0x1
#define CONFIG_FEC_XCV_TYPE		RMII
#endif
#define CONFIG_ETHPRIME			"FEC"

#define CONFIG_PHYLIB
#define CONFIG_PHY_REALTEK //改动
#endif

删除uboot中74LV595的驱动代码

board/freescale/wyl_mx6ullevk/wyl_mx6ullevk.c

①源代码如下

#define IOX_SDI IMX_GPIO_NR(5, 10)
#define IOX_STCP IMX_GPIO_NR(5, 7)
#define IOX_SHCP IMX_GPIO_NR(5, 11)
#define IOX_OE IMX_GPIO_NR(5, 8)

②修改成下面

// #define IOX_SDI IMX_GPIO_NR(5, 10)
// #define IOX_STCP IMX_GPIO_NR(5, 7)
// #define IOX_SHCP IMX_GPIO_NR(5, 11)
// #define IOX_OE IMX_GPIO_NR(5, 8)
#define ENET1_RESET IMX_GPIO_NR(5, 7)
#define ENET2_RESET IMX_GPIO_NR(5, 8)

③接着找到iox_pads[]数组,注释或者删除

源码如下

static iomux_v3_cfg_t const iox_pads[] = {
	/* IOX_SDI */
	MX6_PAD_BOOT_MODE0__GPIO5_IO10 | MUX_PAD_CTRL(NO_PAD_CTRL),
	/* IOX_SHCP */
	MX6_PAD_BOOT_MODE1__GPIO5_IO11 | MUX_PAD_CTRL(NO_PAD_CTRL),
	/* IOX_STCP */
	MX6_PAD_SNVS_TAMPER7__GPIO5_IO07 | MUX_PAD_CTRL(NO_PAD_CTRL),
	/* IOX_nOE */
	MX6_PAD_SNVS_TAMPER8__GPIO5_IO08 | MUX_PAD_CTRL(NO_PAD_CTRL),
};

④找到iox74lv_init和iox74lv_set函数,注释或者删除

⑤找到 board_init 函数,删除或注释,mx_iomux_v3_setup_multiple_pads和iox74lv_init函数调用

int board_init(void)
{
	/* Address of boot parameters */
	gd->bd->bi_boot_params = PHYS_SDRAM + 0x100;

	// imx_iomux_v3_setup_multiple_pads(iox_pads, ARRAY_SIZE(iox_pads));

	// iox74lv_init();  //注释
	...
}

⑥找到fec1_pads[]和fec2_pads[]数组,修改io配置

static iomux_v3_cfg_t const fec1_pads[]{
	...
    //最后一行添加
    MX6_PAD_SNVS_TAMPER7__GPIO5_IO07 | MUX_PAD_CTRL(NO_PAD_CTRL),
}
static iomux_v3_cfg_t const fec2_pads[] = {
	...
    //最后一行添加
    MX6_PAD_SNVS_TAMPER8__GPIO5_IO08 | MUX_PAD_CTRL(NO_PAD_CTRL),
}

⑦找到setup_iomux_fec函数

源码

static void setup_iomux_fec(int fec_id)
{
	if (fec_id == 0)
		imx_iomux_v3_setup_multiple_pads(fec1_pads,
						 ARRAY_SIZE(fec1_pads));
	else
		imx_iomux_v3_setup_multiple_pads(fec2_pads,
						 ARRAY_SIZE(fec2_pads));
}

修改为

static void setup_iomux_fec(int fec_id)
{
	if (fec_id == 0){
		imx_iomux_v3_setup_multiple_pads(fec1_pads,
						 ARRAY_SIZE(fec1_pads));
		gpio_direction_output(ENET1_RESET, 1);
		gpio_set_value(ENET1_RESET, 0);
		mdelay(20);
		gpio_set_value(ENET1_RESET, 1);
	}	
	else{
		imx_iomux_v3_setup_multiple_pads(fec2_pads,
						 ARRAY_SIZE(fec2_pads));
		gpio_direction_output(ENET2_RESET, 1);
		gpio_set_value(ENET2_RESET, 0);
		mdelay(20);
		gpio_set_value(ENET2_RESET, 1);
	}
	mdelay(150); /* 复位结束后至少延时 150ms 才能正常使用*/
}

烧录测试

串口打印->Net: FEC1,成功配置,为网口2

在uboot命令行界面配置

setenv ipaddr 192.168.0.55 //开发板 IP 地址
setenv ethaddr b8:ae:1d:01:00:00 //开发板网卡 MAC 地址
setenv gatewayip 192.168.0.1 //开发板默认网关
setenv netmask 255.255.255.0 //开发板子网掩码
setenv serverip 192.168.0.254 //服务器地址,也就是 Ubuntu 地址
saveenv //保存环境变量
ping 192.168.0.254

ping返回:

Using FEC1 device
host 192.168.0.254 is alive

测试网口2

修改include/configs/wyl_mx6ullevk.h文件

#define CONFIG_FEC_ENET_DEV		1
//修改为// 0 1 对应网口1和2
#define CONFIG_FEC_ENET_DEV		0

重新编译烧录

网线连接到enet1口

串口打印->Net: FEC0,成功配置,为网口1

直接ping 192.168.0.254能ping通

print查看ip信息。

其他需要修改的地方

board/freescale/wyl_mx6ullevk/wyl_mx6ullevk.c中checkboard函数

int checkboard(void)
{
	if (is_mx6ull_9x9_evk())
		puts("Board: MX6ULL 9x9 EVK\n");
	else
		puts("Board: MX6ULL 14x14 EVK\n");

	return 0;
}

修改为

int checkboard(void)
{
	if (is_mx6ull_9x9_evk())
		puts("Board: wyl_MX6ULL 9x9 EVK\n");
	else
		puts("Board: wyl_MX6ULL_EVK\n");

	return 0;
}

确保EMMC里面有完整的正点原子系统

由于一直使用的是SD卡测试,EMMC中的系统一直都是没有动过的

  1. 查看emmc中是否有linux镜像和.dtb文件

    =>mmc dev 1 //切换到EMMC
    
    switch to partitions #0, OK
    mmc1(part 0) is current device
    
    =>mmcinfo //mmc基本信息
    Device: FSL_SDHC
    Manufacturer ID: 15
    OEM: 100
    Name: 8GTF4
    Tran Speed: 52000000
    Rd Block Len: 512
    MMC version 4.0
    High Capacity: Yes
    Capacity: 7.3 GiB
    Bus Width: 4-bit
    Erase Group Size: 512 KiB
    
    =>fatls mmc 1:1 //查看EMMC分区1文件
      6785480   zimage
        39459   imx6ull-14x14-emmc-4.3-480x272-c.dtb
        39459   imx6ull-14x14-emmc-4.3-800x480-c.dtb
        39459   imx6ull-14x14-emmc-7-800x480-c.dtb
        39459   imx6ull-14x14-emmc-7-1024x600-c.dtb
        39459   imx6ull-14x14-emmc-10.1-1280x800-c.dtb
        40295   imx6ull-14x14-emmc-hdmi.dtb
        40203   imx6ull-14x14-emmc-vga.dtb
    
    8 file(s), 0 dir(s)
    
    =>fatload mmc 1:1 80800000 zimage //加载分区1中的zimage到ddr地址80800000中
    reading zimage
    6785480 bytes read in 362 ms (17.9 MiB/s)
    
    =>fatload mmc 1:1 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb //加载分区1中的dtb到ddr地址83000000中
    reading imx6ull-14x14-emmc-7-1024x600-c.dtb
    39459 bytes read in 24 ms (1.6 MiB/s)
    
    =>bootz 80800000 - 83000000 //加载linux
    系统启动成功
    

网络启动Linux

  • uboot下安装ftp服务器

    apt install vsftpd -y
    
  • 设置文件夹权限

    chmod -R 775 /root/tool/uboot/ftpboot
    
  • 修改ftp配置

    //备份
    cp /etc/vsftpd.conf /etc/vsftpd.conf.backup
    
    
    //添加
    local_root=/root/tool/uboot/ftpboot
    
    systemctl stop vsftpd
    systemctl start vsftpd
    systemctl stop ufw.service //关闭防火墙
    
  • 将正点原子提供的zImage和dtb文件上传到ubuntu中的某一个位置/root/tool/uboot/ftpboot

  • 在uboot命令行界面执行命令

    tftp 80800000 zimage
    tftp 83000000 imx6ull-14x14-emmc-7-1024x600-c.dtb
    

bootcmd

include/env_default.h

bootcmd 保存着 uboot 默认命令,uboot 倒计时结束以 后就会执行 bootcmd 中的命令

bootargs

bootargs 保存着 uboot 传递给 Linux 内核的参数

uboot 启动 Linux 测试

EMMC 启动 Linux 系
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'mmc dev 1; fatload mmc 1:1 80800000 zImage; fatload mmc 1:1 83000000 
imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000;'
saveenv
网络启动 Linux 系统
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw'
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 
80800000 - 83000000'
saveenv

裸机

imxdownload软件下载的bin文件,会在bin文件头部添加IVT,DCD数据。

uboot

uboot编译会生成u-boot.imx,也就是添加了头部信息的bin文件。

编译时打印信息在根目录下/tool/mkimage文件

./tools/mkimage -n board/freescale/wyl_mx6ullevk/imximage.cfg.cfgtmp -T imximage -e 0x87800000 -d u-boot.bin u-boot.imx 

可以看出添加的头部信息在imximage.cfg.cfgtmp文件下,实际上是没有imximage.cfg.cfgtmp的只有imximage.cfg文件。编译过程中会生成这个文件。这里面保存了DCD数据,DCD包含了DDR初始化。

校验

使用nxp官方提供的ddr_stress_tester工具挨个校验。主要是校准值。不校验超频会死机

地址:0x021B083C 值改成:0x01380138
地址:0x021B0848 值改成:0x40402E32
地址:0x021B0850 值改成:0x40403432

修改以下board/freescale/wyl_mx6ullevk/imximage.cfg文件

DATA 4 0x021B083C 0x41640158
DATA 4 0x021B0848 0x40403237
DATA 4 0x021B0850 0x40403C33
修改为:
DATA 4 0x021B083C 0x01380138
DATA 4 0x021B0848 0x40402E32
DATA 4 0x021B0850 0x40403432

①uboot 或 Linux 内核可以通过输入“make menuconfig”来打开图形化配置界面,menuconfig 是一套图形化的配置工具,需要 ncurses 库支持。ncurses 库提供了一系列的 API 函数供调用者 生成基于文本的图形界面,因此需要先在 Ubuntu 中安装 ncurses 库,命令如下:

sudo apt-get install build-essential
sudo apt-get install libncurses5-dev

②必须在uboot顶层Makefile中执行以下编译命令

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_wyl_emmc_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

Kconfig文件

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_wyl_emmc_defconfig之后。

.config文件中一堆内容,这些默认的内容是从顶层 Kconfig:根目录下的 Kconfig 是整个配置系统的入口,它会通过 source 语句包含其他子目录的 Kconfig。分散在整个 U-Boot 源码树中的 Kconfig 文件

图形化界面配置DNS

在command line interface --> Network commands --> dns

=> setenv dnsip 114.114.114.114
=> saveenv
=> dns www.baidu.com
111.45.11.5

原理

安装

sudo apt update
sudo apt install vsftpd -y
sudo systemctl start vsftpd
sudo systemctl enable vsftpd   # 设置开机自启

修改配置文件

在/etc/vsftpd.conf目录下

注意ftp文件目录是/srv/ftp

# 基本设置
listen=YES
listen_ipv6=NO
anonymous_enable=YES
local_enable=NO
write_enable=YES
dirmessage_enable=YES
use_localtime=YES
xferlog_enable=YES
connect_from_port_20=YES

# 匿名用户权限
anon_upload_enable=YES
anon_mkdir_write_enable=YES
anon_other_write_enable=YES

# 匿名根目录(请确保目录存在)
anon_root=/srv/ftp

# 被动模式端口范围(需防火墙放行)
pasv_enable=YES
pasv_min_port=30000
pasv_max_port=31000

# 安全限制(可选,先注释掉以减少干扰)
# chroot_local_user=YES
# allow_writeable_chroot=YES

windows访问测试

  • 文件资源管理器

    ftp://192.168.0.254
    

以IMX6ULL为例子

裸机启动

在编写led裸机程序链接生成bin文件。还要加上3k的头文件SPL程序。

内部BootROM启动一级程序,判断一个寄存器里面的值(外部flash用哪个)。将SPL程序读到内部ram中的0x09000000,执行SPL完成ddr初始化。再引导led程序到ddr中所链接的地址,从ddr链接地址位置开始执行。完成点灯。

uboot

uboot编译出来时就有uboot.bin和uboot.imx,其中uboot.imx有3k的SPL程序。

内部BootROM启动一级程序,判断一个寄存器里面的值(外部flash用哪个)。将SPL程序读到内部ram中的0x09000000,执行SPL完成ddr初始化。再将uboot加载到ddr中所链接的地址,跳转到ddr链接地址位置开始执行。

uboot启动linux

uboot启动完成之后,会自己执行一些命令将linux镜像解压等工作,随后放入ddr。跳转到ddr中linux的位置,启动linux。

安装

apt update
apt upgrade
apt install nfs-kernel-server

修改文件

在/etc/exports添加

/srv/nfs *(rw,sync,no_subtree_check,no_root_squash)

重启

systemctl status nfs-kernel-server.service
systemctl restart nfs-kernel-server.service

安装

apt install tftpd-hpa -y

修改配置文件

在/etc/default/tftpd-hpa

默认就好,不需要改

# /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure"

1.去官网下载交叉编译器

Linaro Releases 版本:gcc-arm-none-eabi-10.3-2021.10-x86_64-linux.tar.bz2

2.上传到ubuntu

3.设置环境变量

export PATH=$PATH:/root/tool/arm-linux-gnueabihf

PATH 								环境变量
= 									赋值
$PATH 								当前PATH的值
:/root/tool/arm-linux-gnueabihf 	追加一行

4.查看是否设置成功

arm-linux-gnueabihf-gcc -v

直接使用apt进行安装

apt install arm-linux-gnueabihf-gcc
.global _start @全局标号

_start:
    /*使能所有外设时钟*/
    ldr r0, =0x020c4068 @CCGR0
    ldr r1, =0xffffffff @向ccgr0要写入的数据
    str r1, [r0]

    ldr r0, =0x020c406c @CCGR1
    @ldr r1, =0xffffffff @向ccgr01要写入的数据
    str r1, [r0]

    ldr r0, =0x020c4070 @CCGR2
    str r1, [r0]

    ldr r0, =0x020c4074 @CCGR3
    str r1, [r0]

    ldr r0, =0x020c4078 @CCGR4
    str r1, [r0]
    
    ldr r0, =0x020c407c @CCGR5
    str r1, [r0]

    ldr r0, =0x020c4080 @CCGR6
    str r1, [r0]

    /*配置功能复用*/
    ldr r0, =0x020e0068
    ldr r1, =0x5
    str r1, [r0]

    /*配置电气属性*/
    ldr r0, =0x020e02f4
    ldr r1, =0x10b0 
    str r1, [r0]

    /*设置gpio*/
    ldr r0, =0x0209c004
    ldr r1, =0x8 
    str r1, [r0]

    /*打开led*/
    ldr r0, =0x0209c000
    ldr r1, =0x0
    str r1, [r0]

loop:
    b loop

将汇编编程编译后文件(.o)

arm-linux-gnueabihf-gcc -g -c leds.s -o leds.o

将.o文件链接为elf文件

arm-linux-gnueabihf -Ttext 0X87800000 leds.o -o leds.elf

将elf文件编程bin文件

arm-linux-gnueabihf-objcopy -O binary -S -g leds.elf leds.bin

反汇编

arm-linux-gnueabihf-objdump -D leds.elf > leds.dis

启动方式选择:

​ boot模式配置,通过配置引脚来配置启动方式。

设置启动设备:

​ 设置完启动模式后,还需要配置nor flash还是raw nand ,sd,mmc,spi,eeprom。

烧录的bin文件需要添加头部

  1. 要想使用cpu的一些功能。就要使用特权模式SVC模式。
  2. 汇编初始化SP指针。
  3. 设置跳转到main函数。即可使用c语言环境
.global _start
    _start:
        /*设置处理器进入svc模式*/
        mrs r0, cpsr 
        bic r0, #0x1f
        orr r0, r0, #0x13
        msr cpsr, r0

        /*设置sp指针*/
        ldr sp, =0x80200000
        b main /*跳转到c语言main函数*/

说明翻到后面

main.h

#ifndef __MAIN_H
#define __MAIN_H


/* 
 * CCM相关寄存器地址 
 */
#define CCM_CCGR0 			*((volatile unsigned int *)0X020C4068)
#define CCM_CCGR1 			*((volatile unsigned int *)0X020C406C)

#define CCM_CCGR2 			*((volatile unsigned int *)0X020C4070)
#define CCM_CCGR3 			*((volatile unsigned int *)0X020C4074)
#define CCM_CCGR4 			*((volatile unsigned int *)0X020C4078)
#define CCM_CCGR5 			*((volatile unsigned int *)0X020C407C)
#define CCM_CCGR6 			*((volatile unsigned int *)0X020C4080)

/* 
 * IOMUX相关寄存器地址 
 */
#define SW_MUX_GPIO1_IO03 	*((volatile unsigned int *)0X020E0068)
#define SW_PAD_GPIO1_IO03 	*((volatile unsigned int *)0X020E02F4)

/* 
 * GPIO1相关寄存器地址 
 */
#define GPIO1_DR 			*((volatile unsigned int *)0X0209C000)
#define GPIO1_GDIR 			*((volatile unsigned int *)0X0209C004)
#define GPIO1_PSR 			*((volatile unsigned int *)0X0209C008)
#define GPIO1_ICR1 			*((volatile unsigned int *)0X0209C00C)
#define GPIO1_ICR2 			*((volatile unsigned int *)0X0209C010)
#define GPIO1_IMR 			*((volatile unsigned int *)0X0209C014)
#define GPIO1_ISR 			*((volatile unsigned int *)0X0209C018)
#define GPIO1_EDGE_SEL 		*((volatile unsigned int *)0X0209C01C)

#endif

main.c

#include "main.h"

/*
 * @description	: 使能I.MX6U所有外设时钟
 * @param 		: 无
 * @return 		: 无
 */
void clk_enable(void)
{
	CCM_CCGR0 = 0xffffffff;
	CCM_CCGR1 = 0xffffffff;
	CCM_CCGR2 = 0xffffffff;
	CCM_CCGR3 = 0xffffffff;
	CCM_CCGR4 = 0xffffffff;
	CCM_CCGR5 = 0xffffffff;
	CCM_CCGR6 = 0xffffffff;
}

/*
 * @description	: 初始化LED对应的GPIO
 * @param 		: 无
 * @return 		: 无
 */
void led_init(void)
{
	/* 1、初始化IO复用 */
	SW_MUX_GPIO1_IO03 = 0x5;	/* 复用为GPIO1_IO03 */

	/* 2、、配置GPIO1_IO03的IO属性	
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
	SW_PAD_GPIO1_IO03 = 0X10B0;		

	/* 3、初始化GPIO */
	GPIO1_GDIR = 0X0000008;	/* GPIO1_IO03设置为输出 */

	/* 4、设置GPIO1_IO03输出低电平,打开LED0 */
	GPIO1_DR = 0X0;
}

/*
 * @description	: 打开LED灯
 * @param 		: 无
 * @return 		: 无
 */
void led_on(void)
{
	/* 
	 * 将GPIO1_DR的bit3清零	 
	 */
	GPIO1_DR &= ~(1<<3); 
}

/*
 * @description	: 关闭LED灯
 * @param 		: 无
 * @return 		: 无
 */
void led_off(void)
{
	/*    
	 * 将GPIO1_DR的bit3置1
	 */
	GPIO1_DR |= (1<<3);
}

/*
 * @description	: 短时间延时函数
 * @param - n	: 要延时循环次数(空操作循环次数,模式延时)
 * @return 		: 无
 */
void delay_short(volatile unsigned int n)
{
	while(n--){}
}

/*
 * @description	: 延时函数,在396Mhz的主频下
 * 			  	  延时时间大约为1ms
 * @param - n	: 要延时的ms数
 * @return 		: 无
 */
void delay(volatile unsigned int n)
{
	while(n--)
	{
		delay_short(0x7ff);
	}
}

/*
 * @description	: mian函数
 * @param 	    : 无
 * @return 		: 无
 */
int main(void)
{
	clk_enable();		/* 使能所有的时钟		 	*/
	led_init();			/* 初始化led 			*/

	while(1)			/* 死循环 				*/
	{	
		led_off();		/* 关闭LED   			*/
		delay(500);		/* 延时大约500ms 		*/

		led_on();		/* 打开LED		 	*/
		delay(500);		/* 延时大约500ms 		*/
	}

	return 0;
}

makefile

objs = start.o main.o #定义变量 start.o在前
ledc.bin : $(objs) #使用变量
	arm-linux-gnueabihf-ld -Ttext 0x87800000 $^ -o ledc.elf
	arm-linux-gnueabihf-objcopy -O binary -S ledc.elf $@
	arm-linux-gnueabihf-objdump -D -m arm ledc.elf > ledc.dis

%.o : %.c #    %匹配所有
	arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<

%.o : %.s
	arm-linux-gnueabihf-gcc -Wall -nostdlib -c -o $@ $<

clean :
	rm -rf *.o ledc.bin ledc.elf ledc.dis

链接脚本编写

SECTIONS{
	. = 0X87800000;
	.text :
	{
		start.o 
		main.o 
		*(.text)
	}
	.rodata ALIGN(4) : {*(.rodata*)}     
	.data ALIGN(4)   : { *(.data) }    
	__bss_start = .;    
	.bss ALIGN(4)  : { *(.bss)  *(COMMON) }    
	__bss_end = .;
}

LVGL项目

library:  所需要的库 so lib等
	arm 
	linux64
	windows
platform:  平台抽象出来的功能
	linux:
		common	<---- 所有平台可以共用的
		rv1106  <---- 板子特有功能
	mcu:
		xxx板子

sdk

Rk github

Uboot

env.txt里面存放了分区信息,可以不进行烧录

mtdparts=spi-nand0:256K(env),1M@256K(idblock),1M(uboot),4M(boot),32M(rootfs),48M(oem),32M(userdata)
sys_bootargs= ubi.mtd=4 ubi.block=0,rootfs root=/dev/ubiblock0_0 rootfstype=squashfs rk_dma_heap_cma=66M
sd_parts=mmcblk0:16K@512(env),512K@32K(idblock),4M(uboot)

env.img生成是由uboot下的/tools/mkenvimage去生成,是uboot内置的一个工具。

./tools/mkenvimage -s 0x10000 -o env.img env.txt

-s 0x10000 指定环境变量分区大小(64KB)。这个

📦 U-Boot 编译生成的文件详解

当你执行 make rv1106_defconfigmake -j16 后,会生成以下关键文件:

1. 核心镜像文件

u-boot.bin ✅ 最重要的文件

# 这是最终的 U-Boot 二进制镜像
# 包含:U-Boot 主程序 + 设备树(DTB)
ls -lh u-boot.bin
  • 内容:完整的 U-Boot 可执行代码
  • 格式:原始二进制格式
  • 用途:可以直接烧录到存储设备
  • 加载地址:由 CONFIG_SYS_TEXT_BASE 决定(RV1106 是 0x00200000

u-boot-nodtb.bin

# U-Boot 主程序(不包含设备树)
ls -lh u-boot-nodtb.bin
  • 内容:纯 U-Boot 代码,不含设备树
  • 用途:用于 FIT 镜像打包,或与外部 DTB 合并
  • 大小:比 u-boot.bin 小(少了 DTB)

u-boot.dtb

# 设备树二进制文件
ls -lh u-boot.dtb
  • 内容:硬件描述信息(内存、外设、引脚等)
  • 来源:从 .dts 文件编译而来
  • 用途:告诉 U-Boot 板子的硬件配置

2. SPL/TPL 相关文件

spl/u-boot-spl.bin

# Secondary Program Loader (二级引导程序)
ls -lh spl/u-boot-spl.bin
  • 作用:第一个运行的代码(在 DDR 初始化之前)

  • 大小:通常 < 128KB

  • 功能:

    1. 初始化 DDR 内存
    2. 初始化时钟
    3. 从存储设备加载主 U-Boot (u-boot.bin)
    4. 跳转到主 U-Boot

tpl/u-boot-tpl.bin(如果有)

# Tertiary Program Loader (三级引导程序)
ls -lh tpl/u-boot-tpl.bin
  • 作用:比 SPL 更早运行(在某些平台上)
  • 功能:初始化最基本的硬件(SRAM、时钟)

3. FIT 镜像相关文件

u-boot.img

# 带 mkimage 头部的 U-Boot 镜像
ls -lh u-boot.img
  • 内容u-boot.bin + 64 字节的头部信息

  • 头部包含:

    • 魔数(0x27051956)
    • 加载地址
    • 入口地址
    • 镜像大小
    • CRC 校验码
    • 镜像名称
  • 用途:U-Boot 可以通过 bootm 命令加载这种格式

u-boot-dtb.img

# 包含设备树的 FIT 格式镜像(如果启用 FIT)
ls -lh u-boot-dtb.img
  • 内容:U-Boot + DTB 的 FIT 格式打包
  • 用途:支持更灵活的镜像管理

u-boot.itb(Image Tree Blob)

# FIT 格式的完整镜像
ls -lh u-boot.itb
  • 内容:可以包含多个组件(SPL、U-Boot、DTB、TEE 等)

  • 格式:基于设备树的镜像描述

  • 优势:

    • 支持签名验证
    • 支持多个配置选项
    • 支持压缩

4. 调试和符号文件

u-boot.sym

# 符号表文件
ls -lh u-boot.sym
cat u-boot.sym | head -20
  • 内容:所有函数和变量的地址映射

  • 格式地址 类型 名称

  • 用途:

    • 调试时查找崩溃位置
    • 分析内存布局
    • 使用 addr2line 转换地址到源码行号

示例

00200000 T _start
00200100 t board_init_f
00210000 T main_loop

u-boot.map

bash# 链接映射文件(非常详细)
ls -lh u-boot.map
head -50 u-boot.map
  • 内容:完整的内存布局信息

  • 包含:

    • 每个段的位置和大小
    • 每个函数的地址
    • 全局变量的位置
    • 内存使用情况统计
  • 用途:

    • 优化内存使用
    • 调试链接问题
    • 分析代码大小

示例输出

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x00000000         0xffffffff

Linker script and memory map

.text           0x00200000   0x123456
                0x00200000                . = ALIGN (0x4)
                0x00200000                _start = .
 *(.text._start)
 .text._start   0x00200000       0x80 arch/arm/cpu/armv7/start.o

System.map

# 系统符号映射(类似 u-boot.sym)
ls -lh System.map
  • 内容:内核风格的符号表
  • 用途:与 Linux 内核工具兼容

5. ELF 格式文件

u-boot(无扩展名)

# ELF 格式的可执行文件
ls -lh u-boot
file u-boot
  • 格式:ELF(Executable and Linkable Format)

  • 内容:包含调试信息、符号表、重定位信息

  • 用途:

    • GDB 调试
    • 反汇编分析
    • 查看详细的段信息

常用命令

# 查看 ELF 头信息
readelf -h u-boot

# 查看段信息
readelf -S u-boot

# 反汇编
arm-linux-gnueabihf-objdump -D u-boot > u-boot.dis

# 提取特定段
arm-linux-gnueabihf-objcopy -O binary -j .text u-boot u-boot-text.bin

u-boot.srec

# Motorola S-Record 格式
ls -lh u-boot.srec
  • 格式:文本格式的十六进制数据
  • 用途:用于某些编程器或仿真器

u-boot.hex

# Intel HEX 格式
ls -lh u-boot.hex
  • 格式:文本格式的十六进制数据
  • 用途:用于 JTAG 烧录或仿真器

6. Rockchip 特定文件

当你使用 ./make.sh 编译时,还会生成:

uboot.img

bash# Rockchip 格式的 U-Boot 镜像
ls -lh uboot.img
  • 内容:经过 Rockchip 工具处理的 U-Boot

  • 特点:

    • 添加了 Rockchip 特定的头部
    • 可能进行了加密或签名
    • 对齐到特定边界

trust.img

# ARM Trusted Firmware / OP-TEE 镜像
ls -lh trust.img
  • 内容:安全世界固件(ATF 或 TEE)
  • 用途:提供安全启动支持

rv1106_idblock_*.img

# IDB(Initial Data Block)镜像
ls -lh rv1106_idblock_v*.img
  • 内容:DDR 初始化代码 + SPL
  • 用途:第一个被 ROM Code 加载的代码块
  • 烧录位置:SPI Flash/NAND 的 offset 0x0

rv1106_download_*.bin

# 完整的下载镜像
ls -lh rv1106_download_v*.bin
  • 内容:DDR + SPL + U-Boot + Trust 的完整打包
  • 用途:通过 USB 一次性烧录所有内容

📊 文件关系流程图

源代码 (.c, .S)
    ↓
编译 (.o 文件)
    ↓
链接
    ↓
┌─────────────────────────────────────┐
│         u-boot (ELF 格式)            │ ← 用于 GDB 调试
└─────────────────────────────────────┘
    ↓ objcopy (去除调试信息)
┌─────────────────────────────────────┐
│      u-boot-nodtb.bin (二进制)       │ ← 纯 U-Boot 代码
└─────────────────────────────────────┘
    ↓ cat (合并设备树)
┌─────────────────────────────────────┐
│       u-boot.bin (二进制)            │ ← 可直接烧录
└─────────────────────────────────────┘
    ↓ mkimage (添加头部)
┌─────────────────────────────────────┐
│        u-boot.img (带头部)           │ ← bootm 加载
└─────────────────────────────────────┘
    ↓ rkbin 工具打包
┌─────────────────────────────────────┐
│        uboot.img (Rockchip 格式)     │ ← 最终烧录
└─────────────────────────────────────┘

同时:
spl/ 目录生成 → spl/u-boot-spl.bin → 打包到 idblock.img

🔍 如何查看文件内容

1. 查看文件大小和类型

file u-boot*
ls -lh u-boot*

2. 查看符号表

# 查看所有符号
arm-linux-gnueabihf-nm u-boot | head -20

# 查看特定函数
arm-linux-gnueabihf-nm u-boot | grep board_init

# 按大小排序
arm-linux-gnueabihf-nm --size-sort u-boot | tail -20

3. 反汇编

# 反汇编整个文件
arm-linux-gnueabihf-objdump -D u-boot > u-boot.dis

# 反汇编特定函数
arm-linux-gnueabihf-objdump -D u-boot | grep -A 20 "<board_init_f>:"

# 交互式反汇编
arm-linux-gnueabihf-objdump -d -S u-boot | less

4. 查看段信息

# 查看所有段
readelf -S u-boot

# 查看程序头
readelf -l u-boot

# 查看符号
readelf -s u-boot

5. 提取特定部分

# 只提取 .text 段
arm-linux-gnueabihf-objcopy -O binary -j .text u-boot text_only.bin

# 提取 .rodata 段
arm-linux-gnueabihf-objcopy -O binary -j .rodata u-boot rodata_only.bin

💡 实际使用建议

对于 RV1106 SPI NAND 启动:

你需要的是:

  1. spl/u-boot-spl.bin → 打包到 rv1106_idblock_*.img
  2. u-boot.bin → 打包到 uboot.img

手动编译步骤

cd /root/rk-tools/u-boot

# 1. 配置
make rv1106-spi-nand-tb_defconfig

# 2. 修改 .config(禁用 META)
scripts/config --disable CONFIG_ROCKCHIP_META

# 3. 编译
make -j16 CROSS_COMPILE=arm-linux-gnueabihf-

# 4. 检查生成的文件
ls -lh u-boot.bin spl/u-boot-spl.bin

# 5. 使用 rkbin 工具打包(需要 make.sh 或手动调用脚本)
cd ../uboot  # 回到有 make.sh 的目录
./make.sh loader  # 打包 loader
./make.sh uboot   # 打包 uboot

U-Boot 设备树的来源


📂 设备树文件的层次结构

1. 设备树源文件(.dts/.dtsi)

arch/arm/dts/
├── rv1106.dtsi                    # RV1106 SoC 公共定义(包含)
├── rv1106-evb.dts                 # RV1106 EVB 开发板配置
├── rv1106-evb2.dts                # RV1106 EVB2 开发板配置
├── rv1106b-evb2.dts               # RV1106B EVB2 开发板配置
└── rv1106b-evb2-spi-nand.dts      # RV1106B SPI NAND 配置 ⭐ 你的板子用这个

🌳 设备树来源详解

设备树的三层结构

┌─────────────────────────────────────────────────┐
│  1. rv1106b-evb2-spi-nand.dts (板级配置)          │ ← 最顶层:具体板子
│     #include "rv1106b-evb2.dts"                  │    - 选择启动设备
│                                                  │    - 启用/禁用外设
└────────────────────┬────────────────────────────┘
                     ↓ include
┌─────────────────────────────────────────────────┐
│  2. rv1106b-evb2.dts (开发板配置)                 │ ← 中间层:开发板
│     #include "rv1103b.dtsi"                      │    - 内存配置
│     #include "rv1103b-u-boot.dtsi"               │    - 外设使能
│                                                  │    - 引脚定义
└────────────────────┬────────────────────────────┘
                     ↓ include
┌─────────────────────────────────────────────────┐
│  3. rv1106.dtsi / rv1103b.dtsi (SoC 基础定义)    │ ← 底层:芯片
│     - CPU 核心                                   │    - CPU 信息
│     - 内存控制器                                  │    - 所有外设地址
│     - 中断控制器                                  │    - 时钟定义
│     - 所有外设节点(默认 disabled)                │    - 寄存器映射
└─────────────────────────────────────────────────┘

📝 设备树编译流程

步骤 1:从 .dts 到 .dtb

编译流程图

1. 配置阶段 (make rv1106-spi-nand-tb_defconfig)
   ↓
   .config 中设置:
   CONFIG_DEFAULT_DEVICE_TREE="rv1106-evb"
   
2. 编译阶段 (make -j16)
   ↓
   a. 预处理 .dts 文件
      rv1106-evb.dts + rv1106.dtsi + ... 
      ↓ (cpp 预处理器)
      rv1106-evb.dts.pre.tmp
   
   b. 编译成 .dtb
      rv1106-evb.dts.pre.tmp
      ↓ (dtc - Device Tree Compiler)
      arch/arm/dts/rv1106-evb.dtb
   
   c. 合并到 U-Boot
      u-boot-nodtb.bin + rv1106-evb.dtb
      ↓ (cat 命令)
      u-boot-dtb.bin
   
   d. 打包成镜像
      u-boot-dtb.bin
      ↓ (mkimage)
      u-boot-dtb.img

🔍 实际查看生成的设备树

查看设备树内容的方法

# 方法 1: 反编译 .dtb 为可读的 .dts 格式
dtc -I dtb -O dts /root/rk-tools/u-boot/arch/arm/dts/rv1106b-evb2-spi-nand.dtb

# 方法 2: 使用 U-Boot 工具
fdtdump /root/rk-tools/u-boot/arch/arm/dts/rv1106b-evb2-spi-nand.dtb

# 方法 3: 在 U-Boot 命令行中查看(烧录后)
=> fdt list /
=> fdt print /memory
=> fdt print /spi_nand

📚 完整答案:设备树从哪里来?

1. 设备树的来源

设备树不是"从某处复制"的,而是:

  1. Rockchip 官方编写 → 放在 U-Boot 源码的 arch/arm/dts/ 目录
  2. 编译时自动生成 → 通过 dtc (Device Tree Compiler) 编译成二进制

2. 设备树文件的位置

U-Boot 源码目录:
/root/rk-tools/u-boot/arch/arm/dts/
├── rv1106.dtsi                    # SoC 基础定义(Rockchip 编写)
├── rv1106-u-boot.dtsi             # U-Boot 特定配置
├── rv1106-evb.dts                 # EVB 开发板配置
├── rv1106b-evb2.dts               # EVB2 开发板配置
└── rv1106b-evb2-spi-nand.dts      # ⭐ SPI NAND 板子配置

3. 设备树的继承关系

dts// rv1106b-evb2-spi-nand.dts (你的板子)
/dts-v1/;
#include "rv1106b-evb2.dts"        // ← 包含开发板配置

/ {
    model = "Rockchip RV1106B EVB2 Board";
    compatible = "rockchip,rv1106b-evb2-spi-nand", "rockchip,rv1106b";
    
    chosen {
        stdout-path = &uart0;
        u-boot,spl-boot-order = &spi_nand;  // ← 从 SPI NAND 启动
    };
};

&spi_nand {
    u-boot,dm-spl;                  // ← SPL 阶段启用 SPI NAND
    status = "okay";                // ← 启用 SPI NAND
};
dts// rv1106b-evb2.dts (开发板层)
/dts-v1/;
#include "rv1103b.dtsi"            // ← 包含 SoC 定义
#include "rv1103b-u-boot.dtsi"     // ← U-Boot 配置

/ {
    model = "Rockchip RV1106B EVB2 Board";
    
    chosen {
        u-boot,spl-boot-order = &spi_nor, &emmc;  // 默认从 SPI NOR/eMMC 启动
    };
};

&spi_nand {
    status = "disabled";            // ← 默认禁用 SPI NAND
};
dts// rv1106.dtsi / rv1103b.dtsi (SoC 层 - Rockchip 编写)
/ {
    compatible = "rockchip,rv1106";
    
    cpus {
        cpu0: cpu@f00 {
            compatible = "arm,cortex-a7";
            reg = <0xf00>;
        };
    };
    
    // 所有外设节点(默认 disabled)
    spi_nand: spi@20d40000 {
        compatible = "rockchip,rv1106-sfc";
        reg = <0x20d40000 0x1000>;
        interrupts = <GIC_SPI 59 IRQ_TYPE_LEVEL_HIGH>;
        clocks = <&cru CLK_SFC>;
        status = "disabled";        // ← 默认禁用,由板级配置启用
    };
    
    // ... 更多外设定义
};

🔧 设备树编译过程


✅ 最终答案

设备树 u-boot.dtb / u-boot-dtb.img 的来源:

  1. 源文件位置

    /root/rk-tools/u-boot/arch/arm/dts/
    ├── rv1106.dtsi              # Rockchip 官方编写的 SoC 定义
    ├── rv1106-evb.dts           # Rockchip 官方编写的开发板配置
    └── rv1106b-evb2-spi-nand.dts # 你的板子配置
    
  2. 编译过程

    # 步骤 1: 预处理(展开 #include)
    cpp rv1106-evb.dts > rv1106-evb.dts.pre.tmp
    
    # 步骤 2: 编译成二进制 DTB
    dtc -I dts -O dtb -o rv1106-evb.dtb rv1106-evb.dts.pre.tmp
    
    # 步骤 3: 复制到输出目录
    cp rv1106-evb.dtb dts/dt.dtb
    
    # 步骤 4: 合并到 U-Boot
    cat u-boot-nodtb.bin dts/dt.dtb > u-boot-dtb.bin
    
    # 步骤 5: 打包成镜像
    mkimage -A arm -T firmware -d u-boot-dtb.bin u-boot-dtb.img
    
  3. 由谁编写

    • Rockchip 官方工程师编写和维护
    • ✅ 放在 U-Boot 源码的 arch/arm/dts/ 目录
    • ✅ 随 U-Boot 源码一起发布
    • ✅ 你可以根据需要修改或创建新的 .dts 文件
  4. 如何查看内容

    # 反编译为可读格式
    dtc -I dtb -O dts /root/rk-tools/u-boot/arch/arm/dts/rv1106-evb.dtb
    
    # 或在 U-Boot 命令行中
    => fdt list /
    => fdt print /memory
    

💡 关键点总结

  • 设备树不是"从某处复制",而是 Rockchip 官方编写并包含在 U-Boot 源码中
  • 编译时自动生成 .dtb 文件
  • 三层结构:SoC 定义 (.dtsi) → 开发板配置 (.dts) → 具体板子配置 (.dts)
  • 通过 #include 继承,避免重复定义
  • 最终合并到 u-boot.bin 形成 u-boot-dtb.bin

开发板_defconfig 文件在configs目录,这个和kernel有出入

方式1:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- rv1106_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j8

方式2:

在uboot的顶层makefile中添加

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

直接执行

make distclean
make rv1106_defconfig
make -j8

方式3:

直接执行rk提供的uboot下的make.sh脚本。这个步骤会把rkbin相关的东西也编译。

大多数Rockchip 芯片(包括 RK、RV 系列)的 BootROM 启动逻辑基本遵循这个模式

Rockchip(RK/RV 系列)的启动介质选择机制与 i.MX 完全不同RK 芯片一般不通过引脚直接选择启动介质,而是依靠 BootROM 内置的固定优先级顺序OTP/eFuse 配置的启动顺序

SPI NAND

  • 特性:NAND 介质出厂可能包含坏块,且擦写次数多后坏块会增多。

  • 策略:用 "逐块扫描 + 签名匹配" 的方式。

BootROM 初始化 SPI NAND 控制器
         ↓
读取 Block 0 (0x000000 - 0x01FFFF) → 检查是否有 BOOT 签名?
         ↓  (没有)
读取 Block 1 (0x020000 - 0x03FFFF) → 检查是否有 BOOT 签名?
         ↓  (没有)
读取 Block 2 (0x040000 - 0x05FFFF) → 检查是否有 BOOT 签名?
         ↓  (找到! 签名 "BOOT" = 0x544F4F42)
BootROM 根据 ID Block 的描述,将 **TPL**(FlashData,即 ddr_*.bin)加载到 SRAM(如 0x10000000)
         ↓
跳转到 TPL 执行 → TPL 初始化 DDR
         ↓
TPL 从 NAND 的指定位置(由 ID Block 或固定偏移)读取 **SPL**(FlashBoot,即 spl_spi_nand_*.bin)到 DDR 中(如 0x60000000)
         ↓
跳转到 SPL 执行 → SPL 初始化其他外设、加载 U‑Boot
         ↓
U‑Boot 启动内核

SPI NOR

  • 特性:无坏块问题,支持直接按字节寻址,随机读取性能好。
  • 策略:固定偏移扫描(通常从 0x00000000 开始)。BootROM 会直接读取 NOR 起始地址处的数据,检查是否有有效签名(如 BOOT 或 ID Block 标志)。
  • 某些 BootROM 也支持“双备份”模式:检查 offset 0 和 offset 某个页大小(如 0x1000)两个位置,但不做全盘逐块扫描。

eMMC / SD 卡

  • 特性:基于扇区(512 字节(0x4000))访问,有 MBR/GPT 分区表,不存在物理坏块(由卡内部控制器管理)。
  • 策略:
    • 优先检查用户分区 LBA 0(第一个扇区),寻找 BOOT 签名或 ID Block 结构。
    • 如果未找到,部分 BootROM 会尝试检查 eMMC 的 Boot Area Partition 1/2(若支持且配置了)。
    • 一般不会扫描多个 LBA,因为 SD/eMMC 的坏块管理由设备自身处理,BootROM 无需跳过坏块。
  • 注意:某些 Rockchip 芯片允许通过 efuse 或 OTP 配置从 eMMC 的特定偏移(如 4KB、8KB)启动,但默认仍是固定扇区 0。

其他介质(如 SDIO、UART 等)

  • 同样使用签名匹配,但扫描方式不同。例如 UART 下载模式是等待主机发送包含签名的数据流,而非扫描存储区域。

当uboot和kernel&设备树编译完成之后,就要用到rkbin这个工具了

输入文件

u-boot.bin
zImage
xxx.dtb

目录结构

rkbin/
├── bin/rv11/          ← RV1106 的预编译固件(DDR、SPL、TEE、USB下载固件等)
├── tools/              ← 打包工具(boot_merger, trust_merger, mkimage 等)
├── RKBOOT/             ← Loader 打包配置文件(*.ini)
├── RKTRUST/            ← Trust/TEE 打包配置文件(*.ini)
├── scripts/            ← 辅助脚本
└── img/                ← 其他镜像
原料工具产物烧录位置
u-boot.bin 或 spl/u-boot-spl.binmkimage -T rksduboot.imgNAND: uboot分区
DDR (闭源二进制)boot_mergeridblock.imgNAND: idblock分区
USB (闭源二进制)-download.bin烧录工具用
TEE (闭源二进制)trust_mergertrust.img可选
zImage + dtb + resource.imgmkimage -f boot.itsboot.imgNAND: boot分区
env.txtmkenvimageenv.imgNAND: env分区
rootfs目录mkfs.ubifs + ubinizerootfs_*.ubi
rootfs.img (软链接)
NAND: rootfs分区
oem目录同上oem_*.ubiNAND: oem分区
userdata目录同上userdata_*.ubiNAND: userdata分区
以上全部afptool + rkImageMakerupdate.img整包

建议直接用配置好的docker环境

直接在windows安装docker,再加载提供的镜像。再使用vscode安装dev contarner插件,直接使用界面进行开发。

安装docker

省略

加载镜像

docker load -i rk-develop.tar

启动容器

  • windows下(必须使用绝对路径)
docker run -it -d --name rk-develop --privileged -v E:\rk-develop\v-share:/home 4a59acf452d5 /bin/bash
  • linux下(可以使用相对路径)
docker run -it -d --name rk-develop --privileged -v ./v-share:/home 4a59acf452d5 /bin/bash

RK的github地址

https://github.com/rockchip-linux

RK的SOC的支持包都在这里面。

下载

尽量直接在github网页上下载zip文件。直接使用git clone命令会下载很慢。

  • rkbin
  • u-boot
  • kernel 主要要下载5.10以上的分支

下载交叉编译器

根据下载的uboot中的make.sh脚本中可以看到rk推荐的交叉编译器版本。

https://developer.arm.com/Downloads/-/Legacy%20Linaro%20GNU%20Toolchains
  • 64位
gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu
  • 32位
gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf

配置交叉编译器

vim /etc/profile
export PATH=$PATH:/rk-tools/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin

export PATH=$PATH:/rk-tools/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
source /etc/profile
arm-linux-gnueabihf-gcc -v
aarch64-linux-gnu-gcc -v

虚拟机下载必要的环境

编译需要gcc等

apt-get update
apt-get install -y git ssh make gcc gcc-multilib g++-multilib module-assistant expect g++ gawk texinfo libssl-dev bison flex fakeroot cmake unzip gperf autoconf device-tree-compiler libncurses5-dev pkg-config bc python-is-python3 passwd openssl openssh-server openssh-client vim file cpio rsync

docker 一键部署

使用docker,docker镜像打包时已经包含了一些环境。该docker的基础镜像是ubuntu22.04,所以可以使用一些ubuntu的命令

docker镜像版本规定

  • rk-docker-development:1.0.0

    这个版本只有虚拟机必要的环境gcc等,没有rk的uboot,rkbin等。

  • rk-docker-development:2.0

    这个版本在1.0.0的基础上把rk git的rkbin,u-boot,rkdeveloptool,kernel4.4,交叉编译器。注意没有配置交叉环境

  • rk-docker-development:3.0

    这个版本配置了所有的环境,交叉编译器,gcc,还有uboot,rkbin等源码。注意,没有rk其他工具,比如rknpu,rknn-toolkit等

  • rk-docker-development:4.0

    待续------

手动编译未使用env.img

  • download : 不输入地址

  • idblock : 0x0000 0000 (占用8M)

  • uboot : 0x0080 0000 (占用512K)

  • boot : 0x0088 0000 (占用4M)

  • rootfs : 0x00C8 0000 (占用剩余的256M减4M减512K减8M)

  • mtd启动命令(rk写法-根据块)

    mtd dev 1; mtd read 0x02000000 0x4400 0x2000; bootm 0x02000000
    

    第0x4400块对应0x0088 0000,0x2000对应读4M到0x02000000RAM地址中

手动编译使用env.img

首先

在rockchip的github下载的内容,这一步已经在docker里面有这些内容了。我自己配置了docker环境包含了所有的开发环境(rk官方资料以及可编译的必要环境)

  • 选开发板,编译生成u-boot.bin

这个步骤也会生成uboot.img这个与imx6ull编译uboot产生的uboot.imx异曲同工。rk厂商的uboot的makefile也提供了类似nxp的功能。如果是uboot厂商编译出来的可能就没有这个相似的文件。

  • 选开发板,编译生成kernel(rv系列必须是5.10以上的内核版本,rk只提供了5.10以上的版本)

生成zImage和dtb设备树文件以及一些资源文件resource.img。

生成完download.bin&idblock.img后,在编译uboot时可以不指定ini文件。

注意:

  • 官方提供的串口的波特率比较高:1500000
  • RV1106MINIALL_SPI_NAND_TB.ini这种带有TB的都是启动链不同的。可以前往rkbin生成spl注意点.md
  • RV1106MINIALL.ini是rv1106芯片最基础的SPL,通用型SPL,这个SPL会去nand flash的0x00800000找uboot。

进入到rkbin

./tools/boot_merger RKBOOT/RV1106MINIALL.ini

rk-github编译流程-rkbin使用1

TB 就是 Tiny Boot

  • 正常理解的启动链一般是:
  • BootROM -> idblock/SPL -> U-Boot -> 停在 U-Boot 或再启动内核
  • TB 路线更像是:
  • BootROM -> Tiny SPL/Trust 组合 -> 直接找 boot 分区/系统镜像 -> 尽快起系统
#必须修改为RV1106TOS.ini否则肯定会进入Trust TB(tiny boot)
#CONFIG_TRUST_INI="RV1106TOS_TB.ini"
CONFIG_TRUST_INI="RV1106TOS.ini"
  • TB :
    • 默认继续找系统分区
    • 可能带 MCU 检查
    • 可能带 Trust tiny-boot 路径
    • 没有系统分区时就容易卡死
  • 普通 U-Boot 路线:
    • 更容易停在命令行
    • 更适合调试
    • 逻辑更直观
    • 你只烧 idblock + uboot 往往就能看到主 U-Boot banner

编译uboot

添加串口打印输出

  • 路径

    /uboot/u-boot/board/rockchip/evb_rv1106/evb_rv1106.c
    
  • 添加

    int rk_board_init(void)
    {
    	printf("\n[MY_MOD] =====================================\n");
    	printf("[MY_MOD] U-Boot 已修改!当前为自定义版本!\n");
    	printf("[MY_MOD] =====================================\n\n");
    	return 0;
    }
    

uboot使用的设备树在:配置文件中

CONFIG_DEFAULT_DEVICE_TREE="rv1106-evb"

注意:

  • rkbin必须在uboot同级目录并且名称也必须时rkbin,可以在make.sh进行修改

    RKBIN_TOOLS=../rkbin/tools
    

使用rk-github仓库的uboot有两种编译方式。

  1. rk提供的build.sh编译
  2. 原生编译(Makefile),不推荐。

2. 手动编译

2.1 rkbin生成

cd rkbin
./tools/boot_merge RKBOOT/RV1106MINIALL.ini
rk-github编译流程-uboot编译5

2.2 修改xxx_defconfig

  • 复制一个新的
cp ./configs/rv1106-spi-nand-tb-nofastae_defconfig ./configs/rv1106-spi-nand-nomcu_defconfig
  • 配置内容,注释/删除
CONFIG_LOADER_INI="RV1106MINIALL.ini"
CONFIG_TRUST_INI="RV1106TOS.ini"
  • 检查nand配置项
rk-github编译流程-uboot编译2
  • 设置uboot倒计时
CONFIG_BOOTDELAY=3

2.3 手动make编译

如果删除了xxx_defconfig里面的CONFIG_LOADER_INI,CONFIG_TRUST_INI内容只能进行手动make。

make rv1106-spi-nand-nomcu_defconfig
make -j16 #不使用make.sh需要指定编译核数

2.4 在串口工具调试

进入串口工具之后,有3秒倒计时

ctrl + c #成功进入uboot

2.5 注意

  • 串口打印 Trying fit image at 0x4000 sector,说明从4000扇区开始读uboot,即0x00800000(nand 1扇区2k,共2048个块*16扇区)

  • 如果打印了,说明要进行env.img校验

    ENVF: !bad CRC @ 0x0
    ENVF: !bad CRC @ 0x0
    No env partition table
    ## Unknown partition table type 0
    

    注释或者删除即可处理这个问题。

    CONFIG_ENVF=y
    
  • 改uboot的env.img的获取地址

    CONFIG_ENV_NAND_OFFSET= 0x0 #放入第0扇区
    CONFIG_ENV_NAND_SIZE = 0x40000 #大小,按字节,而不是扇区
    
  • 如果要修改uboot的启动地址

    • xxx_defconfig修改,或者临时修改在**.config**文件
    CONFIG_MTD_BLK_U_BOOT_OFFS=0x2000 #0x00400000 / 512 = 0x2000
    
    • 利用uboot生成的spltpl

    • tplspl放到rkbin/bin目录

      u-boot-tpl.bin 和 u-boot-spl.bin
      
    • 创建在RKBOOT目录创建RV1106MINIALL_MY_TPL_SPL.ini,复制RV1106MINIALL.ini内容到RV1106MINIALL_MY_TPL_SPL.ini

      [CHIP_NAME]
      NAME=RV1106
      [VERSION]
      MAJOR=1
      MINOR=1
      [CODE471_OPTION]
      NUM=1
      Path1=bin/rv11/u-boot-tpl.bin
      Sleep=1
      [CODE472_OPTION]
      NUM=1
      Path1=bin/rv11/rv1106_usbplug_v1.09.bin
      [LOADER_OPTION]
      NUM=2
      LOADER1=FlashData
      LOADER2=FlashBoot
      FlashData=bin/rv11/u-boot-tpl.bin
      FlashBoot=bin/rv11/u-boot-spl.bin
      [OUTPUT]
      PATH=rv1106_download_v1.15.108.bin
      IDB_PATH=rv1106_idblock_v1.15.102.img
      [SYSTEM]
      NEWIDB=true
      [FLAG]
      471_RC4_OFF=true
      RC4_OFF=true
      CREATE_IDB=true
      
    • tpl可以不改也没必要改,但也提供思路

      [CHIP_NAME]
      NAME=RV1106
      [VERSION]
      MAJOR=1
      MINOR=1
      [CODE471_OPTION]
      NUM=1
      Path1=bin/rv11/rv1106_ddr_924MHz_v1.15.bin
      Sleep=1
      [CODE472_OPTION]
      NUM=1
      Path1=bin/rv11/rv1106_usbplug_v1.09.bin
      [LOADER_OPTION]
      NUM=2
      LOADER1=FlashData
      LOADER2=FlashBoot
      FlashData=bin/rv11/rv1106_ddr_924MHz_v1.15.bin
      FlashBoot=bin/rv11/u-boot-spl.bin #只改动这里
      [OUTPUT]
      PATH=rv1106_download_v1.15.108.bin
      IDB_PATH=rv1106_idblock_v1.15.102.img
      [SYSTEM]
      NEWIDB=true
      [FLAG]
      471_RC4_OFF=true
      RC4_OFF=true
      CREATE_IDB=true
      

1. make.sh编译uboot

1.1 配置交叉编译环境

  • 修改uboot根目录的make.sh,注意要绝对路径
CROSS_COMPILE_ARM32=/root/rk-tools/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
CROSS_COMPILE_ARM64=/root/rk-tools/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-
  • 修改uboot根目录的makefile
ARCH = arm
CROSS_COMPILE = arm-linux-gnueabihf-

1.2 修改xxx_defconfig

对于现有的uboot的_defconfig是不能满足我们需要的功能的。

  • 复制一个新的
cp ./configs/rv1106-spi-nand-tb-nofastae_defconfig ./configs/rv1106-spi-nand-nomcu_defconfig
  • 配置内容

由于官方提供得spi-nand都有TB,启动链方式要验证kernel。所以要配置RV1106TOS.ini

CONFIG_LOADER_INI="RV1106MINIALL.ini"
CONFIG_TRUST_INI="RV1106TOS.ini"
  • 检查nand配置项
rk-github编译流程-uboot编译2

1.3 使用make.sh选择xxx_defconfig文件

这一步相当于make xxx_defconfig,make.sh帮我们做了这个步骤。

#注意make.sh会自己拼接_defconfig
./make.sh rv1106-spi-nand-nomcu

等待执行完

rk-github编译流程-uboot编译3

1.4 烧录

rk官方提供的RV1106MINIALL.ini这个SPL会去nand flash的0x00800000找uboot。

如果要指定SPL找uboot的地址。需要在配置文件中配置

CONFIG_SYS_LOAD_ADDR = 

rk-github编译流程-uboot编译4

命令

make distclean
make clean
make xxx_defconfig
make menuconfig
make xxx.config

编译kernel

- 注意:

  • 默认情况下,rv1106_defconfig使用的是rv1106g-evb1-v11-spi-nand-cvr.dts设备树

- 配置交叉编译器

在kernel顶层makefile添加

ARCH = arm
CROSS_COMPILE = arm-linux-gnueabihf-

- 配置xxx_defconfig(必须)通过下面的合并xxx.config也能实现

CONFIG_MTD=y
CONFIG_MTD_NAND=y
CONFIG_MTD_SPI_NAND=y
CONFIG_MTD_UBI=y
CONFIG_UBIFS_FS=y
CONFIG_MTD_CMDLINE_PARTS=y
CONFIG_MTD_OF_PARTS=y
CONFIG_SPI_ROCKCHIP_SFC=y

- 选择xxx_defconfig

kernel顶层执行:

make rv1106_defconfig

- 合并xxx.config

这一步主要是将自定义或者其他的xxx.config合并到现有的kernel根目录下的**.config**

又称:基础 defconfig + 若干 fragment

make rv1106-nand.config
make rv1106-cvr.config #或 make rv1106-evb.config

工作原理:

  • rv1106_defconfig 会生成基础的 .config 文件
  • rv1106-nand.config 会被识别为配置片段(fragment),通过 merge_config.sh 脚本合并到现有的 .config
  • 合并后会自动执行 olddefconfig 来解决依赖关系

- 执行编译

  • 编译所有

    make -j16
    
  • 只编译设备树

    make dtbs
    
  • 编译单个设备树

    设备树名通常与对应的 .dts 文件名相同

    make <dts文件名>.dtb
    

编译结果:

  • arch/arm/boot/zImage文件
  • arch/arm/boot/dts/* 所有设备树文件
    • rv1106_defconfig使用的是rv1106g-evb1-v11-spi-nand-cvr.dts设备树

Linux 内核的标准构建目标就是这些:

  • zImage / Image
  • dtbs
  • modules

基于内存的临时文件系统(ramdisk)

在**.its**文件进行配置

现在不需要,所以在配置中关闭xxx_defconfig/.config

#CONFIG_BLK_DEV_INITRD =y

resources.img用来存放开机logo,开机视频等资源文件

rkbin/tools/resource_tool 会工具在当前目录生成resource.img

./rkbin/tools/resource_tool --pack \
    --image=resource.img \
    rv1106g-evb1-v11-spi-nand-cvr.dtb \      # 必须是第一个文件
    logo.bmp \
    logo_kernel.bmp

resources.img中的dtb

  • 主 DTB 放在 FIT 的 fdt 节点,U-Boot 传递给内核。
  • resources.img 放在 FIT 的 loadables 节点,U-Boot 只是把它搬运到约定地址,之后内核通过主 DTB 中的 rockchip,resource 属性找到它并解析。
  • 两者职责不同,缺一不可。

Android boot.img(暂时用不到)

kernel/scripts/mkbootimg

mkbootimg --kernel zImage \
          --ramdisk ramdisk.img \ #可以留空
          --second dtb_file.dtb \          # dtb 装入 second 区
          --cmdline "console=ttyS2,115200n8 root=/dev/mmcblk0p2 rw init=/init" \
          --base 0x02000000 \              # RV1106 DDR 起始可能不同,需确认
          --kernel_offset 0x00008000 \
          --ramdisk_offset 0x04000000 \
          --second_offset 0x00f00000 \
          --tags_offset 0x00000100 \
          -o boot.img

目标:

  • 将多个组件(内核、DTB、ramdisk、资源文件等)打包成一个镜像文件。让uboot能快速启动内核。

注意点:

  • 128M RAM:0x0800 0000

  • uboot需要将boot.img镜像放到ddr中某个位置

mtd read 0x02000000 0x04400000 0x800000
  • 从nand的0x04400000位置读取0x800000(8M)数据到ddr的0x02000000

  • 内核load = <0x03000000>; 尽量不和boot.img在ddr的8M地址冲突

  • 内核启动之后,会释放uboot占用的ddr,boot.img占用的ddr也释放,uboot通过FIT加载的设备树的ddr也会释放,除了内核不会被释放(0x03000000开始的部分)

1. 创建boot.its文件

/dts-v1/;
/ {
    description = "RV1106 NAND boot image";
    #address-cells = <1>;

    images {
        kernel@1 {
            data = /incbin/("zImage");
            type = "kernel";
            arch = "arm";
            os = "linux";
            compression = "none";
            load = <0x03000000>; // 告诉uboot将内核放到ddr的0x03000000
            entry = <0x03000000>; 
        };
        fdt@1 {
            data = /incbin/("rv1106g-evb1-v11-spi-nand-cvr.dtb");
            type = "flat_dt";
            arch = "arm";
            compression = "none";
            // load = <0x02800000>; 不分配地址,让uboot自己分配
        };
		/*
		resource@1 {
            data = /incbin/("resource.img");
            type = "firmware";
            arch = "arm";
            compression = "none";
            load = <0x06000000>;
        };
		*/
    };

    configurations {
        default = "conf@1";
        conf@1 {
            kernel = "kernel@1";
            fdt = "fdt@1";
			//loadables = "resource@1";
        };
    };
};

2. **生成FIT格式的数据

../uboot/tools/mkimage -f ./boot.its ./boot.img

3. 如果用到resource,需要修改设备树

rv1106g-evb1-v11-spi-nand-cvr.dts

/ {
    reserved-memory {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        /* 假设 resources.img 大小为 8MB */
        resource: resource@0x3f800000 {
            compatible = "rockchip,resource";
            reg = <0x3f800000 0x800000>; /* 从 1016MB 处开始,预留 8MB */
            no-map;
        };
    };
};
  • FIT做打包只依赖与uboot下的tools/mkimage,与uboot本身并没有关系,其实就是说,uboot能识别FIT镜像,所以要提供FIT镜像给到uboot。所以要在uboot的编译文件xxx_defconfig/.config配置开启。

    CONFIG_FIT=y
    #CONFIG_FIT_ENABLE_SHA256_SUPPORT=y #关闭校验
    #其他的FIT地址不列举了
    

必须在uboot的xxx_defconfig/.config进行配置

例子在:rv1106-spi-nand-nomcu_defconfig.md

CONFIG_FIT=y
# CONFIG_SPL_KERNEL_BOOT is not set #关闭spl启动内核

#CONFIG_ENVF=y #关闭校验env.img
#CONFIG_ENV_IS_NOWHERE=y
#CONFIG_ANDROID_BOOT_IMAGE=y #安卓镜像
#CONFIG_FIT_ENABLE_SHA256_SUPPORT=y
#CONFIG_FIT_ENABLE_RSASSA_PKCS=y
CONFIG_BOOTDELAY=3 #3s后无操作的话,启动kernel
CONFIG_BAUDRATE=115200
CONFIG_MTD=y
#CONFIG_CMD_MTD=y
#CONFIG_MTD_PARTITIONS=y #开启这个会导致mtd dev 1命令找不到设备号
CONFIG_ENVF=y
CONFIG_ENV_NAND_OFFSET=0x400000
CONFIG_ENV_NAND_SIZE=0x400000

3. 配置dev.img方式启动

CONFIG_ENVF=y
CONFIG_ENV_NAND_OFFSET=0x400000#烧录地址
CONFIG_ENV_NAND_SIZE=0x400000#大小

CONFIG_ENV_NAND_OFFSET=0x400000:设置 ENV 的物理偏移量为 4M( env 分区位置即烧录地址)。

CONFIG_ENV_NAND_SIZE=0x400000:设置 ENV 大小为 4M。

# 1. MTD 分区表(SPI NAND)
mtdparts=spi-nand0:4M(idblock),4M(env),512K(uboot),4M(boot),-(rootfs)
#    		↑          ↑                                        ↑
#         设备名    分区定义                                  剩余空间给rootfs

# 2. 内核启动参数
sys_bootargs= ubi.mtd=4 root=ubi0:rootfs rootfstype=ubifs rk_dma_heap_cma=66M
#              ↑         ↑              ↑                    ↑
#           MTD索引4  UBI卷名      文件系统类型        DMA内存预留66M

# 3. SD卡分区表(可选,用于SD卡启动)
sd_parts=mmcblk0:16K@512(env),512K@32K(idblock),4M(uboot)
../uboot/tools/mkenvimage -s 0x400000 -o env.img .env.txt
  • env注意点

    这3者必须完全对应。Config,mkenvimage,烧录地址。

U-Boot ConfigCONFIG_ENV_OFFSET=0x400000 CONFIG_ENV_SIZE=0x400000告诉 U-Boot 去 0x400000 读,读 4M
mkenvimage-s 0x400000生成4M 大小的镜像
烧录地址0x400000env.img 烧录到 Flash 的 0x400000 处

Rockchip 平台的特殊性

再次强调,Luckfox RV1106 默认使用 CONFIG_ENVF (Environment Framework)

  • 在这种模式下,U-Boot 可能忽略 CONFIG_ENV_OFFSET,而是通过 MTD 分区表 来查找名为 env 的分区。

  • 如果是这种情况,只要你:

    1. .env.txt 中的 mtdparts 定义了 env 在正确位置。
    2. 烧录工具根据 mtdpartsenv.img 烧录到正确位置。
    3. U-Boot 能正确解析 MTD 分区。

    那么即使 CONFIG_ENV_OFFSET 没改(或者是错的),系统可能仍然能正常工作,因为 ENVF 框架更智能。

  • 但是,如果你禁用了 CONFIG_ENVF 或强制使用 CONFIG_ENV_IS_IN_NAND,那么必须严格一致

启动命令解析(针对1,2)

mtd dev 1
mtd read 0x02000000 0x4400 0x4000
iminfo 0x02000000
bootm 0x02000000
  • **mtd:**选 NAND 设备
  • mtd read
    • 0x4400:nand存放boot.img镜像快地址 (0x0088 0000/512)
    • 0x02000000 :DDR 加载地址
    • 0x4000: boot.img 大小 0x0080 0000(8M) /512
  • **bootm:**启动 FIT 镜像(必须是boot.img)
  • **iminfo:**检查镜像

1. uboot命令行启动

在本篇md中启动命令解析这一章节

这种方式无法挂载文件系统!!

2. uboot配置修改启动

  • 目标:直接uboot直接启动kernel。不需要env.img配置

uboot启动kernel详细在:env.img文件&uboot启动kernel流程.md

  • 2.1 uboot/include/configs/rv1106_common.h修改

    include"rockchip-common.h"之后添加

#include "rockchip-common.h" 

#ifndef RKIMG_BOOTCOMMAND
#define RKIMG_BOOTCOMMAND	"mtd dev 1; mtd read 0x02000000 0x4400 0x4000; bootm 0x02000000"
#endif
  • 2.2 将
    #undef RKIMG_BOOTCOMMAND
    #ifdef CONFIG_FIT_SIGNATURE
    #define RKIMG_BOOTCOMMAND		\
    	"boot_fit;"
    #else
    #define RKIMG_BOOTCOMMAND		\
    	"boot_fit;"			\
    	"boot_android ${devtype} ${devnum};"
    #endif
    
  • 2.3改为
    #undef RKIMG_BOOTCOMMAND
    #define RKIMG_BOOTCOMMAND	"mtd dev 1; mtd read 0x02000000 0x4400 0x4000; bootm 0x02000000"
    

这种方式无法挂载文件系统!!

在xxx_defconfig文件中有一个配置ENVF(Environment File) 功能

CONFIG_ENVF=y

什么是 ENVF?

  • ENVF 是 Rockchip 的一种机制,让 U-Boot 从外部文件(env.img)读取环境变量
  • 环境变量包括:bootargs(内核启动参数)、bootcmd(启动命令)、mtdparts(分区信息)等
  • 这个 env.img 文件通常存储在 SPI NAND 的某个分区中

启动流程

1. U-Boot 启动
2. 从 SPI NAND(emmc,nor等) 读取 env.img 文件
3. 从 env.img 中获取 bootargs 和 bootcmd
4. 执行 bootcmd(通常是 "boot_fit")
5. 使用 bootargs 启动 Linux 内核

boot_fit一个自定义的 U‑Boot 环境变量,通常用来封装“加载并启动 FIT(Flattened Image Tree)镜像”的一系列操作。它就是第4步中的实际执行者,连接了环境变量与内核引导的“最后一公里”。

boot_fit 最终会调用 bootm(或 booti)等命令,解析 FIT 镜像,把内核、设备树、initramfs 加载好,并把 bootargs 传递给内核,跳转执行。此后 Linux 接管系统。

下载

https://buildroot.org/

不要使用root账户

#修改root密码
passwd root 
#创建一个新账户
useradd -m builder
#删除
userdel builder
#使用root账户给builder用户分配解压的buildroot文件夹权限
chown -R builder:builder /home/builder/buildroot
#后面再使用builder用户
su builder

修改分区规划

.env.txt文件

mtdparts=spi-nand0:4M(env),4M(idblock),512K(uboot),4M(boot),30M(oem),10M(userdata),-(rootfs)
sys_bootargs= ubi.mtd=6 root=ubi0:rootfs rootfstype=ubifs rk_dma_heap_cma=66M;

配置Target options

make menuconfig
Target options
        -> Target Architecture = ARM (little endian)
        -> Target Binary Format = ELF
        -> Target Architecture Variant = cortex-A7
        -> Target ABI = EABIhf
        -> Floating point strategy = NEON/VFPv4
        -> ARM instruction set = ARM

配置Toolchain

Toolchain
   -> Toolchain type = External toolchain
   -> Toolchain = Custom toolchain //选择用户的交叉编译器
   -> Toolchain origin = Pre-installed toolchain
   -> Toolchain path =/root/gcc-linaro-6.5.0-2019.12-x86_64_arm-linux-gnueabihf //交叉编译器路径
   -> Toolchain prefix = arm-linux-gnueabihf //前缀
   -> External toolchain gcc version = 6.x
   -> External toolchain kernel headers series = 5.10.x
   -> External toolchain C library = glibc/eglibc
   -> [*] Toolchain has SSP support? (NEW) //选中
   -> [*] Toolchain has RPC support? (NEW) //选中
   -> [*] Toolchain has C++ support? //选中
   -> [*] Enable MMU support (NEW) //选中

配置System configuration

System configuration
   -> System hostname = Embedfire_imx6ull //平台名字
   -> System banner = Welcome to embedfire i.mx6ull //欢迎语
   -> Init system = BusyBox //使用 busybox
   -> /dev management = Dynamic using devtmpfs + mdev //使用 mdev
   -> [*] Enable root login with password (NEW) //使能登录密码
   -> Root password = root //登录密码为 root

配置Filesystem images

-> Filesystem images
   -> [*] ext2/3/4 root filesystem //如果是 EMMC 或 SD 卡的话就用 ext3/ext4
      -> ext2/3/4 variant = ext4 //选择 ext4 格式
-> [*] ubi image containing an ubifs root filesystem //如果使用 NAND 的话就用 ubifs
- > [*] squashfs root filesystem
	-> block size (128k)
	-> compression algorithm (zstd)

nand配置

  • nand信息
rk-github编译流程-rootfs构建6
  • 配置

    [*] ubi image containing an ubifs root filesystem
        physical eraseblock size           = 0x20000
        sub-page size                      = 0
        
    [*] ubifs root filesystem
        logical eraseblock size            = 0x1f000
        minimum I/O unit size              = 0x800
        maximum logical eraseblock count   = 2000
        ubifs runtime compression          = lzo
        Compression method                 = no compression
    

    rk-github编译流程-rootfs构建7

关闭kernel和uboot的编译(默认关闭)

-> Kernel
-> [ ] Linux Kernel //取消 Linux Kernel 选项!

-> Bootloaders
-> [ ] U-Boot //取消 U-Boot 选项!

执行编译

make -j32

下载

https://buildroot.org/

不要使用root账户

#修改root密码
passwd root 
#创建一个新账户
useradd -m builder
#删除
userdel builder
#使用root账户给builder用户分配解压的buildroot文件夹权限
chown -R builder:builder /home/builder/buildroot
#后面再使用builder用户
su builder

配置Target options

make menuconfig
Target options
        -> Target Architecture = ARM (little endian)
        -> Target Binary Format = ELF
        -> Target Architecture Variant = cortex-A7
        -> Target ABI = EABIhf
        -> Floating point strategy = NEON/VFPv4
        -> ARM instruction set = ARM

配置Toolchain

Toolchain
   -> Toolchain type = External toolchain
   -> Toolchain = Custom toolchain //选择用户的交叉编译器
   -> Toolchain origin = Pre-installed toolchain
   -> Toolchain path =/root/gcc-linaro-6.5.0-2019.12-x86_64_arm-linux-gnueabihf //交叉编译器路径
   -> Toolchain prefix = arm-linux-gnueabihf //前缀
   -> External toolchain gcc version = 6.x
   -> External toolchain kernel headers series = 5.10.x
   -> External toolchain C library = glibc/eglibc
   -> [*] Toolchain has SSP support? (NEW) //选中
   -> [*] Toolchain has RPC support? (NEW) //选中
   -> [*] Toolchain has C++ support? //选中
   -> [*] Enable MMU support (NEW) //选中

配置System configuration

System configuration
   -> System hostname = Embedfire_imx6ull //平台名字
   -> System banner = Welcome to embedfire i.mx6ull //欢迎语
   -> Init system = BusyBox //使用 busybox
   -> /dev management = Dynamic using devtmpfs + mdev //使用 mdev
   -> [*] Enable root login with password (NEW) //使能登录密码
   -> Root password = root //登录密码为 root

配置Filesystem images

-> Filesystem images
   -> [*] ext2/3/4 root filesystem //如果是 EMMC 或 SD 卡的话就用 ext3/ext4
      -> ext2/3/4 variant = ext4 //选择 ext4 格式
-> [*] ubi image containing an ubifs root filesystem //如果使用 NAND 的话就用 ubifs

nand配置

  • nand信息
rk-github编译流程-rootfs构建6
  • 配置

    [*] ubi image containing an ubifs root filesystem
        physical eraseblock size           = 0x20000
        sub-page size                      = 0 
        
    [*] ubifs root filesystem
        logical eraseblock size            = 0x1f000
        minimum I/O unit size              = 0x800
        maximum logical eraseblock count   = 2000
        ubifs runtime compression          = lzo
        Compression method                 = no compression
    

    1. [*] ubi image containing an ubifs root filesystem

    • 作用:告诉 Buildroot 生成一个 UBI 镜像ubi.img),其中包含一个 UBIFS 格式的根文件系统。
    • 背景:UBI (Unsorted Block Images) 是 Linux 内核中为原始 NAND Flash 设计的磨损平衡、坏块管理层。UBI 镜像可以直接烧写到 NAND 分区上,内核通过 UBI 驱动挂载内部的 UBIFS。

    子参数:

    physical eraseblock size = 0x20000

    • 含义:NAND Flash 芯片的物理擦除块大小(Physical Erase Block Size),十六进制 0x20000 = 128 KiB。
    • 作用:告诉 UBI 镜像生成工具(ubinize)你的 Flash 芯片每个擦除块实际有多大。这个值必须与硬件严格匹配,否则镜像无法正确烧写或挂载。

    sub-page size = 0

    • 含义:NAND Flash 的子页大小(Sub-page Size)。值为 0 表示不使用子页功能。
    • 作用:某些 NAND 芯片支持比页更小的写入单元(子页),用于优化小数据写入。设为 0 表示禁用该特性,UBI 将按完整页操作。

    2. [*] ubifs root filesystem

    • 作用:启用 UBIFS 格式的根文件系统(而不是 ext4、squashfs 等)。UBIFS 是专为 UBI 层设计的日志型文件系统,适合 NAND Flash。

    子参数:

    logical eraseblock size = 0x1f000

    • 含义逻辑擦除块大小(Logical Erase Block Size),十六进制 0x1f000 = 126,976 字节(约 124 KiB)。
    • 计算:LEB 大小 = PEB 大小 - UBI 头开销。UBI 在每个物理擦除块头部占用少量字节(通常 2 个页或 64 字节)。 这里 PEB = 128 KiB,LEB ≈ 128 KiB - 开销 = 124 KiB,吻合。这个值由 ubinize 自动计算,一般无需手动改。

    minimum I/O unit size = 0x800

    • 含义最小 I/O 单元大小,即 NAND Flash 的页大小(Page Size)。 0x800 = 2048 字节(2 KiB)。
    • 作用:UBIFS 和 UBI 必须按页对齐写入。常见 NAND 页大小为 2KiB、4KiB 或 8KiB。此值必须匹配硬件。

    maximum logical eraseblock count = 2000

    • 含义:UBIFS 文件系统中允许的最大逻辑擦除块数量
    • 作用:限制 UBIFS 占用的最大体积。实际文件系统大小 = LEB 大小 × 最大 LEB 数量。 这里:124 KiB × 2000 ≈ 242 MiB。如果你的 UBI 分区大于此值,UBIFS 只会使用前 2000 个 LEB;如果分区更小,则自动缩小。 建议:将此值设置为分区 LEB 总数的上限,或留一些余量(过大会浪费内存用于元数据)。

    ubifs runtime compression = lzo

    • 含义:UBIFS 在运行时写入数据时使用的压缩算法,这里为 LZO。
    • 作用:当应用程序向文件系统写入文件时,UBIFS 会尝试用 LZO 压缩数据后再存入 Flash。可节约空间,但牺牲少量 CPU 性能。可选 nonelzozlib 等。

    Compression method = no compression

    • 含义:Buildroot 生成 UBIFS 镜像时(即制作根文件系统镜像阶段)采用的压缩方法。这里为 no compression(不压缩)。
    • 注意:这与“运行时压缩”是两个不同概念:
      • 镜像生成压缩:制作 ubifs.img 时是否压缩整个文件系统。如果选 lzo,生成的镜像体积小,但挂载时需要解压(只读,如 squashfs 方式)。UBIFS 通常不启用此选项,因为它本身支持运行时压缩。
      • 运行时压缩:文件系统运行时对写入文件实时压缩。
    • 为什么选 no compression:防止 Buildroot 在生成镜像时对内容做额外压缩,因为运行时压缩已经足够,且双重压缩无意义。

    rk-github编译流程-rootfs构建7

关闭kernel和uboot的编译(默认关闭)

-> Kernel
-> [ ] Linux Kernel //取消 Linux Kernel 选项!

-> Bootloaders
-> [ ] U-Boot //取消 U-Boot 选项!

执行编译

make -j32

中文支持

  • libbb/printable_string.c

    //注释 30行左右的
    
    //	if (c >= 0x7f)
    //		break;
    
    //修改40行左右的
    while (1) {
    	unsigned char c = *d;
    	if (c == '\0')
    		break;
    	// if (c < ' ' || c >= 0x7f) //这里
    	if (c < ' ')
    		*d = '?';
    	d++;
    }
    
  • libbb/unicode.c

    //修改1020行左右的
    while ((int)--width >= 0) {
    	unsigned char c = *src;
    	if (c == '\0') {
    		do
    			*d++ = ' ';
    		while ((int)--width >= 0);
    		break;
    	}
    	//*d++ = (c >= ' ' && c < 0x7f) ? c : '?'; //这里
    	*d++ = (c >= ' ') ? c : '?'; 
    	src++;
    }
    
    //修改1040行左右的
    while (*d) {
    	unsigned char c = *d;
    	//if (c < ' ' || c >= 0x7f) //这里
    	if (c < ' ')
    		*d = '?';
    	d++;
    }
    
  • 图形化配置界面

    Settings
    	-> [*] Support Unicode
    

编译失败修改

关闭yescrypt

Login/Password Management Utilities
	-> [] Enable yescrypt functions 

编译结果

bin/
sbin/
user/
linuxrc
mkdir dev etc lib mnt proc sys tmp var

文件移植

确保编译时没有静态编译

Settings
	-> [] Build static binary (no shared libs)

将交叉编译器的

  • etc

    • 创建init.d文件夹
    /etc/init.d
    
    • 创建init.d/rcS文件
    
    
  • lib

    cp -r /root/rk-tools/gcc-linaro-6.3.1-2017.05-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/* . 
    

安装mtd-utils

mkfs.ubifs 的作用是将普通的文件夹转换成一个 UBIFS 格式的镜像文件。

apt-get update
apt-get install mtd-utils

生成可烧写的oem.img

  1. 创建oem文件夹

  2. 执行命令生成ubifs文件

    mkfs.ubifs -r oem -m 2048 -e 126976 -c 247 -o oem_ubifs.img
    
    • r <目录>:指定要打包的根目录(如 oem),该目录下的所有文件和权限都会被保留并打包。
    • -m <大小>:最小 I/O 单元大小(Minimum I/O unit size),单位字节。Page Size(页大小)
    • -e <大小>:逻辑擦除块大小(Logical Erase Block, LEB),单位字节。
      • 计算关系-e 的值通常等于物理擦除块大小(PEB)减去 UBI 头部开销。例如,如果物理擦除块是 128KB (131072字节),UBI 开销通常占去 2个页大小,即 131072 - 2048*2 = 126976
    • -c <数量>:文件系统可使用的最大逻辑擦除块数量(Max LEB count)。按 30 MiB = 30 × 1024 × 1024 字节。
      • c=(30×1024×1024)/126976≈247.8,向下取整得到 247 个 LEB
    • -o <文件名>:指定输出的 UBIFS 镜像文件名。
  3. 封装 UBI 镜像(ubinize)

    ubinize 的作用是将上一步生成的 UBIFS 镜像,加上 UBI 层的卷(Volume)管理信息,封装成最终可以烧录到 NAND Flash 上的 .ubi 文件。

    • 创建配置文件 oem-ubinize.cfg

      [oem_volume]
      mode=ubi
      image=oem_ubifs.img
      vol_id=0
      vol_size=30MiB
      vol_type=dynamic
      vol_name=oem
      #vol_flags=autoresize
      
      • mode=ubi:指定工作模式为 UBI。

      • image=:指向第一步生成的 UBIFS 镜像文件。

      • vol_id=:卷的 ID 编号,必须唯一。例如 oem 设为 0,userdata 可以设为 1。

      • vol_size=:卷的大小。

        强烈建议直接写具体的数值(如 64MiB)。

        • 避坑指南:这个值不能大于你在 parameter.txt 中为该分区预留的物理空间大小,且建议预留 8~10MB 的余量给 UBI 自身的元数据开销,否则 ubinize 会报错提示空间不足。
      • vol_type=dynamic:卷类型。日常使用的读写分区(如 oem, userdata)都设为 dynamic;如果是只读分区可设为 static

      • vol_name=:卷的名称。这个名字非常重要,后续在 /etc/fstab 中挂载时,就是通过 ubi0:oem 这样的名字来识别的。

      • vol_flags=autoresize:自动调整大小标志。如果设置该标志,文件系统会在第一次挂载时自动扩展到该卷允许的最大尺寸,非常适合 userdata 这种需要利用剩余空间的分区。

    • 执行封装命令

      ubinize -o oem.ubi -m 2048 -p 128KiB -s 512 oem-ubinize.cfg
      
      • -o <文件名>:指定最终生成的 UBI 镜像文件名(如 oem.ubi)。
      • -m <大小>:最小 I/O 单元大小,必须与 mkfs.ubifs 中的 -m 参数保持一致(即 NAND 的页大小)。
      • -p <大小>:物理擦除块大小(Physical Erase Block, PEB),单位支持 KiB/MiB。这个值必须严格匹配你的 NAND Flash 硬件手册中的 Block Size(擦除块大小),常见值为 128KiB 或 256KiB。
      • -s <大小>:子页大小(Sub-page size)。如果不确定,通常设为 512 或与 -m 相同即可。
  4. 生成oem.img

    直接修改oem.ubi名称就好了

    cp oem.ubi oem.img
    

生成可烧写的userdata.img

  1. 创建userdata文件夹

  2. 执行命令生成ubifs文件

    mkfs.ubifs -r userdata -m 2048 -e 126976 -c 247 -o userdata.img
    
  3. 封装 UBI 镜像(ubinize)

    • 创建配置文件 userdata-ubinize.cfg

      [userdata_volume]
      mode=ubi
      image=userdata_ubifs.img
      vol_id=0
      vol_size=30MiB
      vol_type=dynamic
      vol_name=userdata
      vol_flags=autoresize
      
    • 执行封装命令

      ubinize -o userdata.ubi -m 2048 -p 128KiB -s 512 userdata-ubinize.cfg
      
    1. 生成oem.img

      cp userdata.ubi userdata.img
      

只需要将rootfs.ubi重命名就好

cp rootfs.ubi rootfs.img

使用 Rockchip 工具打包

  1. afptool: 将各个分区镜像打包成一个中间文件(通常叫 package-filefirmware.img)。
  2. rkImageMaker: 给中间文件加上 Rockchip 专用的头部信息,生成最终的 update.img

Rk sdk修改

交叉编译器:

  • 路径:tools/linux/toolchain/arm-rockchip830-linux-uclibcgnueabihf/

sdk根目录的config目录

软链接,方便修改和查看配置?不需要自己去翻那么深层的目录。

  • buildroot_defconfig ---> sysdrv/.../configs/luckfox_pico_defconfig
  • dts_config -> sysdrv/source/kernel/arch/arm/boot/dts/rv1106-luckfox-pico-pro-max-ipc.dtsi
  • kernel_defconfig -> sysdrv/source/kernel/arch/arm/configs/luckfox_rv1106_linux_defconfig

模块目录

所有模块都放在这个地方。编译完成之后的模块会放到oem分区。

  • sysdrv/drv_ko

uboot配置文件:

  • 路径:sysdrv/source/uboot/u-boot/configs/luckfox_pico_defconfig

uboot使用的设备树:

  • 路径:sysdrv/source/uboot/u-boot/arch/arm/dts/rv1106-luckfox.dts

kernel配置文件:

  • 路径:sysdrv/source/kernel/arch/arm/configs/luckfox_rv1106_linux_defconfig

设备树文件:

  • 路径:sysdrv/source/kernel/arch/arm/boot/dts/rv1106g-luckfox-pico-pro-max.dts

buildroot文件:

  • 路径:sysdrv/source/buildroot/buildroot-2023.02.6/configs/luckfox_pico_defconfig

buildroot-overlay:

一、拿到新板子后要改的核心文件

按重要性和改动频率从高到低排列:

1. Device Tree (DTS) — 最核心,几乎必改

位置: sysdrv/source/kernel/arch/arm/boot/dts/

这是硬件描述文件,PCB 上每个外设都要在这里声明。你需要:

需求做法
GPIO 复用(哪个引脚做什么)改 pinctrl 节点
新增/换用 SPI/I2C/UART/PWM在 dts 里使能对应控制器,配引脚
换摄像头 sensorrockchip-cif + sensor 节点,更新 RK_CAMERA_SENSOR_IQFILES
改网口 / WiFi 模块配 gmac / sdio 节点
换屏幕改 display / backlight / panel 节点
改 DDR 类型或频率见下文 DDR 部分

推荐做法: 拷贝一个最接近你硬件的 .dts(比如 rv1106g-luckfox-pico-pro-max.dts),改成你板子的名字,然后:

  • 删掉你板上没有的外设
  • 加上你板上有但 Luckfox 没有的外设
  • 检查所有 GPIO 引脚号是否匹配 PCB 原理图

然后在对应 BoardConfig .mk 里把 RK_KERNEL_DTS 指向你的新 .dts


2. BoardConfig.mk — 每块板子一个配置文件

位置: project/cfg/BoardConfig_IPC/

存放位置,把你的配置拷贝一份,改成你自己的名字:

cp BoardConfig-SPI_NAND-Buildroot-RV1106_Luckfox_Pico_Pro_Max-IPC.mk \
   BoardConfig-SPI_NAND-Buildroot-RV1106_My_Company_Board-IPC.mk

需要修改的关键项:

RK_KERNEL_DTS          → 指向你的新 dts
RK_PARTITION_CMD_IN_ENV → 按 flash 大小调整分区
RK_CAMERA_SENSOR_IQFILES → 换成你用的 sensor
LF_WIFI_SSID / LF_WIFI_PSK → 公司 WiFi
RK_POST_OVERLAY        → 你的自定义 overlay

最后 ln -sf.BoardConfig.mk


3. U-Boot Defconfig — 一般不动,除非换了启动介质

位置: sysdrv/source/uboot/u-boot/configs/luckfox_rv1106_uboot_defconfig

Luckfox 的 U-Boot 配置已经适配了 RV1106,大多数情况下直接用。需要改的情况:

  • 换了 SPI NAND / SPI NOR / eMMC / SD 卡启动 → 换 RK_UBOOT_DEFCONFIG_FRAGMENT
  • 换了 DDR 频率 → 改 RKBOOT/*.ini 里选的 DDR bin
  • 需要在 U-Boot 阶段加驱动(比如网卡)→ 改 defconfig

4. DDR 配置 — 换 RAM 芯片才需要动

位置: sysdrv/source/uboot/rkbin/

DDR 是 Rockchip 提供的闭源二进制 .bin。Luckfox 默认使用 rv1106_ddr_924MHz_v1.15.bin

如果你公司板子换了 DDR 颗粒(比如 DDR3→DDR4、或改了频率),需要用 tools/ddrbin_param.txt + tools/ddrbin_tool 生成新的 DDR bin,然后修改 RKBOOT/RV1106MINIALL.ini 指向新 bin。


5. Kernel Defconfig — 加驱动才需要动

位置: sysdrv/source/kernel/arch/arm/configs/luckfox_rv1106_linux_defconfig

默认已经启用了常用的驱动。你如果需要:

  • 新的 USB 设备驱动 → make menuconfig 或直接改 defconfig
  • 额外的网络协议 / 文件系统
  • 开启/关闭某些内核特性

二、新板 bring-up 的标准流程

1.  PCB 工程师给原理图
      ↓
2.  选一个最接近的 Luckfox dts 拷贝为 xxx-my-board.dts
      ↓
3.  根据原理图改 pinctrl(GPIO复用)、regulator(电源)、
     i2c/spi/uart/pwm 等外设节点
      ↓
4.  编译内核:./build.sh kernel  → 出 dtb
      ↓
5.  如果能进系统了,逐个验证外设:
    - 串口 →  dmesg | grep tty
    - 网络 → ifconfig, ping
    - 摄像头 → v4l2-ctl --list-devices
    - 屏幕 → 看背光、fb 设备
      ↓
6.  调 ISP(摄像头画质),需要 IQ 文件调优
      ↓
7.  固件打包:./build.sh firmware
      ↓
8.  烧录验证

三、总结对照表

你要做什么改哪个文件
改 GPIO / 外设.dts (kernel)
换 flash / 调分区BoardConfig.mk
换摄像头 sensor.dts + RK_CAMERA_SENSOR_IQFILES
换 WiFi 模块.dts + kernel defconfig
改主机名/密码overlay 或 defconfig
换 DDR 颗粒rkbin/ DDR bin + INI
加内核驱动kernel defconfig
换启动方式U-Boot defconfig

luckfox-sdk的RV系列SOC是幸狐官方针对rk官方sdk进行修改的。可直接作为rk官方sdk进行参考。

luckfox提供了比较好的sdk,相比于在rk的github手动下载uboot,kernel,自己去构建rootfs还要从root,yocto,ubuntu等地方去下载,难度比较大。

  • 可以快速处理从SPL到rootfs过程。
  • 还能直接将生成的一堆img文件打包成一个update.img文件,不需要手动填写地址进行烧录。

luckfox编译流程

一、选择开发板

进入根目录执行

./build.sh lunch

按提示步骤

root@6e056017e4b4:~/luckfox-dev/luckfox-pico# ./build.sh lunch   
You're building on Linux
  Lunch menu...pick the Luckfox Pico hardware version:
  选择 Luckfox Pico 硬件版本:
                [0] RV1103_Luckfox_Pico
                [1] RV1103_Luckfox_Pico_Mini
                [2] RV1103_Luckfox_Pico_Plus
                [3] RV1103_Luckfox_Pico_WebBee
                [4] RV1106_Luckfox_Pico_Pro_Max
                [5] RV1106_Luckfox_Pico_Ultra
                [6] RV1106_Luckfox_Pico_Pi
                [7] RV1106_Luckfox_Pico_86Panel
                [8] RV1106_Luckfox_Pico_Zero
                [9] custom
Which would you like? [0~9][default:0]: 4
  Lunch menu...pick the boot medium:
  选择启动媒介:
                [0] SD_CARD
                [1] SPI_NAND
Which would you like? [0~1][default:0]: 1
  Lunch menu...pick the system version:
  选择系统版本:
                [0] Buildroot 
Which would you like? [0][default:0]: 0 

二、编译

编译uboot,kernel,rootfs,recovery image。既全量编译

./build.sh all

选择编译

./build.sh uboot

方式一、移除 overlay

编辑 .BoardConfig.mk 第 124 行,把 overlay-luckfox-buildroot-shadowRK_POST_OVERLAY 中去掉:

overlay-luckfox-buildroot-shadow

export RK_POST_OVERLAY="overlay-luckfox-config overlay-luckfox-buildroot-init overlay-luckfox-wifibt-firmware

sysdrv/source/buildroot/buildroot-2023.02.6/configs/luckfox_pico_defconfig

修改

BR2_TARGET_GENERIC_HOSTNAME="wyl" #主机名称
BR2_TARGET_GENERIC_ISSUE="Welcome to wyl" #欢迎语
#BR2_TARGET_GENERIC_ROOT_PASSWD="luckfox" #原始密码
BR2_TARGET_GENERIC_ROOT_PASSWD="root" #密码

方式二、修改overlay中密码

project/cfg/BoardConfig_IPC/overlay/下的overlay-luckfox-buildroot-shadow/etc

shadow文件

root:$1$dXmV8ZLO$eNAQzSYOgRkYMJRdsHwLS1:19664::::::
daemon:*:::::::
bin:*:::::::
sys:*:::::::
sync:*:::::::
mail:*:::::::
www-data:*:::::::
operator:*:::::::
nobody:*:::::::
  • openssl passwd -1 # 然后输入你要的密码

  • 将生成的 hash 替换 overlay 中 root 行的 $1$dXmV8ZLO$eNAQzSYOgRkYMJRdsHwLS1 部分。

方法三、在 shadow overlay 中直接写明文让你当前的密码生效

在 overlay 中放一个空的 /etc/shadow,让 Buildroot 生成的 shadow 文件不被覆盖。或者把 overlay 中的 shadow 文件内容改成空的 root 行,系统会在首次启动时用 Buildroot 的密码。

内容

mtdparts=spi-nand0:256K(env),256K@256K(idblock),512K(uboot),4M(boot),30M(oem),10M(userdata),210M(rootfs)
sys_bootargs= ubi.mtd=6 root=ubi0:rootfs rootfstype=ubifs rk_dma_heap_cma=66M
sd_parts=mmcblk0:16K@512(env),512K@32K(idblock),4M(uboot)

解析

.BoardConfig.mk第 38 行 — RK_PARTITION_CMD_IN_ENV

export RK_PARTITION_CMD_IN_ENV="256K(env),256K@256K(idblock),512K(uboot),4M(boot),30M(oem),10M(userdata),210M(rootfs)"

脚本 project/build.sh:1696-1716 根据 RK_BOOT_MEDIUM=spi_nand 拼上 mtdparts=spi-nand0: 前缀,然后写入文件:

# 第 1713 行
RK_PARTITION_ARGS="mtdparts=spi-nand0:$RK_PARTITION_CMD_IN_ENV"
# 第 1729 行
echo "${RK_PARTITION_ARGS}" >$ENV_CFG_FILE

第 2 行:sys_bootargs= ubi.mtd=6 root=ubi0:rootfs rootfstype=ubifs rk_dma_heap_cma=66M

这行由 4 个来源拼接而成:

参数来源脚本位置
ubi.mtd=6遍历分区列表时,rootfs 是第 6 个分区(0开始计数)[build.sh:1787]
root=ubi0:rootfs rootfstype=ubifsRK_PARTITION_FS_TYPE_CFG 中 rootfs 为 ubifs 类型[build.sh:2120]
rk_dma_heap_cma=66MBoardConfig 的 RK_BOOTARGS_CMA_SIZE="66M"[build.sh:2161-2162]

第 3 行:sd_parts=mmcblk0:16K@512(env)...

硬编码build_env() 函数中:

# build.sh 第 767 行
echo "sd_parts=mmcblk0:16K@512(env),512K@32K(idblock),4M(uboot)" >>$ENV_CFG_FILE

rk提供的一些rkbin示例

  • rkbin的会前往flash的0x0008 0000去拿uboot。

  • 然后放到RAM的0x0002 0000去启动。

由于生成env.img时修改了uboot的存放位置。所以uboot编译时会将这个地址进行改动

uboot基本配置不会进行修改。

但是会根据env.img文件拿到信息启动内核,以及将参数传递给内核。

256K(env),256K@256K(idblock),512K(uboot),4M(boot),20M(oem),10M(userdata),92M(rootfs)

rootfs从210M减少到90M左右

情况1:

​ 如果nand的块大小,页大小都不变,不需要修改nand的参数。既不需要修改编译rootfs时的配置情况。

​ 只需要修改env文件就好,即修改[.BoardConfig.mk]第 38 行:

export RK_PARTITION_CMD_IN_ENV="256K(env),256K@256K(idblock),512K(uboot),4M(boot),20M(oem),10M(userdata),92M(rootfs)"

情况2:

​ nand的块大小,页大小发生了变化,需要修改rootfs编译时的配置信息。

. 软件包管理 — 增删功能

Buildroot 的 defconfig[luckfox_pico_defconfig]控制哪些软件包编译进 rootfs。

常用类别:

类别示例包配置项
脚本语言python3, lua, perlBR2_PACKAGE_PYTHON3=y
网络服务openssh, dhcpcd, nginx, vsftpdBR2_PACKAGE_OPENSSH=y
调试工具strace, gdb, valgrind, tcpdump需要自己加
系统工具htop, nano, iperf3, iptablesBR2_PACKAGE_HTOP=y
USB gadget串口/网卡/摄像头/U盘 模拟内核配置,rootfs 配启动脚本
数据库sqlite, influxdb需要自己加
媒体库ffmpeg, gstreamerRockchip 有自己的 media 库,也可加通用库

添加方法:编辑 defconfig,加上 BR2_PACKAGE_xxx=y,然后重新编译 rootfs。

# 也可用 menuconfig 交互式选择
./build.sh buildrootconfig

2. 文件系统类型 — 可换其他的

当前用的是 ubifs,可以改 RK_PARTITION_FS_TYPE_CFG 切换:

类型特点适用场景
ubifs可读写、压缩、坏块管理需要写 rootfs(当前配置)
squashfs只读、高压缩率、挂载快稳定版固件,不可变系统
erofs只读、比 squashfs 更高效只读 rootfs(改进版)
jffs2可读写SPI NOR Flash 专用

例如切换为 squashfs 只读(更省空间,更稳定):

RK_PARTITION_FS_TYPE_CFG=rootfs@IGNORE@squashfs,oem@/oem@ubifs,userdata@/userdata@ubifs

同时 sys_bootargs 会自动从 root=ubi0:rootfs 变为 root=/dev/ubiblock0_0 rootfstype=squashfs


3. 初始化和启动行为

现有的 init 脚本在 overlay 中,可以:

  • 添加新的开机服务:创建 S<数字><名称> 脚本
  • 修改开机启动顺序:调整数字大小
  • 自定义 /etc/inittab:修改串口登录行为、自动登录
  • 自定义 /etc/profile:改环境变量、PATH、别名
  • 自动登录 root:修改 /etc/inittab/etc/securetty

现有 init 脚本位置:overlay-luckfox-buildroot-init/etc/init.d/


4. 摄像头/ISP 配置

通过 BoardConfig 中的 RK_CAMERA_SENSOR_IQFILESRK_CAMERA_SENSOR_CAC_BIN

export RK_CAMERA_SENSOR_IQFILES="sc4336_OT01_40IRC_F16.json sc3336_CMK-OT2119-PC1_30IRC-F16.json mis5001_CMK-OT2115-PC1_30IRC-F16.json"

IQ 文件会被打包到 OEM 分区,系统启动时 ISP 加载。

  • 换摄像头传感器:改 IQ 文件列表
  • 加多个摄像头:增加列表中的文件
  • 调图像质量:修改 IQ JSON 文件中的参数(AE、AWB、AF 等)

5. USB 功能模拟 (USB Gadget)

RV1106 的 USB 可以模拟多种设备。参考 S99usb0config

  • USB 串口:模拟 USB 转串口,通过 USB 登录板子
  • USB 网卡(RNDIS/ECM/NCM):板子虚拟成 USB 网卡
  • USB 摄像头(UVC):板子采集摄像头通过 USB 输出视频流
  • USB 声卡(UAC):模拟 USB 音频设备
  • USB 大容量存储:模拟 U 盘

内核配置中这些功能已启用(CONFIG_USB_GADGET=y),rootfs 配启动脚本即可切换。


6. 网络功能增强

功能修改方式
WiFi 连接BoardConfig 中 LF_WIFI_SSID / LF_WIFI_PSK
4G/5G 模块内核有 rv1106-wwan-ndis-ppp.config 配置片段
有线网络设备树中启用 GMAC(当前 Pro Max 禁用)
蓝牙内核有 rv1106-bt.config 配置片段
热点模式用 hostapd + dhcpd
WireGuard VPN内核已支持(CONFIG_WIREGUARD=y
Docker 容器内核有 luckfox_rv1106-docker.config 片段

7. 显示/屏幕

Pro Max 板子带 RGB 显示屏(640x480),可以:

  • 换显示分辨率:改设备树的 display-timings
  • 换屏幕型号:改 SPI 初始化命令序列
  • LVDS/MIPI DSI:如果硬件支持
  • framebuffer 应用:直接写 /dev/fb0
  • Qt/LVGL 等 GUI 框架:需要 Buildroot 中添加

8. NPU 推理

RV1106 内置 NPU(0.5TOPS),支持:

  • RKNN 模型推理:在 rootfs 中放 RKNN 模型文件和推理程序
  • TensorFlow Lite / ONNX:转成 RKNN 格式后运行
  • NPU 驱动已经在内核中启用
  • runtime 库在 media 编译中生成

9. 文件系统扩容与分区自定义

除了改分区大小,还可以:

  • 增加新分区:比如加一个 config 分区单独存配置
  • 修改分区文件系统:比如 userdata 用 ext4 而非 ubifs
  • OEM 分区存 appRK_BUILD_APP_TO_OEM_PARTITION=y 把 app 和资源分开

10. 调试与开发

功能配置
串口调试默认 1500000 波特率
网络文件系统(NFS)内核支持,rootfs 挂载网络目录
交叉编译工具链SDK 自带
FIQ 调试器内核已启用,可通过串口中断

11. OTA 远程升级

Buildroot + rootfs 支持 OTA:

  • recovery 模式:独立分区做系统还原
  • 双备份启动(AB 系统):两个 rootfs 分区切换
  • update.img 可网络推送烧录

动手建议路线

简单 ──────────────────────────────────────────→ 复杂

改密码 → 加 init 脚本 → 加自己的 app
                         ↓
                 加软件包 (python/nano/htop)
                         ↓
                 换 squashfs 只读系统
                         ↓
                 摄像头调优 / 换传感器
                         ↓
                 USB Gadget 模式
                         ↓
                 加 NPU 推理 + OTA 升级

你想先做哪个方向?我可以给具体步骤和配置细节。

说到底,编译文件系统其实就是生成一堆文件和文件夹,同时引入一些依赖库。怎么打包成可以烧录的rootfs.img,再结合rk提供的构建工具将uboot,kernel,rootfs等打包成一个update.img整体

源代码/Buildroot包
       ↓
  目录树(bin, lib, etc, usr, ...)
       │
       │ mkfs.ubifs -d 目录 ...
       ↓
 rootfs.img (UBI 格式,含卷头)
       │
       │  + uboot.img + boot.img + oem.img + ...
       │  afptool -pack 目录 update_tmp.img
       │  rkImageMaker -RK1106 download.bin update_tmp.img update.img
       ↓
  update.img  ← 烧录工具最终认的文件
  1. buildroot从网上下载需要的包。

    这个步骤Buildroot 从 defconfig 读取包列表,下载源码

  2. 交叉编译以适配目标开发板。

    配置了交叉编译器、编译生成需要的文件、安装到 staging 目录

  3. 生成rootfs.img

    两个步骤:

    ① 目录 → UBIFS 镜像sysdrv/tools/pc/mtd-utils/mkfs_ubi.sh

    # 第 190 行:目录 → ubifs 裸镜像
    mkfs.ubifs -x lzo -e 0x1FC00 -m 2048 -c 468 -d rootfs_uclibc_rv1106/ -o rootfs.ubifs
      │          │       │        │       │         │
      │          │       │        │       │         └── 源目录
      │          │       │        │       └── 最大逻辑擦除块数 (分区大小/LEB)
      │          │       │        └── 最小 IO 大小 (NAND 页大小)
      │          │       └── LEB 大小 (块大小 - 2×页大小)
      │          └── lzo 压缩
      └── mkfs.ubifs 工具
    
    # 第 202 行:ubifs → UBI 镜像 (包含 UBI 头部)
    ubinize -o rootfs.img -m 2048 -p 0x20000 -v ubinize.cfg
    

    ubinize.cfg 描述了卷信息:

    [ubifs]
    mode=ubi
    vol_id=0
    vol_name=rootfs
    vol_type=dynamic
    vol_flags=autoresize
    image=rootfs.ubifs
    

    最终产出 rootfs.img,可以直接烧录到 NAND 的 rootfs 分区。

    ② 编译时用的是 build_mkimg()project/build.sh:2332-2333):

    # 对于 ubifs 类型,直接调 mkfs_ubi.sh
    $RK_PROJECT_TOOLS_MKFS_UBIFS $src $dst $part_size $part_name $fs_type $comp
    

    其他文件系统也一样简单:

    文件系统命令
    ext4mkfs.ext4 -d 目录 镜像 大小
    squashfsmksquashfs 目录 镜像 -comp xz
    jffs2mkfs.jffs2 -d 目录 -o 镜像
    erofsmkfs.erofs 镜像 目录

    本质上都是把目录 + 元数据 + 压缩选项丢给一个 mkfs 工具。

  4. 使用afptool打包出update.img

    核心是 2 个工具tools/linux/Linux_Pack_Firmware/

    afptool — 把多个子镜像打包成一个

    mk-update_pack.sh:223

    afptool -pack 镜像目录 update_tmp.img
    

    afptool 读取 package-file 清单文件,它列出了每个分区的文件名:

    # package-file(自动生成)
    bootloader	download.bin
    env	        env.img
    uboot	        uboot.img
    boot	        boot.img
    oem	        oem.img
    rootfs	        rootfs.img
    userdata	userdata.img
    package-file	package-file
    

    afptool 把所有子镜像按分区顺序拼接成一个 update_tmp.img,并在头部写入每个分区的偏移和大小信息。

    rkImageMaker — 加上 Rockchip 头部

    mk-update_pack.sh:225

    rkImageMaker -RK1106 download.bin update_tmp.img update.img -os_type:androidos
    

    rkImageMakerupdate_tmp.img 前面加上:

    • 芯片标识(-RK1106
    • download.bin(含 DDR 初始化 + SPL 的 loader)

    最终产出 update.img,这就是烧录工具(rkflash.sh / RKdevTool)识别的完整固件包。

一、开启某些功能支持

比如开启c++支持,adb支持。

  • 通过menuconfig进行界面配置
  • 通过直接修改配置文件开启某一项支持

二、集成自己的程序

有两种方法:

方法一:使用 overlay(推荐)

在 [project/cfg/BoardConfig_IPC/overlay/]下创建你自己的 overlay 目录,按根文件系统路径放置文件。

例如创建一个 overlay-myapp/ 目录结构:

project/cfg/BoardConfig_IPC/overlay/
└── overlay-myapp/
    ├── usr/
    │   └── bin/
    │       └── myapp              ← 你的应用程序
    └── etc/
        └── init.d/
            └── S99myapp           ← 开机启动脚本

然后在 .BoardConfig.mkRK_POST_OVERLAY 中添加:

export RK_POST_OVERLAY="overlay-luckfox-config overlay-luckfox-buildroot-init overlay-luckfox-wifibt-firmware overlay-myapp"

方法二:直接放在 custom_root/(SDK 根目录)

SDK 根目录有 custom_root/(如没有则创建),该目录内容也会被拷贝到 rootfs:

custom_root/
└── usr/
    └── bin/
        └── myapp

这种方式不需要改 BoardConfig。

三、配置开机自启,配置一些脚本

/root/luckfox-dev/luckfox-pico/project/cfg/BoardConfig_IPC/overlay/overlay-luckfox-config/etc/init.d

参考现有的 S99python 脚本风格,创建一个启动脚本。命名规则:S<数字><名称>,数字越小越早执行。

示例 S99myapp

#!/bin/sh

DAEMON=/usr/bin/myapp

start() {
    printf "Starting myapp: "
    if [ -f $DAEMON ]; then
        $DAEMON &
        echo "OK"
    else
        echo "FAIL: $DAEMON not found"
    fi
}

stop() {
    printf "Stopping myapp: "
    killall myapp
    echo "OK"
}

case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart|reload)
        stop
        start
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
esac

exit $?

注意事项

  • 脚本要加可执行权限chmod +x S99myapp
  • 如果 app 是后台守护进程,用 & 放到后台
  • 如果想开机启动后还能登录,不要用 exec,要用 &

验证

# 只重新打包固件(不需要重编整个 rootfs)
./build.sh firmware

Overlay 的文件会在 __PACKAGE_ROOTFS 阶段自动拷贝到 rootfs 中,然后打包成 rootfs.img。烧录后 app 就在板子上了,开机也会自动启动。

Rootfs

下载

https://buildroot.org/

不要使用root账户

#创建一个新账户
useradd -m builder
#使用root账户给builder用户分配解压的buildroot文件夹权限
chown -R builder:builder /home/builder/buildroot
#后面再使用builder用户
su builder

配置Target options

Target options
        -> Target Architecture = ARM (little endian)
        -> Target Binary Format = ELF
        -> Target Architecture Variant = cortex-A7
        -> Target ABI = EABIhf
        -> Floating point strategy = NEON/VFPv4
        -> ARM instruction set = ARM

配置Toolchain

Toolchain
   -> Toolchain type = External toolchain
   -> Toolchain = Custom toolchain //选择用户的交叉编译器
   -> Toolchain origin = Pre-installed toolchain
   -> Toolchain path =/root/gcc-linaro-6.5.0-2019.12-x86_64_arm-linux-gnueabihf //交叉编译器路径
   -> Toolchain prefix = arm-linux-gnueabihf //前缀
   -> External toolchain gcc version = 6.x
   -> External toolchain kernel headers series = 5.10.x
   -> External toolchain C library = glibc/eglibc
   -> [*] Toolchain has SSP support? (NEW) //选中
   -> [*] Toolchain has RPC support? (NEW) //选中
   -> [*] Toolchain has C++ support? //选中
   -> [*] Enable MMU support (NEW) //选中

配置System configuration

System configuration
   -> System hostname = Embedfire_imx6ull //平台名字
   -> System banner = Welcome to embedfire i.mx6ull //欢迎语
   -> Init system = BusyBox //使用 busybox
   -> /dev management = Dynamic using devtmpfs + mdev //使用 mdev
   -> [*] Enable root login with password (NEW) //使能登录密码
   -> Root password = root //登录密码为 root

配置Filesystem images

-> Filesystem images
   -> [*] ext2/3/4 root filesystem //如果是 EMMC 或 SD 卡的话就用 ext3/ext4
      -> ext2/3/4 variant = ext4 //选择 ext4 格式
-> [*] ubi image containing an ubifs root filesystem //如果使用 NAND 的话就用 ubifs

关闭kernel和uboot的编译(默认关闭)

-> Kernel
-> [ ] Linux Kernel //取消 Linux Kernel 选项!

-> Bootloaders
-> [ ] U-Boot //取消 U-Boot 选项!

执行编译

make

Rv1106笔记&流程

Jw board rv1106 v1 sdk(生产)

006. 脚本修改

UMS配置

需要内核开启UMS功能

CONFIG_USB_GADGET=y
CONFIG_USB_CONFIGFS=y
CONFIG_USB_CONFIGFS_MASS_STORAGE=y  #(UMS功能)

设置开机即为SD卡读取模式,即,电脑插入设备,设备会被当做读卡器,自动将sd里面映射为盘符。

/sysdrv/tools/board/android-tools/S50usbdevice
#也可以使用overlay进行覆盖

修改

UMS_EN=on
UMS_BLOCK=/dev/mmcblk1p1

开机自启脚本

start)
	ifconfig lo up
	if [ ! -e "/tmp/.usb_config" ]; then
		echo "$0: Cannot find .usb_config"
		# exit 0
		USB_CONFIG_FILE=/tmp/.usb_config
		echo "usb_adb_en" >> $USB_CONFIG_FILE   #这里的意思是配置为adb模式,我们改成usb_ums_en
	fi
echo "usb_adb_en" >> $USB_CONFIG_FILE
#这里的意思是配置为adb模式,我们改成
 echo "usb_ums_en" >> $USB_CONFIG_FILE

007. 项目开发

参考

注意:没有必要直接操作/dev/rtc0这个节点

:要用应用层的角度看待问题

rtc.h

/*
 * rtc.h — 实时时钟接口
 *
 * 读取 / 设置系统时间。底层走标准 C 库 time() / settimeofday(),
 * 设置时间后自动调用 hwclock -w 同步到硬件 RTC。
 * 不直接操作 /dev/rtc0 节点。
 */

#ifndef RTC_H
#define RTC_H

#include <time.h>

int  rtc_get_time(struct tm *t);
int  rtc_set_time(const struct tm *t);

#endif

rtc.c

#include "rtc.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int rtc_get_time(struct tm *t)
{
    time_t now = time(NULL);
    if (now == (time_t)-1) {
        perror("time");
        return -1;
    }
    if (localtime_r(&now, t) == NULL) {
        perror("localtime_r");
        return -1;
    }
    return 0;
}

int rtc_set_time(const struct tm *t)
{
    char cmd[128];
    snprintf(cmd, sizeof(cmd),
             "date -s '%04d-%02d-%02d %02d:%02d:%02d' 2>/dev/null",
             t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
             t->tm_hour, t->tm_min, t->tm_sec);

    if (system(cmd) != 0) {
        fprintf(stderr, "date -s failed\n");
        return -1;
    }

    if (system("hwclock -w 2>/dev/null") != 0) {
        /* hwclock 写入硬件 RTC 失败,不影响系统时间 */
    }
    return 0;
}

方式一、软件去做实现

方式二、通过S50脚本去切换ums和adb。(废弃)

01. Uboot修改

U-Boot Proper                                         

│    arch/arm/mach-rockchip/board.c                         │
│                                                          │
│    reset → _main → board_init_f() → relocate →           │
│    board_init_r()                                         │
│                                                          │
│    board_init() ← Rockchip 公共实现                        │
│      ├─ board_debug_uart_init()                           │
│      ├─ early_download() 检测下载按键                      │
│      ├─ clks_probe()     时钟驱动                         │
│      ├─ regulators_enable_boot_on() 电源                  │
│      ├─ io_domain_init()                                 │
│      └─ rk_board_init() ← evb_rv1106.c 没重写             │
│                                                          │
│    board_late_init() ← 环境变量、启动参数                   │
│      ├─ rockchip_set_ethaddr()   MAC 地址                 │
│      ├─ rockchip_set_serialno()  序列号                   │
│      ├─ setup_boot_mode()  正常/恢复/下载                  │
│      ├─ env_fixup()  环境变量调整                          │
│      └─ rk_board_late_init() ← evb_rv1106.c 没重写         │
│                                                          │
│    进入命令行/bootcmd:                                     │
│      boot_fit; boot_android ${devtype} ${devnum};          │
│    → 加载 kernel/DTB → bootm → 启动 Linux            

解析

uboot核心层

  1. _main 函数
  2. board_init_f_alloc_reserve,board_init_f_init_reserve (两步)
  3. board_init_f 比较重要
  4. 省略一堆
  5. relocate 比较重要
  6. board_init_r 比较重要

板级层

uboot配置中

CONFIG_ARCH_ROCKCHIP=y

arch/arm/Makefile:80:

machine-$(CONFIG_ARCH_ROCKCHIP) += rockchip
machdirs := $(patsubst %,arch/arm/mach-%/,$(machine-y))
# → machdirs = "arch/arm/mach-rockchip/"
  • RK芯片的默认入口就是这个文件
/uboot/arch/arm/mach-rockchip/board.c
  • 这个文件下有很多启动时会调用的函数,一般不会在这里进行修改

board.c到vb_rv1106.c使用__weak 弱函数实现分层架构,两个 .c 文件都生成 .o,链接器按"强符号覆盖弱符号"规则决定最终用哪个函数

  • 最后会走到某一个板子的目录下
/uboot/board/rockchip/evb_rv1106/evb_rv1106.c
  • 一般在这个目录下做板级修改。比如开机管理等。

uboot凭什么用我们自己的板子

以rv1106为例

/uboot/board/rockchip/evb_rv1106/Kconfig #Kconfig选项
/uboot/board/rockchip/evb_rv1106/Makefile #配置了编译对象 obj-y	+= evb_rv1106.o
/uboot/board/rockchip/evb_rv1106/evb_rv1106.c #源码

在Kconfig配置了

if TARGET_EVB_RV1106

config SYS_BOARD
	default "evb_rv1106"

config SYS_VENDOR
	default "rockchip"

config SYS_CONFIG_NAME
	default "evb_rv1106"

config BOARD_SPECIFIC_OPTIONS # dummy
	def_bool y

endif

又在uboot配置文件开启了

CONFIG_TARGET_EVB_RV1106=y

所以通过SYS_BOARD ,SYS_VENDOR,SYS_CONFIG_NAME拼接

添加自己的板子

  1. board/rockchip/ 下新建目录(如 board/rockchip/myboard/
  2. 新建 Kconfig,定义 TARGET_MYBOARD,设置 SYS_BOARD/SYS_VENDOR
  3. 新建 include/configs/myboard.h
  4. arch/arm/mach-rockchip/rv1106/Kconfig 中添加 source "board/rockchip/myboard/Kconfig"
  5. 新建 config-uboot/myboard_defconfig,设置 CONFIG_TARGET_MYBOARD=y

充电功能

流程

rk_board_late_init()
  ├── 有 USB → show_charge_logo()
  │     ├── 初始化 GPIO0_A2(输出低) / A3(输入)
  │     ├── 轮询 5 秒间隔:
  │     │   ├── USB 拔出 → 退出循环(硬件掉电)
  │     │── 按键按下 → 进入 2 秒等待
  │     │   ├── 2 秒内松开 → 取消,继续轮询
  │     │   ├── 2 秒内 USB 拔出 → 退出
  │     │   └── 按住满 2 秒 → 拉高 A2 → 启动内核
  │     │   └── 充电状态变化 → 打印
  │
  └── 无 USB → return 0(正常启动内核)

需要修改以下文件

/uboot/board/rockchip/evb_rv1106/evb_rv1106.c

添加测试

int rk_board_init(void) //一般不用这个,这个函数只做一些简单的功能
{
	printf("\n[test] =====================================\n");
	printf("[test] U-Boot 已修改!\n");
	printf("[test] =====================================\n\n");
	return 0;
}
int rk_board_late_init(void) //用这个
{
	printf("\n[test] =====================================\n");
	printf("[test] U-Boot 已修改!\n");
	printf("[test] =====================================\n\n");
	return 0;
}
/*
 * SPDX-License-Identifier:     GPL-2.0+
 *
 * (C) Copyright 2022 Rockchip Electronics Co., Ltd
 */

#include <common.h>
#include <asm/io.h>
#include <dwc3-uboot.h>
#include <video_rockchip.h>

DECLARE_GLOBAL_DATA_PTR;

/*
 * ==================== USB VBUS 检测 ====================
 * USB_VBUSDET (SoC引脚25) → USB 2.0 PHY → utmi_bvalid
 * 寄存器: PERI_GRF (0xff000000) + 0x60 bit 9
 * 来源: drivers/phy/phy-rockchip-inno-usb2.c
 *
 * ==================== 电源按键 ====================
 * GPIO0_A3 (SoC引脚65) = 电源按键, 按下为低电平
 * GPIO0_A2 (SoC引脚64) = 电源保持, 拉高后维持供电
 * 对应 DT uboot-charge 节点:
 *   power-key-gpios  = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>
 *   power-hold-gpios = <&gpio0 RK_PA2 GPIO_ACTIVE_HIGH>
 *
 * GPIO3_B7 (SoC引脚124) = 充电芯片 NSTDBY, 低电平=充满
 */
#define PERI_GRF_BASE		0xff000000
#define USB_UTMI_BVALID_OFF	0x0060
#define USB_UTMI_BVALID_BIT	BIT(9)

#define GPIO0_BASE		0xff380000
#define GPIO0_DR_L		0x0000
#define GPIO0_DDR_L		0x0008
#define GPIO0_EXT_PORT		0x0070
#define GPIO0_A2		2
#define GPIO0_A3		3
#define GPIO0_A2_MASK		(1 << GPIO0_A2)
#define GPIO0_A3_MASK		(1 << GPIO0_A3)

#define PMU_IOC_BASE		0xff388000
#define GPIO0A_IOMUX_SEL_L	0x00
#define GPIO0_A2_IOMUX_BIT	8	/* bits[11:8] */
#define GPIO0_A3_IOMUX_BIT	12	/* bits[15:12] */

#define GPIO3_BASE		0xff550000
#define GPIO3_B7_MASK		(1 << 15)

/* V2 GPIO 读写宏(与 drivers/gpio/rk_gpio.c 一致) */
#define gpio_v2_read(addr)	(readl(addr) & 0xFFFF)
#define gpio_v2_write(addr, val)	writel((val) | 0xFFFF0000, addr)

static int check_usb_powered(void)
{
	u32 val = readl(PERI_GRF_BASE + USB_UTMI_BVALID_OFF);
	return (val & USB_UTMI_BVALID_BIT) ? 1 : 0;
}

static int read_nstdby(void)
{
	u32 val = readl(GPIO3_BASE + GPIO0_EXT_PORT);
	return (val & GPIO3_B7_MASK) ? 1 : 0;
}

/* 配置电源按键和电源保持引脚 */
static void power_gpio_init(void)
{
	/* IOMUX: GPIO0_A2/A3 设为 GPIO 功能(function 0)*/
	writel((0xFF << (GPIO0_A2_IOMUX_BIT + 16)) |
	       (0 << GPIO0_A2_IOMUX_BIT) |
	       (0 << GPIO0_A3_IOMUX_BIT),
	       PMU_IOC_BASE + GPIO0A_IOMUX_SEL_L);

	/* DDR: A3=输入(按键), A2=输出(电源保持), 初始低 */
	u32 ddr = gpio_v2_read(GPIO0_BASE + GPIO0_DDR_L);
	ddr |= GPIO0_A2_MASK;		/* A2 = output */
	ddr &= ~GPIO0_A3_MASK;		/* A3 = input */
	gpio_v2_write(GPIO0_BASE + GPIO0_DDR_L, ddr);

	/* DR: A2 = low(不供电)*/
	u32 dr = gpio_v2_read(GPIO0_BASE + GPIO0_DR_L);
	dr &= ~GPIO0_A2_MASK;
	gpio_v2_write(GPIO0_BASE + GPIO0_DR_L, dr);
}

/* 拉高 A2 保持供电 */
static void power_hold_on(void)
{
	u32 dr = gpio_v2_read(GPIO0_BASE + GPIO0_DR_L);
	dr |= GPIO0_A2_MASK;
	gpio_v2_write(GPIO0_BASE + GPIO0_DR_L, dr);
	printf("[PWR] Power hold enabled\n");
}

/* 读按键状态: 按下=低电平, 返回 1=按下 */
static int power_key_pressed(void)
{
	u32 ext = readl(GPIO0_BASE + GPIO0_EXT_PORT);
	return (ext & GPIO0_A3_MASK) ? 0 : 1;
}

/*
 * 充电+电源按键逻辑:
 *   有 USB 供电时进入, 轮询按键和 USB 拔出
 *   按键按下 2 秒 → 拉高 A2 保持供电 → 退出启动内核
 *   USB 拔出 → 直接退出(若无 A2 保持则硬件掉电)
 */
static void show_charge_logo(void)
{
	int charging = read_nstdby();
	int last_charging = charging;
	int battery_level = 0;  /* 0, 1, 2, 3, 4 → 0%, 25%, 50%, 75%, 100% */
	static const char *bmp_names[] = {
		"battery_0.bmp", "battery_25.bmp", "battery_50.bmp",
		"battery_75.bmp", "battery_100.bmp"
	};

	printf("\n[%s] USB Powered\n",
	       charging ? "CHARGING" : "FULL");

	power_gpio_init();

	/*
	 * USB 检测只需要上电时判断这一次。
	 * 进入循环后不再反复读 USB 寄存器 —— USB 拔出后硬件无 power_hold 自然掉电,
	 * 无需软件检测。
	 */

	/* === 按键开机 === */
	if (power_key_pressed()) {
		/*
		 * 上电到进入此函数时按键仍按着——用户从上电一直按到现在,
		 * 已超过 2 秒,直接开机,无需再等。
		 */
		printf("[PWR] Key held since power-on, boot now\n");
		power_hold_on();
		goto exit;
	}

	printf("[PWR] No key press, charge mode\n");

	/* 显示当前电量图片(图片找不到也不影响后续流程)*/
	rockchip_show_bmp(bmp_names[battery_level]);

	while (1) {
		/* 充电状态变化时重置电量指示 */
		if (read_nstdby() != last_charging) {
			last_charging = !last_charging;
			battery_level = last_charging ? 4 : 0;
			printf("[%s] state changed\n",
			       last_charging ? "CHARGING" : "FULL");
			rockchip_show_bmp(bmp_names[battery_level]);
		}

		/* 每 3 秒递增电量指示(充满则停在 100%) */
		if (last_charging) {
			/* charging: 0% → 25% → 50% → 75% → repeat */
			battery_level = (battery_level + 1) % 4;
		} /* else full: 停在 100%,不循环 */

		rockchip_show_bmp(bmp_names[battery_level]);

		/* 等待 3 秒,期间检测按键长按 2s 开机 */
		{
			u32 start = get_timer(0);
			u32 key_start = 0;
			bool key_was_down = false;

			while (get_timer(start) < 3000) {
				mdelay(10);

				if (power_key_pressed()) {
					if (!key_was_down) {
						key_was_down = true;
						key_start = get_timer(0);
					} else if (get_timer(key_start) >= 2000) {
						printf("[PWR] Long press, booting...\n");
						power_hold_on();
						goto exit;
					}
				} else {
					key_was_down = false;
				}
			}
		}
	}

exit:
	printf("[CHARGE] Boot...\n");
}


int rk_board_late_init(void)
{
	if (check_usb_powered()) {
		printf("[CHARGE] USB Power detected!\n");
		show_charge_logo();
	}

	return 0;
}

# LH24030C50 (ST7789V) 显示屏的 U-Boot 配置片段
# 基础配置: rv1106_defconfig
# 用法: ./make.sh rv1106-lcd.config

CONFIG_BASE_DEFCONFIG="rv1106_defconfig"

# ========== 视频驱动核心 ==========
CONFIG_DM_VIDEO=y

# ========== Display Uclass ==========
# rockchip_rgb 需要 UCLASS_DISPLAY
CONFIG_DISPLAY=y

# ========== DRM 核心 + RGB 并口 ==========
CONFIG_DRM_ROCKCHIP=y          # 自动选中 PHY, VIDEO_BRIDGE
CONFIG_DRM_ROCKCHIP_RGB=y      # 自动选中 DRM_ROCKCHIP_PANEL 不需要手动打开CONFIG_DRM_ROCKCHIP_PANEL
CONFIG_DRM_PANEL_LH24030C50=y  # 我们的 LH24030C50 面板驱动
CONFIG_DRM_MEM_RESERVED_SIZE_MBYTES=32

# ========== 帧缓冲颜色深度 ==========
CONFIG_VIDEO_BPP32=y

# ========== GPIO 背光 ==========
CONFIG_BACKLIGHT_GPIO=y
#CONFIG_BACKLIGHT_PWM is not set

# ========== 电压 regulator(驱动链接需要) ==========
CONFIG_DM_REGULATOR=y

# ========== SPI Flash 启动 ==========
CONFIG_ROCKCHIP_SFC_IOMUX=y

这个是内核设备树,uboot拿的是内核设备树来使用

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/display/media-bus-format.h>
#include <dt-bindings/clock/rv1106-cru.h>

/ {
	backlight: backlight {
		status = "okay";
		compatible = "gpio-backlight";
		gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
		default-on;
		power-supply = <&vcc_3v3>;
	};

	panel: panel {
		compatible = "lh,lh24030c50", "simple-panel"; 
		reset-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>; //内核不用,uboot用到
		spi-scl-gpios = <&gpio4 RK_PA7 GPIO_ACTIVE_HIGH>;//内核不用,uboot用到
		spi-sdi-gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_HIGH>;//内核不用,uboot用到
		spi-cs-gpios = <&gpio4 RK_PA5 GPIO_ACTIVE_HIGH>;//内核不用,uboot用到
		bus-format = <MEDIA_BUS_FMT_RGB666_1X18>;
		bpc = <6>;
		backlight = <&backlight>;
		power-supply = <&vcc_3v3>;
		status = "okay";

		display-timings {
			native-mode = <&timing0>;

			timing0: panel-timing {
				clock-frequency = <7000000>;
				hactive = <240>;
				vactive = <320>;
				hfront-porch = <38>;
				hback-porch = <10>;
				hsync-len = <10>;
				vfront-porch = <8>;
				vback-porch = <4>;
				vsync-len = <4>;
				hsync-active = <0>;
				vsync-active = <0>;
				de-active = <1>;
				pixelclk-active = <0>;
			};
		};

		port {
			panel_in_rgb: endpoint {
				remote-endpoint = <&rgb_out_panel>;
			};
		};
	};

	reserved-memory {
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;

		drm_logo: drm-logo@00000000 {
			compatible = "rockchip,drm-logo";
			reg = <0x0 0x0>;
		};
	};
};

&display_subsystem {
	status = "okay";
	logo-memory-region = <&drm_logo>;

	/* U-Boot route node: 内核忽略此节点,仅 U-Boot 使用 */
	route {
		route_rgb: route-rgb {
			status = "okay";
			connect = <&vop_out_rgb>;
			logo,uboot = "logo.bmp";
			logo,kernel = "logo.bmp";
			logo,mode = "center";
			charge_logo,mode = "center";
		};
	};
};

&rgb {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&lcd_pins>;
	ports {
		rgb_out: port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			rgb_out_panel: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&panel_in_rgb>;
			};
		};
	};
};

&rgb_in_vop {
	status = "okay";
};

&route_rgb {
	status = "okay";
};
&vop {
	assigned-clocks = <&cru PLL_CPLL>;
	assigned-clock-rates = <216000000>;
	status = "okay";
};

路径:/u-boot/drivers/video/drm/panel-lh24030c50.c

// SPDX-License-Identifier: GPL-2.0+
/*
 * LH24030C50 RGB LCD panel driver based on the ST7789V controller.
 *
 * GPIO bitbang SPI for panel initialization, RGB interface for display data.
 */

#include <common.h>
#include <dm.h>
#include <errno.h>
#include <backlight.h>
#include <asm/gpio.h>
#include <power/regulator.h>
#include <linux/media-bus-format.h>
#include <drm_modes.h>

#include "rockchip_display.h"
#include "rockchip_panel.h"

struct lh24030c50_priv {
	struct gpio_desc reset;
	struct gpio_desc sclk;
	struct gpio_desc mosi;
	struct gpio_desc cs;
	struct udevice *power_supply;
	struct udevice *backlight;
	bool prepared;
	bool enabled;
};

/*
 * 9-bit SPI bitbang: DC bit + 8 data bits, MSB first.
 * CS active low, SCLK idles low, data clocked on rising edge.
 */
static void lh24030c50_spi_xfer(struct lh24030c50_priv *priv, int dc, u8 data)
{
	int i;

	dm_gpio_set_value(&priv->cs, 0);
	udelay(2);

	/* DC bit */
	dm_gpio_set_value(&priv->sclk, 0);
	dm_gpio_set_value(&priv->mosi, dc ? 1 : 0);
	udelay(2);
	dm_gpio_set_value(&priv->sclk, 1);
	udelay(2);

	/* 8 data bits, MSB first */
	for (i = 0; i < 8; i++) {
		dm_gpio_set_value(&priv->sclk, 0);
		dm_gpio_set_value(&priv->mosi, (data >> (7 - i)) & 1);
		udelay(2);
		dm_gpio_set_value(&priv->sclk, 1);
		udelay(2);
	}

	dm_gpio_set_value(&priv->sclk, 0);
	udelay(1);
	dm_gpio_set_value(&priv->cs, 1);
	udelay(1);
}

static void lh24030c50_write_cmd(struct lh24030c50_priv *priv, u8 cmd)
{
	lh24030c50_spi_xfer(priv, 0, cmd);
}

static void lh24030c50_write_data(struct lh24030c50_priv *priv, u8 data)
{
	lh24030c50_spi_xfer(priv, 1, data);
}

static void lh24030c50_spi_idle(struct lh24030c50_priv *priv)
{
	dm_gpio_set_value(&priv->cs, 1);
	dm_gpio_set_value(&priv->sclk, 0);
	dm_gpio_set_value(&priv->mosi, 0);
}

/*
 * ST7789V LCD initialization sequence.
 * This matches the kernel panel-lh24030c50.c LCD_Init().
 */
static void lh24030c50_lcd_init(struct lh24030c50_priv *priv)
{
	lh24030c50_spi_idle(priv);

	/* Software Reset */
	lh24030c50_write_cmd(priv, 0x01);
	mdelay(150);

	/* Sleep Out */
	lh24030c50_write_cmd(priv, 0x11);
	mdelay(120);

	/* MADCTL: RGB mode */
	lh24030c50_write_cmd(priv, 0x36); //控制 RGB/BGR、屏幕镜像、扫描方向
	lh24030c50_write_data(priv, 0x00);

	/* COLMOD: 18-bit/pixel (RGB666) */
	lh24030c50_write_cmd(priv, 0x3a);
	lh24030c50_write_data(priv, 0x06);

	/* RAMCTRL */
	lh24030c50_write_cmd(priv, 0xB0);
	lh24030c50_write_data(priv, 0x12); /* RM=1, DM=01(RGB) */
	lh24030c50_write_data(priv, 0xC0); /* EPF1 EPF0=11 */

	/* Porch setting */
	lh24030c50_write_cmd(priv, 0xB1);
	lh24030c50_write_data(priv, 0x40);
	lh24030c50_write_data(priv, 0x04);
	lh24030c50_write_data(priv, 0x0a);

	/* Frame Rate Control */
	lh24030c50_write_cmd(priv, 0xB2);
	lh24030c50_write_data(priv, 0x0C);
	lh24030c50_write_data(priv, 0x0C);
	lh24030c50_write_data(priv, 0x00);
	lh24030c50_write_data(priv, 0x33);
	lh24030c50_write_data(priv, 0x33);

	/* Gate Control */
	lh24030c50_write_cmd(priv, 0xB7);
	lh24030c50_write_data(priv, 0x35);

	/* VCOM Setting */
	lh24030c50_write_cmd(priv, 0xBB);
	lh24030c50_write_data(priv, 0x2B);

	/* Power Control 1 */
	lh24030c50_write_cmd(priv, 0xC0);
	lh24030c50_write_data(priv, 0x2C);

	/* Power Control 2 */
	lh24030c50_write_cmd(priv, 0xC2);
	lh24030c50_write_data(priv, 0x01);

	/* Power Control 3 */
	lh24030c50_write_cmd(priv, 0xC3);
	lh24030c50_write_data(priv, 0x11);

	/* Power Control 4 */
	lh24030c50_write_cmd(priv, 0xC4);
	lh24030c50_write_data(priv, 0x20);

	/* VCOM Control 1 */
	lh24030c50_write_cmd(priv, 0xC6);
	lh24030c50_write_data(priv, 0x0F);

	/* Power Control A */
	lh24030c50_write_cmd(priv, 0xD0);
	lh24030c50_write_data(priv, 0xA4);
	lh24030c50_write_data(priv, 0xA1);

	/* Positive Gamma Correction */
	lh24030c50_write_cmd(priv, 0xE0);
	lh24030c50_write_data(priv, 0xD0);
	lh24030c50_write_data(priv, 0x00);
	lh24030c50_write_data(priv, 0x06);
	lh24030c50_write_data(priv, 0x09);
	lh24030c50_write_data(priv, 0x0B);
	lh24030c50_write_data(priv, 0x2A);
	lh24030c50_write_data(priv, 0x3C);
	lh24030c50_write_data(priv, 0x55);
	lh24030c50_write_data(priv, 0x4B);
	lh24030c50_write_data(priv, 0x08);
	lh24030c50_write_data(priv, 0x16);
	lh24030c50_write_data(priv, 0x14);
	lh24030c50_write_data(priv, 0x19);
	lh24030c50_write_data(priv, 0x20);

	/* Negative Gamma Correction */
	lh24030c50_write_cmd(priv, 0xE1);
	lh24030c50_write_data(priv, 0xD0);
	lh24030c50_write_data(priv, 0x00);
	lh24030c50_write_data(priv, 0x06);
	lh24030c50_write_data(priv, 0x09);
	lh24030c50_write_data(priv, 0x0B);
	lh24030c50_write_data(priv, 0x29);
	lh24030c50_write_data(priv, 0x36);
	lh24030c50_write_data(priv, 0x54);
	lh24030c50_write_data(priv, 0x4B);
	lh24030c50_write_data(priv, 0x0D);
	lh24030c50_write_data(priv, 0x16);
	lh24030c50_write_data(priv, 0x14);
	lh24030c50_write_data(priv, 0x21);
	lh24030c50_write_data(priv, 0x20);

	lh24030c50_write_cmd(priv, 0x21); // 开启像素反转,解决黑白颠倒
}

static void lh24030c50_prepare(struct rockchip_panel *panel)
{
	struct lh24030c50_priv *priv = dev_get_priv(panel->dev);

	if (priv->prepared)
		return;

	/* Enable power supply */
	if (priv->power_supply)
		regulator_set_enable(priv->power_supply, true);

	/* Reset sequence: assert for 50ms, then deassert */
	dm_gpio_set_value(&priv->reset, 1);
	mdelay(50);
	dm_gpio_set_value(&priv->reset, 0);
	mdelay(50);

	/* SPI idle then run LCD init */
	lh24030c50_spi_idle(priv);
	mdelay(150);

	lh24030c50_lcd_init(priv);

	priv->prepared = true;
}

static void lh24030c50_unprepare(struct rockchip_panel *panel)
{
	struct lh24030c50_priv *priv = dev_get_priv(panel->dev);

	if (!priv->prepared)
		return;

	/* Sleep In */
	lh24030c50_write_cmd(priv, 0x10);
	mdelay(120);

	dm_gpio_set_value(&priv->reset, 1);
	lh24030c50_spi_idle(priv);

	if (priv->power_supply)
		regulator_set_enable(priv->power_supply, false);

	priv->prepared = false;
}

static void lh24030c50_enable(struct rockchip_panel *panel)
{
	struct lh24030c50_priv *priv = dev_get_priv(panel->dev);

	if (priv->enabled)
		return;

	/* Wait for first frame to settle, then turn display on */
	mdelay(20);
	lh24030c50_write_cmd(priv, 0x29);
	mdelay(50);

	/* Enable backlight */
	if (priv->backlight)
		backlight_enable(priv->backlight);

	priv->enabled = true;
}

static void lh24030c50_disable(struct rockchip_panel *panel)
{
	struct lh24030c50_priv *priv = dev_get_priv(panel->dev);

	if (!priv->enabled)
		return;

	/* Disable backlight */
	if (priv->backlight)
		backlight_disable(priv->backlight);

	/* Display Off */
	lh24030c50_write_cmd(priv, 0x28);

	priv->enabled = false;
}

/* Panel display timing: 240x320 @ 7MHz RGB */
static int lh24030c50_get_mode(struct rockchip_panel *panel,
				struct drm_display_mode *mode)
{
	printf("lh24030c50: get_mode called\n");

	mode->clock = 7000;
	mode->hdisplay = 240;
	mode->hsync_start = 240 + 38;
	mode->hsync_end = 240 + 38 + 10;
	mode->htotal = 240 + 38 + 10 + 10;
	mode->vdisplay = 320;
	mode->vsync_start = 320 + 8;
	mode->vsync_end = 320 + 8 + 4;
	mode->vtotal = 320 + 8 + 4 + 4;
	mode->flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC;
	mode->vrefresh = 70;
	mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;

	return 0;
}

static const struct rockchip_panel_funcs lh24030c50_funcs = {
	.prepare   = lh24030c50_prepare,
	.unprepare = lh24030c50_unprepare,
	.enable    = lh24030c50_enable,
	.disable   = lh24030c50_disable,
	.get_mode  = lh24030c50_get_mode,
};

static int lh24030c50_probe(struct udevice *dev)
{
	struct lh24030c50_priv *priv = dev_get_priv(dev);
	struct rockchip_panel *panel;
	int ret;

	/* Reset GPIO (active low) */
	ret = gpio_request_by_name(dev, "reset-gpios", 0, &priv->reset,
				   GPIOD_IS_OUT);
	if (ret) {
		printf("%s: Cannot get reset GPIO: %d\n", __func__, ret);
		return ret;
	}

	/* SPI bitbang GPIOs */
	ret = gpio_request_by_name(dev, "spi-scl-gpios", 0, &priv->sclk,
				   GPIOD_IS_OUT);
	if (ret) {
		printf("%s: Cannot get SPI SCLK GPIO: %d\n", __func__, ret);
		return ret;
	}

	ret = gpio_request_by_name(dev, "spi-sdi-gpios", 0, &priv->mosi,
				   GPIOD_IS_OUT);
	if (ret) {
		printf("%s: Cannot get SPI SDI GPIO: %d\n", __func__, ret);
		return ret;
	}

	ret = gpio_request_by_name(dev, "spi-cs-gpios", 0, &priv->cs,
				   GPIOD_IS_OUT);
	if (ret) {
		printf("%s: Cannot get SPI CS GPIO: %d\n", __func__, ret);
		return ret;
	}

	/* Power supply (optional) */
	ret = uclass_get_device_by_phandle(UCLASS_REGULATOR, dev,
					   "power-supply", &priv->power_supply);
	if (ret && ret != -ENOENT && ret != -ENODEV) {
		printf("%s: Cannot get power supply: %d\n", __func__, ret);
		return ret;
	}

	/* Backlight (optional) */
	ret = uclass_get_device_by_phandle(UCLASS_PANEL_BACKLIGHT, dev,
					   "backlight", &priv->backlight);
	if (ret && ret != -ENOENT && ret != -ENODEV) {
		printf("%s: Cannot get backlight: %d\n", __func__, ret);
		return ret;
	}

	/* Set initial idle state */
	dm_gpio_set_value(&priv->cs, 1);
	dm_gpio_set_value(&priv->sclk, 0);
	dm_gpio_set_value(&priv->mosi, 0);
	dm_gpio_set_value(&priv->reset, 0);

	/* Allocate and register rockchip_panel */
	panel = calloc(1, sizeof(*panel));
	if (!panel)
		return -ENOMEM;

	dev->driver_data = (ulong)panel;
	panel->dev = dev;
	panel->bpc = 6; /* RGB666: 6 bits per component */
	panel->bus_format = MEDIA_BUS_FMT_RGB666_1X18;
	panel->funcs = &lh24030c50_funcs;

	return 0;
}

static const struct udevice_id lh24030c50_ids[] = {
	{ .compatible = "lh,lh24030c50" },
	{ }
};

U_BOOT_DRIVER(lh24030c50) = {
	.name	  = "lh24030c50",
	.id	  = UCLASS_PANEL,
	.of_match = lh24030c50_ids,
	.probe	  = lh24030c50_probe,
	.priv_auto_alloc_size = sizeof(struct lh24030c50_priv),
};

修改这个图片

sysdrv/source/kernel/logo.bmp

build.sh构建内核就好

阶段问题描述根因分析解决方案当前状态
U-Boot 显示 logoU-Boot 阶段屏幕 logo 正常显示无异常-正常
内核启动 logo 消失切换内核瞬间开机 logo 黑屏消失U-Boot panel 驱动配置错误video,flags,参数配置为 CSYNC,正确应为 NHSYNC;导致内核rockchip_drm_logo.c屏幕模式匹配失败,CRTC 显示通道被关闭修改修复 U-Boot 驱动文件panel-lh24030c50.c内同步标志位配置已修复
内核持续显示 logo修复 U-Boot 驱动后,开机 logo 可从 U-Boot 阶段平滑过渡至内核阶段,无黑屏断层驱动同步参数修正后,内核rockchip_drm_show_logo()可正常无缝承接画面输出重新编译烧录 U-Boot 固件即可生效验证通过

文件路径:

rv1106/sysdrv/source/kernel/drivers/gpu/drm/rockchip/rockchip_drm_logo.c

匹配逻辑在 setup_initial_state() 函数,核心是 679-689 行:

679: list_for_each_entry(mode, &connector->modes, head) {
680:     if (mode->clock == set->clock &&                    // 7MHz
681:         mode->hdisplay == set->hdisplay &&              // 240
682:         mode->vdisplay == set->vdisplay &&              // 320
683:         mode->crtc_hsync_end == set->crtc_hsync_end &&  // 288
684:         mode->crtc_vsync_end == set->crtc_vsync_end &&  // 332
685:         drm_mode_vrefresh(mode) == set->vrefresh &&     // 70
689:         mode->flags == (set->flags & DRM_MODE_FLAG_ALL) // ← 这里
690:     ) {
691:         found = 1;
692:         match = 1;
693:         break;
694:     }
695: }
697: if (!found) {
698:     ret = -EINVAL;
700:     dev_err(..., "can't found any match mode\n");  // 不匹配 → CRTC 关闭
709:     goto error_conn;
710: }
  • mode = 内核面板模式(来自 DTS timer,flags=NHSYNC|NVSYNC=0x0A)
  • set = U-Boot 写入 route 节点的模式(flags=NVSYNC|CSYNC=0x48 → 不匹配)
  • 689 行 flag 严格相等比较失败 → found=0 → 697 行走 error 路径 → CRTC 被关闭、屏幕灭

需要修改

内核配置

CONFIG_FRAMEBUFFER_CONSOLE=y
CONFIG_VT=y

内核启动时会调用 rockchip_drm_show_logo() → 它从 U-Boot 继承了 456K 的启动 logo 数据,并通过 DRM atomic commit 把 CRTC 配置为扫描 logo 所在的 framebuffer(rockchip_drm_logo.c:897-1094)。 同时 rockchip_drm_fbdev_init() 创建了 /dev/fb0,分配了一个独立的 fbdev framebuffer(在 CMA 内存中)。 re005 打开 /dev/fb0 写入像素 → 写入的是 fbdev 的 framebuffer,但 CRTC 仍在扫描 logo 的 framebuffer,所以屏幕不更新。

uboot的设备驱动在probe之后才会执行rk_board_late_init,所以能显示充电图片。

uboot配置开启功能

详细跳转:config

SDK的.BoardConfig.mk文件开启fragment

# Uboot defconfig fragment
export RK_UBOOT_DEFCONFIG_FRAGMENT=rv1106-lcd.config

设备树

由于uboot配置文件配置了,使用内核的设备树。这是RK平台的特性,FIT启用后支持这么玩。

CONFIG_USING_KERNEL_DTB_V2=y

由于设备树中RGB节点,需要匹配内核以及uboot的SPI初始化功能。

compatible = "lh,lh24030c50", "simple-panel";
顺序尝试匹配结果
1stU-Boot 搜 lh24030c50✅ 命中我们的驱动
2ndsimple-panel不再尝试,已被第一个拿走
  • rockchip_panel 驱动(匹配 simple-panel)根本看不到这个节点。

  • Kernel 侧同理:lh,lh24030c50 是 SPI 驱动(不匹配根节点下的 panel),跳过→ 回退到 simple-panel,命中 panel-simple

驱动代码

  • 代码:跳转

  • Kconfig

    路径:/u-boot/drivers/video/drm/Kconfig

    config DRM_PANEL_LH24030C50
    	bool "LH24030C50 (ST7789V) 240x320 RGB panel"
    	depends on DRM_ROCKCHIP
    	help
    	  Say Y to enable support for the LH24030C50 / ST7789V-based
    	  240x320 RGB LCD panel. This driver uses GPIO bitbang SPI for
    	  panel initialization and parallel RGB for display data.
    
  • Makefile

    路径:/u-boot/drivers/video/drm/Makefile

    obj-$(CONFIG_DRM_PANEL_LH24030C50) += panel-lh24030c50.o
    

编译错误修改

1. rockchip_post_csc.c → 第 7 行加了一行 #include <common.h>

这文件原本缺这个头文件。编译时 rockchip_post_csc.h → edid.h → i2c.h 会用到 uchar 类型,而 uchar 在 <common.h> 里定义。不加就编译报错。

02. 内核修改

.BoardConfig.mk

  • 内核设备树
# Kernel dts
export RK_KERNEL_DTS=jw_borad_v1.dts
  • 关闭原有的摄像头
#export RK_CAMERA_SENSOR_IQFILES="sc4336_OT01_40IRC_F16.bin sc3336_CMK-OT2119-PC1_30IRC-F16.bin sc530ai_CMK-OT2115-PC1_30IRC-F16.bin"
#export RK_CAMERA_SENSOR_CAC_BIN="CAC_sc4336_OT01_40IRC_F16 CAC_sc530ai_CMK-OT2115-PC1_30IRC-F16"
  • 关闭原有的一些功能
export RK_APP_IPCWEB_BACKEND=n    #web 功能 nginx
export RK_ENABLE_ROCKCHIP_TEST=n  #测试,基准测试,内存测试
export RK_ENABLE_WIFI=n           #wifi功能 会编译/sysdrv/drv_ko/wifi 下的模块
  • 配置overlay
export RK_POST_OVERLAY=jw #对应/project/cfg/BoardConfig_IPC/overlay/jw 文件目录

这个文件主要是开启一些调试等功能。

路径:/sysdrv/cfg/package.mk

ADB功能开启

CONFIG_SYSDRV_ENABLE_ADBD=y
$(eval $(call MACRO_CHECK_ENABLE_PKG, RK_ENABLE_ADBD))

热插拔usb

CONFIG_SYSDRV_ENABLE_EUDEV=y
$(eval $(call MACRO_CHECK_ENABLE_PKG, RK_ENABLE_EUDEV))

这些配置项对应的源码目录

/sysdrv/tools/board

/media/cfg/cfg.mk文件主要是配置摄像头相关参数

比如配置libv4l库,用于v4l2-ctl 调试摄像头

# Enable libv4l
export CONFIG_LIBV4L=y

修改这个图片

sysdrv/source/kernel/logo.bmp

build.sh构建内核就好

阶段问题描述根因分析解决方案当前状态
U-Boot 显示 logoU-Boot 阶段屏幕 logo 正常显示无异常-正常
内核启动 logo 消失切换内核瞬间开机 logo 黑屏消失U-Boot panel 驱动配置错误video,flags,参数配置为 CSYNC,正确应为 NHSYNC;导致内核rockchip_drm_logo.c屏幕模式匹配失败,CRTC 显示通道被关闭修改修复 U-Boot 驱动文件panel-lh24030c50.c内同步标志位配置已修复
内核持续显示 logo修复 U-Boot 驱动后,开机 logo 可从 U-Boot 阶段平滑过渡至内核阶段,无黑屏断层驱动同步参数修正后,内核rockchip_drm_show_logo()可正常无缝承接画面输出重新编译烧录 U-Boot 固件即可生效验证通过

文件路径:

rv1106/sysdrv/source/kernel/drivers/gpu/drm/rockchip/rockchip_drm_logo.c

匹配逻辑在 setup_initial_state() 函数,核心是 679-689 行:

679: list_for_each_entry(mode, &connector->modes, head) {
680:     if (mode->clock == set->clock &&                    // 7MHz
681:         mode->hdisplay == set->hdisplay &&              // 240
682:         mode->vdisplay == set->vdisplay &&              // 320
683:         mode->crtc_hsync_end == set->crtc_hsync_end &&  // 288
684:         mode->crtc_vsync_end == set->crtc_vsync_end &&  // 332
685:         drm_mode_vrefresh(mode) == set->vrefresh &&     // 70
689:         mode->flags == (set->flags & DRM_MODE_FLAG_ALL) // ← 这里
690:     ) {
691:         found = 1;
692:         match = 1;
693:         break;
694:     }
695: }
697: if (!found) {
698:     ret = -EINVAL;
700:     dev_err(..., "can't found any match mode\n");  // 不匹配 → CRTC 关闭
709:     goto error_conn;
710: }
  • mode = 内核面板模式(来自 DTS timer,flags=NHSYNC|NVSYNC=0x0A)
  • set = U-Boot 写入 route 节点的模式(flags=NVSYNC|CSYNC=0x48 → 不匹配)
  • 689 行 flag 严格相等比较失败 → found=0 → 697 行走 error 路径 → CRTC 被关闭、屏幕灭

需要修改

内核配置

CONFIG_FRAMEBUFFER_CONSOLE=y
CONFIG_VT=y

内核启动时会调用 rockchip_drm_show_logo() → 它从 U-Boot 继承了 456K 的启动 logo 数据,并通过 DRM atomic commit 把 CRTC 配置为扫描 logo 所在的 framebuffer(rockchip_drm_logo.c:897-1094)。 同时 rockchip_drm_fbdev_init() 创建了 /dev/fb0,分配了一个独立的 fbdev framebuffer(在 CMA 内存中)。 re005 打开 /dev/fb0 写入像素 → 写入的是 fbdev 的 framebuffer,但 CRTC 仍在扫描 logo 的 framebuffer,所以屏幕不更新。

psplash

轻量级,直接操作帧缓冲(Framebuffer),资源占用极低(约2.8MB)。

资源受限的嵌入式设备(如RAM < 64MB的系统)

由于可以直接用simple_panel驱动,所以直接配置设备树就好了

摄像头模块

# CONFIG_VIDEO_GC2053=m
# CONFIG_VIDEO_IMX415=m
# CONFIG_VIDEO_OS04A10=m
# CONFIG_VIDEO_SC200AI=m
# CONFIG_VIDEO_SC3336=m
# CONFIG_VIDEO_SC401AI=m
# CONFIG_VIDEO_SC4336=m
# CONFIG_VIDEO_SC450AI=m
# CONFIG_VIDEO_SC530AI=m

wifi子系统

# CONFIG_WIRELESS=y
# CONFIG_WIRELESS_EXT=y
# CONFIG_WIRELESS_WDS is not set

# CONFIG_CFG80211_*所有=m
# CONFIG_MAC80211_*所有=m
# CONFIG_WLAN_*所有=y
# CONFIG_WL_ROCKCHIP=m

# CONFIG_WIRELESS_EXT=y
# CONFIG_WEXT_CORE=y
# CONFIG_WEXT_PRIV=y
# CONFIG_WEXT_PROC=y
# 所有 WLAN_VENDOR_* 不需要

文件系统

# CONFIG_NFS_FS=y
# CONFIG_NFS_V2=y
# CONFIG_NFS_V3=y
# CONFIG_NFS_V3_ACL=y
# CONFIG_NFS_V4=y

# CONFIG_SUNRPC=y
# CONFIG_LOCKD=y

ipv6

# CONFIG_IPV6=m

连带 fragment 里几十行 IPV6 子选项一起清掉。

usb声卡,键盘鼠标

# CONFIG_SND_USB=y
# CONFIG_USB_U_AUDIO=y
# CONFIG_USB_CONFIGFS_F_UAC1=y
# CONFIG_USB_CONFIGFS_F_UAC2=y
# CONFIG_USB_F_UAC1=y
# CONFIG_USB_F_UAC2=y

# CONFIG_USB_CONFIGFS_F_HID=y 
# CONFIG_USB_F_HID=y

Debug调试

# CONFIG_BLK_DEBUG_FS=y
# CONFIG_DEBUG_INFO=y

hdmi

# CONFIG_DRM_SII902X=y

npu和安卓 开关

# CONFIG_ANDROID=y
# CONFIG_ROCKCHIP_RKNPU=m
# CONFIG_ROCKCHIP_RKNPU_PROC_FS=y

网口

# CONFIG_STMMAC_ETH=y
# CONFIG_RK630_PHY=y

Audio CODEC 音频

#CONFIG_SOUND=y               → n  ← 关这个,下面全自动关
  CONFIG_SND=y
  CONFIG_SND_SOC=y
  CONFIG_SND_SOC_ROCKCHIP=y
  CONFIG_SND_SOC_RV1106=y
  CONFIG_SND_SIMPLE_CARD=y
  CONFIG_SND_JACK_INPUT_DEV=y

多余算法

用不到 IPsec/WiFi/NFS 等,很多算法链不会被触发。保守起见可以不动,但以下明显不用:

CONFIG_CRYPTO_DEFLATE=y   → n(不是用 SquashFS 的 LZ4?)
CONFIG_CRYPTO_LZO=y       → n
CONFIG_CRYPTO_ZSTD=y      → n
CONFIG_CRYPTO_RSA=y       → n

03. 设备树修改

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/display/media-bus-format.h>
#include <dt-bindings/clock/rv1106-cru.h>

/ {
	backlight: backlight {
		status = "okay";
		compatible = "gpio-backlight";
		gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
		default-on;
		power-supply = <&vcc_3v3>;
	};

	panel: panel {
		compatible = "lh,lh24030c50", "simple-panel"; 
		reset-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>; //内核不用,uboot用到
		spi-scl-gpios = <&gpio4 RK_PA7 GPIO_ACTIVE_HIGH>;//内核不用,uboot用到
		spi-sdi-gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_HIGH>;//内核不用,uboot用到
		spi-cs-gpios = <&gpio4 RK_PA5 GPIO_ACTIVE_HIGH>;//内核不用,uboot用到
		bus-format = <MEDIA_BUS_FMT_RGB666_1X18>;
		bpc = <6>;
		backlight = <&backlight>;
		power-supply = <&vcc_3v3>;
		status = "okay";

		display-timings {
			native-mode = <&timing0>;

			timing0: panel-timing {
				clock-frequency = <7000000>;
				hactive = <240>;
				vactive = <320>;
				hfront-porch = <38>;
				hback-porch = <10>;
				hsync-len = <10>;
				vfront-porch = <8>;
				vback-porch = <4>;
				vsync-len = <4>;
				hsync-active = <0>;
				vsync-active = <0>;
				de-active = <1>;
				pixelclk-active = <0>;
			};
		};

		port {
			panel_in_rgb: endpoint {
				remote-endpoint = <&rgb_out_panel>;
			};
		};
	};

	reserved-memory {
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;

		drm_logo: drm-logo@00000000 {
			compatible = "rockchip,drm-logo";
			reg = <0x0 0x0>;
		};
	};
};

&display_subsystem {
	status = "okay";
	logo-memory-region = <&drm_logo>;

	/* U-Boot route node: 内核忽略此节点,仅 U-Boot 使用 */
	route {
		route_rgb: route-rgb {
			status = "okay";
			connect = <&vop_out_rgb>;
			logo,uboot = "logo.bmp";
			logo,kernel = "logo.bmp";
			logo,mode = "center";
			charge_logo,mode = "center";
		};
	};
};

&rgb {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&lcd_pins>;
	ports {
		rgb_out: port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			rgb_out_panel: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&panel_in_rgb>;
			};
		};
	};
};

&rgb_in_vop {
	status = "okay";
};

&route_rgb {
	status = "okay";
};
&vop {
	assigned-clocks = <&cru PLL_CPLL>;
	assigned-clock-rates = <216000000>;
	status = "okay";
};

04. 模块修改

内核模块

  • modules.order文件

    内核编译完成之后会在内核根目录下生成modules.order文件,记录了各个ko文件的路径。

  • modules.builtin文件

    modules.builtin文件记录了原来可以编译成模块的配置,现在编译进了内核。专门给外部用户态工具(如 modprobedepmod)看的一份“已内置模块花名册”。它保证了即使你把驱动编进内核,用户态的热插拔和加载机制依然能“假装”找到了这个模块,从而顺畅执行后续流程。

  • ko文件生成和复制流程

    • 内核配置文件将一个配置设置成模块

    • 在驱动文件对应的目录生成ko文件

      比如:

      /kernel/drivers/media/i2c/gc2145.c
      /kernel/drivers/media/i2c/gc2145.ko
      

      如果配置文件的配置是y,那么生成的就是.o文件用于链接

    • sysdrv/Makefile中ko文件复制

      /sysdrv/out
      	↓
      /output/out/sysdrv_out/kernel_drv_ko
      	↓
      /output/out/oem/usr/ko
      	↓
      /oem/usr/ko/     ← 最终部署位置
      
    • insmod_ko.sh脚本

      `drv_ko/insmod_ko.sh`
      	↓
      `drv_ko/out/`
      	↓
      `output/out/sysdrv_out/kernel_drv_ko/`
      	↓
      `oem/usr/ko/` 
      	↓
      板子 `/oem/usr/ko/insmod_ko.sh`
      

      执行时机:

      开机 → S20linkmount (挂载 oem 到 /oem)
           → S21appinit → RkLunch.sh
               → cd /oem/usr/ko && sh insmod_ko.sh
      

外部模块

  • 编译受到配置文件的影响
/sysdrv/cfg/package.mk

比如:CONFIG_SYSDRV_ENABLE_WIFI=n就不会编译wifi模块

  • 编译流程
/sysdrv/drv_ko/xxx模块/Makefile
	↓
/sysdrv/drv_ko/xxx模块/out/xxx.ko
	↓ 复制
/sysdrv/drv_ko/out
	↓ 复制
/output/out/sysdrv_out/kernel_drv_ko
	↓ 复制
/output/out/oem/usr/ko
	↓ 复制
/oem/usr/ko/     ← 最终部署位置
  • insmod_ko.sh脚本
`drv_ko/insmod_ko.sh`
	↓
`drv_ko/out/`
	↓
`output/out/sysdrv_out/kernel_drv_ko/`
	↓
`oem/usr/ko/` 
	↓
板子 `/oem/usr/ko/insmod_ko.sh`
  • 执行时机:
开机 → S20linkmount (挂载 oem 到 /oem)
     → S21appinit → RkLunch.sh
         → cd /oem/usr/ko && sh insmod_ko.sh
完成情况
ADC按钮1
开关机按钮1
usb摄像头还差摄像头控制器功能还没移植
RGB屏幕1,在内核
sd卡1
mipi-csi摄像头
充电管理,电量管理充电管理引脚一直不变
ADB功能切换1

Rv1106 ipc linux sdk(测试)

build_all() 完整编译流程

build_all()
  ├── [可选] build_recovery     ← 只有 RK_ENABLE_RECOVERY=y 才执行
  ├── build_sysdrv              ← uboot + 内核 + 根文件系统 + busybox + 驱动模块
  ├── build_media               ← Rockchip 媒体库(rockit、mpi、isp、rga 等)
  ├── build_app                 ← 用户态应用程序(rkipc 等)
  ├── build_firmware            ← 打包所有镜像
  └── finish_build

第一步:build_sysdrv — 系统驱动层

# build.sh 第 541 行
function build_sysdrv(){
    make -C ${SDK_SYSDRV_DIR}    # → sysdrv/Makefile 的 all 目标
}

sysdrv/ 的 Makefile all 目标:

all: uboot kernel rootfs env

它依次调用 4 个子步骤:

1.1 make uboot — 编译 uboot

# sysdrv/Makefile 第 82-92 行
uboot: prepare
    make -C u-boot rv1106_defconfig          # 配置 uboot
    ./u-boot/make.sh --spl-new               # 编译 uboot + SPL
    cp uboot.img  output/image/
    cp idblock.img output/image/             # 一级引导
    cp download.bin output/image/            # 下载模式固件

产出文件:

output/out/sysdrv_out/ → 最终复制到 output/image/
  ├── uboot.img          ← uboot 主镜像
  ├── idblock.img        ← 一级引导(SPL + ddr init + 安全启动)
  └── download.bin       ← 烧录模式固件(loader)

1.2 make kernel — 编译内核

# sysdrv/Makefile 第 100-140 行
kernel: prepare
    make -C kernel ARCH=arm rv1106_defconfig     # 配置内核(含 fragment 合并)
    make -C kernel ARCH=arm zImage dtbs -jN      # 编译内核本身
    make -C kernel ARCH=arm boot.img             # 打包 boot.img(含内核 + dtb + resource)
    cp boot.img output/image/
    cp vmlinux   output/bin/                     # 带调试信息的完整内核
    cp *.dtb     output/bin/

关键点: 这里用的就是 make 命令,但通过 .config + rv1106-evb.config(fragment)合并生成最终配置。它不只是简单地执行 make,还做了:

  • fragment 合并:rv1106_defconfig + rv1106-evb.config → 最终 .config
  • 编译驱动模块(make modules
  • 调用 update_dtb_bootargs.sh 修改设备树中的启动参数(root=/dev/xxx)
  • 打包 FIT 格式的 boot.img

产出文件:

output/image/
  ├── boot.img          ← FIT 格式,内含 zImage + dtb + resource
output/out/sysdrv_out/
  ├── vmlinux           ← 完整内核(带调试符号)
  ├── rv1106g-evb1-v11.dtb
  └── kernel_drv_ko/    ← 内核驱动模块 .ko 文件

1.3 make rootfs — 构建根文件系统

# 这步最复杂,依赖链:rootfs_prepare → pctools → busybox → boardtools → drv → strip
rootfs: rootfs_prepare pctools busybox boardtools drv

子步骤详情:

子步骤做的事产出
rootfs_prepare解压 rootfs 脚本模板 + 拷贝工具链运行时库(libc, libm, libpthread 等) 到 rootfs_*/基础根文件系统骨架
pctools编译 PC 端工具(mkenvimage、mk-fitimage.sh、mkfs.ubifs 等)output/out/sysdrv_out/pc/
busybox编译 busybox(busybox-1.27.2),生成 _install/bin/busybox 及所有 symlink提供 sh, ls, cp, mv 等基础命令
boardtools编译板端工具(来自 sysdrv/tools/board/一些板级小工具
drvmake modules_install → 提取所有 .ko 到 kernel_drv_ko/内核模块拷贝
strip如果是 RELEASE 模式,strip 掉调试符号缩小根文件系统体积

最后根据存储介质打包根文件系统镜像:

  • spi_nand → rootfs_ubimkfs_ubi.shrootfs_base.img(UBIFS 格式)
  • emmc → rootfs_ext4mkfs_ext4.shrootfs_base.img(ext4 格式)
  • spi_nor → rootfs_jffs2mkfs_jffs2.shrootfs_base.img(JFFS2 格式)

产出文件:

output/out/sysdrv_out/
  ├── rootfs_glibc_rv1106/     ← 根文件系统目录(未打包)
  ├── rootfs_glibc_rv1106.tar  ← 根文件系统 tar 包
  ├── pc/                      ← PC 端工具
  ├── bin/                     ← 板端工具 + 调试文件
  └── kernel_drv_ko/           ← 内核驱动模块

第二步:build_media — 媒体库

# build.sh 第 523 行
function build_media(){
    make -C ${SDK_MEDIA_DIR}    # → media/Makefile
}
# media/Makefile
all: media_libs
    make -C ./samples                    # 编译 media 示例程序
    cp -rfa ... 到 output/out/media_out/ # 拷贝全部产出

media_libs:
    # 遍历所有子目录编译:
    # rockit/     — Rockchip 多媒体处理框架
    # isp3.x/     — 图像信号处理库
    # rv1106/     — 芯片级库(mpp、rga 等)
    # 等等

产出文件(到 output/out/media_out/):

media_out/
  ├── lib/              ← 所有 .so(librockit.so, libmpi.so, libisp.so, librga.so, libmpp.so...)
  ├── include/          ← 头文件(rockit 等 API 头)
  ├── bin/              ← 示例程序
  ├── share/isp_iqfiles/ ← 各传感器 IQ 调优文件(*.bin)
  ├── usr/              ← 额外资源
  └── root/             ← 需要放到根文件系统 / 下的文件

这是给 app 层提供依赖build_app 会链接 media_out 下的 .so 和头文件。


第三步:build_app — 用户态应用程序

# build.sh 第 461 行
function build_app(){
    check_config RK_APP_TYPE || return 0         # RK_APP_TYPE 为空则跳过
    build_meta --export --media_dir ...           # 导出 meta 头文件
    make -C ${SDK_APP_DIR}                       # → project/app/Makefile
}

会被跳过的条件: RK_APP_TYPE 未设置或为空 → 不编译

# project/app/Makefile
all:
    # 遍历所有子目录(rkipc/、ipcweb/、uvc_app_tiny/ 等)
    # 每个子目录根据 RK_APP_TYPE 决定是否编译
    make -C component/rkadk/     # 条件编译
    make -C component/lvgl/      # 条件编译
    make -C rkipc/               # RK_APP_TYPE=RKIPC_RV1106 时编译
    make -C uvc_app_tiny/        # RK_APP_TYPE=UVC_TINY 时编译
    # ...
    # 最后把所有产出拷贝到 out/
    MAROC_COPY_PKG_TO_APP_OUTPUT  # → project/app/out/

产出文件(到 project/app/out/):

app/out/
  ├── bin/rkipc         ← 主程序(IP Camera 守护进程)
  ├── lib/              ← .so(librkfsmk.so, libwpa_client.so 以及第三方库)
  └── share/            ← 配置文件(*.ini)、字体、测试音频

第四步:build_firmware — 打包固件镜像

# build.sh 第 1943 行
function build_firmware(){
    build_env                    # 生成 env.img(uboot 环境变量分区)
    build_meta                   # 生成 meta 分区(摄像头参数、IQ 文件等)

    __PACKAGE_ROOTFS             # 解压 rootfs.tar → 合并 app_out/media_out 内容
    __PACKAGE_OEM                # 打包 OEM 分区(/oem,放 app 和 media 的 bin/lib/share)
    __PACKAGE_USERDATA           # 打包空 userdata 分区

    build_mkimg rootfs ...       # 制作 rootfs 分区镜像(UBIFS/ext4/JFFS2)
    build_mkimg oem ...          # 制作 OEM 分区镜像
    build_mkimg userdata ...     # 制作 userdata 分区镜像

    build_updateimg              # 打包 update.img(统一烧录镜像)
}

最终固件产出(到 output/image/):

output/image/
  ├── uboot.img          ← uboot 镜像
  ├── idblock.img        ← 一级引导
  ├── download.bin       ← 烧录 loader
  ├── boot.img           ← 内核 + dtb + resource
  ├── rootfs.img         ← 根文件系统(UBIFS/ext4/JFFS2)
  ├── oem.img            ← OEM 分区(app + media 的 bin/lib/share + IQ 文件)
  ├── userdata.img       ← 空 userdata 分区
  ├── env.img            ← uboot 环境变量(分区表、启动参数)
  ├── misc.img           ← misc 分区(恢复模式标记)
  └── update.img         ← 统一烧录包(包含以上所有)

编译流程全局图

                 .BoardConfig.mk  ← 您在这里配置芯片、分区、APP_TYPE 等
                        │
                  build.sh all
                        │
         ┌──────────────┼──────────────┐
         │              │              │
    build_sysdrv   build_media    build_app
         │              │              │
    ┌────┼────┐    media/ 下     app/ 下各子目录
    │    │    │    各库源码      根据 RK_APP_TYPE 选择编译
   uboot kernel rootfs
    │    │    │         │              │
    │    │    │    media_out/     app/out/
    │    │    │    (.so + .h)    (rkipc + .so + .ini)
    │    │    │         │              │
    └────┼────┼─────────┼──────────────┘
         │    │         │              │
         │    │    build_firmware      │
         │    │    __PACKAGE_ROOTFS ◄──┘
         │    │    __PACKAGE_OEM    ◄──┘(合并到 oem 分区)
         │    │         │
         │    │    output/image/
         │    │    ├── boot.img
         │    │    ├── rootfs.img
         │    │    ├── oem.img
         │    │    └── update.img
         │    │
    ┌────┘    └──────────────────────────┐
    │                                    │
  uboot 阶段编译              kernel 阶段编译
  只是 make + 脚本拷贝       不只是 make:
                              - fragment 合并配置
                              - 修改 dtb 启动参数
                              - 打包 FIT boot.img
                              - 编译驱动模块
  • 关闭内核模块编译

    • 所有模块都放在根目录下的 /sysdrv/drv_ko

    • 编译内核时编译这些模块生成 ko 文件

    • 打包时将 ko 文件复制到文件系统

    - 去除模块

    内核 config 关掉不用的模块就好了

    #比如
    CONFIG_ROCKCHIP_RKNPU=m
    #设置成
    # CONFIG_ROCKCHIP_RKNPU is not set
    
  • 关闭其他服务

    **方式1:**修改./build.sh文件

    build_all()函数下注释掉build_app

    方式2:

    .BoardConfig.mk下注释:export RK_APP_TYPE=RKIPC_RV1106

    #web 服务
    export RK_APP_IPCWEB_BACKEND=n
    # wifi 功能
    export RK_ENABLE_WIFI=n
    export RK_ENABLE_WIFI_CHIP=RTL8189FS
    # rockchip 测试,会在/根目录下生成 rockchip_test 文件用于测试
    export RK_ENABLE_ROCKCHIP_TEST=n
    #摄像头
    export RK_CAMERA_SENSOR_IQFILES="sc4336_OT01_40IRC_F16.bin sc3336_CMK-OT2119-PC1_30IRC-F16.bin sc530ai_CMK-OT2115-PC1_30IRC-F16.bin"
    export RK_CAMERA_SENSOR_CAC_BIN="CAC_sc4336_OT01_40IRC_F16 CAC_sc530ai_CMK-OT2115-PC1_30IRC-F16"
    

    /sysdrv/cfg/package.mk

    #GDB调试
    CONFIG_SYSDRV_ENABLE_GDB=n
    
  • 关闭文件系统配置

    /sysdrv/tools/board/busybox/config_normal
    
    CONFIG_UDHCPC=y
    #改成
    #CONFIG_UDHCPC is not set
    
  • 开启v4l2功能

    路径:/media/cfg/cfg.mk

    # Enable libv4l
    export CONFIG_LIBV4L=y
    

DTS — 去掉 CSI 摄像头节点(最重要)

# /root/sdk/rv1106/sysdrv/source/kernel/arch/arm/boot/dts/rv1106g-evb1-v10.dts

第 10 行改成注释:

// #include "rv1106-evb-cam.dtsi"

这行删掉后,MIPI CSI 的管线、ISP、I2C4 上的 9 个传感器全部不会初始化。内核完全不会再碰 CSI 接口。

配置adb功能

package.mk:8         CONFIG_SYSDRV_ENABLE_ADBD=y
     │
     │ MACRO_CHECK_ENABLE_PKG(RK_ENABLE_ADBD)
     ▼
Makefile.param       ENABLE_ADBD=y
     │
     │ 传到 tools/board/
     ▼
Makefile.tools.board.mk   board-build-adbd:
                           $(MAKE) -C .../android-tools
     │
     ▼
android-tools/Makefile    编译 adbd(开源 Android adbd 源码)
                          复制 adbd → out/usr/bin/adbd
                          复制 S50usbdevice → out/etc/init.d/S50usbdevice
     │
     ▼
MAROC_COPY_PKG_TO_SYSDRV_OUTPUT
     │
     ▼
根文件系统打包 → /usr/bin/adbd + /etc/init.d/S50usbdevice
     │
     ▼
开机 rcS → S50usbdevice start
             → 挂载 configfs
             → 配置 ADB/UVC 等 gadget 函数
             → echo UDC > /sys/kernel/config/usb_gadget/rockchip/UDC
             → start-stop-daemon ... /usr/bin/adbd

关闭adb自启

  • 直接关
killall adbd
/usr/bin/adbd &  #启动
  • 程序里关
system("killall adbd");
  • 进制开机自启

    S50usbdevice文件删除或者不给执行权限

修改设备树支持usb

# /delete-node/ vcc5v0-usb; 注释这个 “删除节点”
#/delete-node/ usb;
# 恢复 USB 引脚控制
&pinctrl {
	usb {
		usb_pwren: usb-pwren {
			rockchip,pins = <0 RK_PA2 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};
};
#恢复 VBUS 供电到 USB PHY
&u2phy_otg {
	vbus-supply = <&vcc5v0_usb>;
	status = "okay";
};
#USB 为 otg 模式
&usbdrd_dwc3 {
	dr_mode = "otg";
};

内核-开启UVC驱动支持

# 开启 USB Video Class 驱动(连接 UVC 摄像头)
CONFIG_MEDIA_SUPPORT=y
CONFIG_MEDIA_USB_SUPPORT=y
CONFIG_USB_VIDEO_CLASS=y
CONFIG_V4L_PLATFORM_DRIVERS=y

# 如果需要其他 USB HID 设备(键盘鼠标)
CONFIG_USB_HID=y
CONFIG_HID_GENERIC=y

查看测试

由于外接了usb切换芯片,一路usb转成两路。

引脚:GPIO0_A0_z

echo 0 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio0/direction
echo 1 > /sys/class/gpio/gpio0/value
  • usb移除测试
# ls /sys/bus/usb/devices/udhcpc: sending discover
2-0:1.0  1-1:1.0  usb2     1-0:1.0  1-1      1-1:1.1  usb1

# echo 0 > /sys/class/gpio/gpio0/value
# [  284.664998] usb 1-1: USB disconnect, device number 2

  • 查看v4l2设备
# 列出所有 video 设备
ls -la /dev/video*

# 查看 v4l2 设备
cat /sys/class/video4linux/*/name
# ls -la /dev/video*
crw-rw----    1 root     video      81,  24 /dev/video21
crw-rw----    1 root     video      81,  20 /dev/video20

# cat /sys/class/video4linux/*/name
HikCamera: UVC Camera
HikCamera: UVC Camera
  • 查看连接的usb设备
# ls /sys/bus/usb/devices
2-0:1.0  1-1:1.0  usb2     1-0:1.0  1-1      1-1:1.1  usb1

USB模式切换

写的内容实际模式
hostHost(主机)
deviceDevice/Peripheral(设备)
otgOTG(自动识别)
# find /sys -name "mode" -path "*usb*" 2>/dev/null
/sys/kernel/debug/usb/ffb00000.usb/mode
echo device /sys/kernel/debug/usb/ffb00000.usb/mode
echo host /sys/kernel/debug/usb/ffb00000.usb/mode

切换为host模式(主机模式)

  • 方式一:lvgl程序自动切换

  • 方式二:

    # 1. 导出GPIO 0
    echo 0 > /sys/class/gpio/export
    # 2. 设置GPIO方向为输出
    echo out > /sys/class/gpio/gpio0/direction
    
    # 正确的 USB → Host 切换顺序:
    # ★ 先切 PHY # 再切 DWC3  # 最后切路由
    echo host > /sys/devices/platform/ff3e0000.usb2-phy/otg_mode
    echo host > /sys/kernel/debug/usb/ffb00000.usb/mode
    echo 0 > /sys/class/gpio/export
    echo out > /sys/class/gpio/gpio0/direction
    echo 1 > /sys/class/gpio/gpio0/value
    
    # 切换摄像头(可以同步执行)
    echo 1 > /sys/class/gpio/gpio0/value
    echo host > /sys/devices/platform/ff3e0000.usb2-phy/otg_mode
    echo host > /sys/kernel/debug/usb/ffb00000.usb/mode
    

切换为device模式(设备模式)

  • 方式一:lvgl程序自动切换

  • 方式二:

    # 1. 导出GPIO 0
    echo 0 > /sys/class/gpio/export
    # 2. 设置GPIO方向为输出
    echo out > /sys/class/gpio/gpio0/direction
    
    #切换adb功能(分开下执行,要一定间隔)
    echo 0 > /sys/class/gpio/gpio0/value
     #分开执行
    echo device > /sys/kernel/debug/usb/ffb00000.usb/mode
    #分开执行
    echo "" > /sys/kernel/config/usb_gadget/rockchip/UDC 2>/dev/null
    UDC=$(ls /sys/class/udc/)
    echo $UDC > /sys/kernel/config/usb_gadget/rockchip/UDC
    
    
  • 方式三:

    使用 S50usbdevice脚本(sdk的adb功能开启后自带)

    #usb hub切换到外置usb
    echo 0 > /sys/class/gpio/gpio0/value
    #模式切换
    echo device > /sys/kernel/debug/usb/ffb00000.usb/mode
    /etc/init.d/S50usbdevice stop     #报错也没事,甚至可以
    /etc/init.d/S50usbdevice start
    
    
    echo 0 > /sys/class/gpio/gpio0/value && echo device > /sys/kernel/debug/usb/ffb00000.usb/mode && /etc/init.d/S50usbdevice stop && /etc/init.d/S50usbdevice start
    

S50脚本改版后

echo "usb_adb_en" >> /tmp/.usb_config

echo 0 > /sys/class/gpio/gpio0/value

echo "usb_ums_en" >> /tmp/.usb_config
/etc/init.d/S50usbdevice restart

UMS模式

# 1. 停止自动恢复服务
killall usbd 2>/dev/null
killall adbd 2>/dev/null

# 2. 切 Device 模式
echo device > /sys/kernel/debug/usb/ffb00000.usb/mode

# 3. 解绑当前 Gadget
echo "" > /sys/kernel/config/usb_gadget/rockchip/UDC

# 4. 清理旧 Function
rm -f /sys/kernel/config/usb_gadget/rockchip/configs/c.1/f1
rm -f /sys/kernel/config/usb_gadget/rockchip/configs/c.1/f2

# 5. 创建 mass_storage 目录
mkdir -p /sys/kernel/config/usb_gadget/rockchip/functions/mass_storage.0/lun.0

# 6. 关键:先清空旧绑定(解决 Device or resource busy)
echo "" > /sys/kernel/config/usb_gadget/rockchip/functions/mass_storage.0/lun.0/file

# 7. 绑定 SD 卡
echo /dev/mmcblk1p1 > /sys/kernel/config/usb_gadget/rockchip/functions/mass_storage.0/lun.0/file

# 8. 设置可读写
echo 0 > /sys/kernel/config/usb_gadget/rockchip/functions/mass_storage.0/lun.0/ro

# 9. 链接到配置
ln -s /sys/kernel/config/usb_gadget/rockchip/functions/mass_storage.0 /sys/kernel/config/usb_gadget/rockchip/configs/c.1/f1

# 10. 激活(ffb00000.usb 从日志确认)
echo ffb00000.usb > /sys/kernel/config/usb_gadget/rockchip/UDC
# 查看 MCLK 时钟树状态
cat /sys/kernel/debug/clk/mclk_ref_mipi0/clk_rate 2>/dev/null
cat /sys/kernel/debug/clk/mclk_ref_mipi0/clk_enabled 2>/dev/null
cat /sys/kernel/debug/clk/clk_summary 2>/dev/null | grep -i mipi

# 看 GPIO 更全的输出
cat /sys/kernel/debug/gpio

# 看看 mipi_refclk_out0 引脚当前是什么功能
cat /sys/kernel/debug/pinctrl/*/pinmux-pins 2>/dev/null | grep -E "PC4|PC5|PC6"
#测试iic获取地址
i2cdetect -y 4 #iic 4

测试

读一帧数据

#设备支持的分辨率
v4l2-ctl -d /dev/video0 --list-formats-ext
#读一帧数据
v4l2-ctl -d /dev/video0 --set-fmt-video=width=800,height=600,pixelformat=YUYV --stream-mmap --stream-count=1 --stream-to=frame.raw

头注释(1-7行)

通路: GC2145 → csi2-dphy2 → mipi1-csi2 → rkcif-mipi-lvds1

摄像头的完整数据流:传感器 → 物理层(DPHY) → 协议解析(CSI2) → 采集(CIF)

2. I2C4 节点(9-41行)

&i2c4 {
    status = "okay";
    clock-frequency = <400000>;          // 400kHz
    pinctrl-0 = <&i2c4m2_xfer>;         // Pin5=SCL, Pin7=SDA
}
  • 开启 I2C4 总线,用 M2 复用(GPIO3_C7=SCL, GPIO3_D0=SDA)
  • 400kHz 是标准快模式

3. GC2145 传感器节点(16-40行)

gc2145@3c {
    compatible = "galaxycore,gc2145";    // 匹配驱动
    reg = <0x3c>;                        // I2C 地址
    clocks = <&cru MCLK_REF_MIPI0>;      // 24MHz 时钟源
    reset-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_LOW>;  // Pin3 复位
    pinctrl-0 = <&mipi_refclk_out0>;     // Pin2 输出24MHz时钟
    rockchip,camera-module-index = <0>;  // 第0个摄像头
    rockchip,camera-module-facing = "back";  // 后置
}

端口连接:

port {
    gc2145_out: endpoint {
        remote-endpoint = <&csi_dphy2_input>;  // 连到 dphy2
        data-lanes = <1 2>;                     // 2-lane MIPI
    };
};

4. CSI DPHY2 物理层(43-78行)

&csi2_dphy_hw { status = "okay"; };      // DPHY 硬件模块

&csi2_dphy2 {                            // 对应 PHY1 (CK1+D2+D3)
    status = "okay";
    port@0 {  // 入口
        csi_dphy2_input: endpoint@2 {    // 来自 GC2145 传感器
            reg = <2>;
            remote-endpoint = <&gc2145_out>;
            data-lanes = <1 2>;
        };
    };
    port@1 {  // 出口
        csi_dphy2_output: endpoint@0 {   // 传给 mipi1_csi2
            remote-endpoint = <&mipi1_csi2_input>;
        };
    };
};
  • 这是 MIPI D-PHY 物理层,把串行数据转成并行
  • DPHY2 对应硬件 PHY1(CK1 + D2/D3 = 引脚 117-122)
  • endpoint@2reg = <2> 是 SoC 固定的硬件映射

5. MIPI CSI-2 协议层(80-110行)

&mipi1_csi2 {
    status = "okay";
    port@0 {  // 入口
        mipi1_csi2_input: endpoint@1 {
            remote-endpoint = <&csi_dphy2_output>;  // 来自 dphy2
        };
    };
    port@1 {  // 出口
        mipi1_csi2_output: endpoint@0 {
            remote-endpoint = <&cif_mipi_in>;       // 传给 CIF
        };
    };
};
  • 解析 MIPI CSI-2 协议(数据包拆分、VC 解复用)
  • 属于 mipi1(RV1106 有 mipi0 和 mipi1,我们接在 mipi1 上)

6. CIF 采集(112-126行)

&rkcif {
    status = "okay";
    pinctrl-0 = <&mipi_pins>;  // 设置所有 MIPI 引脚功能
};

&rkcif_mipi_lvds1 {
    status = "okay";
    port {
        cif_mipi_in: endpoint {
            remote-endpoint = <&mipi1_csi2_output>;  // 来自 mipi1_csi2
        };
    };
};
  • rkcif = CIF 硬件模块,pinctrl 配置全部 MIPI 引脚为功能 2
  • rkcif_mipi_lvds1 = 第二个 CIF 采集接口,接收 mipi1_csi2 解析后的数据

完整数据流

GC2145 传感器
  → 串行 MIPI 信号 (D2/D3 + CK1, 引脚117-122)
  → csi2_dphy2 (物理层, 串行转并行)
  → mipi1_csi2 (协议解析, 拆包)
  → rkcif_mipi_lvds1 (CIF 采集, DMA到内存)
  → /dev/video0

由于 GC2145.c 只用一根 lane(D2) ,所以设备树也配置为只用一根lane(D2也就是1)

注释现有的摄像头模块

CONFIG_VIDEO_GC2053=m
CONFIG_VIDEO_IMX415=m
CONFIG_VIDEO_OS04A10=m
CONFIG_VIDEO_SC200AI=m
CONFIG_VIDEO_SC3336=m
CONFIG_VIDEO_SC401AI=m
CONFIG_VIDEO_SC4336=m
CONFIG_VIDEO_SC450AI=m
CONFIG_VIDEO_SC530AI=m
#CONFIG_VIDEO_GC2053 is not set
#CONFIG_VIDEO_IMX415 is not set
#CONFIG_VIDEO_OS04A10= is not set
#CONFIG_VIDEO_SC200AI= is not set
#CONFIG_VIDEO_SC3336= is not set
#CONFIG_VIDEO_SC401AI= is not set
#CONFIG_VIDEO_SC4336= is not set
#CONFIG_VIDEO_SC450AI= is not set
#CONFIG_VIDEO_SC530AI= is not set

启用GC2145模块

内核配置文件,rv1106-evb.config(推荐)或者rv1106_defconfig加上

CONFIG_VIDEO_GC2145=m
配置状态作用
CONFIG_VIDEO_GC2145=mGC2145 传感器
CONFIG_VIDEO_ROCKCHIP_CIF=mCIF 视频采集
CONFIG_VIDEO_ROCKCHIP_ISP=mISP 图像处理
CONFIG_ROCKCHIP_DVBM=mISP 依赖的缓冲管理
CONFIG_PHY_ROCKCHIP_CSI2_DPHY=mMIPI DPHY 硬件驱动

开机自动挂载GC2145

/sysdrv/drv_ko/insmod_ko.sh
#__insmod imx415.ko
#__insmod os04a10.ko
#__insmod sc4336.ko
#__insmod sc3336.ko
#__insmod sc530ai.ko
#__insmod gc2053.ko
#__insmod sc200ai.ko
#__insmod sc401ai.ko
#__insmod sc450ai.ko

__insmod gc2145.ko

配置GC2145设备树

jw_rv1106-mipi-csi.dtsi

// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * GC2145 MIPI CSI Camera on RV1106 — 完全对照可用 DTB
 *
 * 通路: GC2145 → csi2-dphy2 → mipi1-csi2 → rkcif-mipi-lvds1
 * GC2145 输出 UYVY, 不走 ISP
 */

/* I2C4 M2:SCL=GPIO3_C7(Pin5), SDA=GPIO3_D0(Pin7) */
&i2c4 {
	status = "okay";
	clock-frequency = <400000>;
	pinctrl-names = "default";
	pinctrl-0 = <&i2c4m2_xfer>;

	gc2145: gc2145@3c {
		compatible = "galaxycore,gc2145";
		status = "okay";
		reg = <0x3c>;
		orientation=<90>;
		clocks = <&cru MCLK_REF_MIPI0>;
		clock-names = "xvclk";

		reset-gpios = <&gpio3 RK_PC5 GPIO_ACTIVE_LOW>;

		pinctrl-names = "default";
		pinctrl-0 = <&mipi_refclk_out0>;

		rockchip,camera-module-index = <0>;
		rockchip,camera-module-facing = "back";
		rockchip,camera-module-name = "CMK-OT2115-PC1";
		rockchip,camera-module-lens-name = "30IRC-F16";

		port {
			gc2145_out: endpoint {
				remote-endpoint = <&csi_dphy2_input>;
				data-lanes = <1>;
			};
		};
	};
};

/* csi2-dphy2 — 对照可用 DTB */
&csi2_dphy_hw {
	status = "okay";
};

&csi2_dphy2 {
	status = "okay";

	ports {
		#address-cells = <1>;
		#size-cells = <0>;

		port@0 {
			reg = <0>;
			#address-cells = <1>;
			#size-cells = <0>;

			csi_dphy2_input: endpoint@2 {
				reg = <2>;
				remote-endpoint = <&gc2145_out>;
				data-lanes = <1>;
			};
		};

		port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			csi_dphy2_output: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&mipi1_csi2_input>;
			};
		};
	};
};

/* mipi1-csi2 — 对照可用 DTB */
&mipi1_csi2 {
	status = "okay";

	ports {
		#address-cells = <1>;
		#size-cells = <0>;

		port@0 {
			reg = <0>;
			#address-cells = <1>;
			#size-cells = <0>;

			mipi1_csi2_input: endpoint@1 {
				reg = <1>;
				remote-endpoint = <&csi_dphy2_output>;
			};
		};

		port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			mipi1_csi2_output: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&cif_mipi_in>;
			};
		};
	};
};

&rkcif {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&mipi_pins>;
};

&rkcif_mipi_lvds1 {
	status = "okay";

	port {
		cif_mipi_in: endpoint {
			remote-endpoint = <&mipi1_csi2_output>;
		};
	};
};

测试

读一帧数据

#设备支持的分辨率
v4l2-ctl -d /dev/video0 --list-formats-ext
#读一帧数据
v4l2-ctl -d /dev/video0 --set-fmt-video=width=800,height=600,pixelformat=YUYV --stream-mmap --stream-count=1 --stream-to=frame.raw

GC2145 IQ 文件配置

GC2145 → MIPI CSI → rkcif(采集) → rkisp(ISP处理) → /dev/video0 (YUV)
                         ↓ 不经过 ISP
                    /dev/video0 (RAW Bayer)
  • 路径一:走 ISP → YUV 输出(当前 DTSI 的配置)

    GC2145 → ... → rkcif → rkisp → rkisp_vir0 → /dev/video0
                                                  YUYV 格式
    
    • ✅ 自动做 AWB/AE/去马赛克/降噪
    • ✅ 应用层直接读 YUV 画质可用
    • 需要 IQ 文件,否则颜色偏绿/偏紫、曝光不对
    • 需要 build_media(AIQ 用户态库负责加载 IQ 文件并控制 ISP 参数)

    路径二:不经过 ISP → RAW 输出(不需要任何 IQ)

    GC2145 → ... → rkcif → /dev/video0
                             RAW Bayer (GBRG)
    
    • 不需要 IQ 文件
    • 不需要 build_media(完全在内核里完成)
    • 拿到的是 RAW Bayer 数据,需要自己在 app 里做去马赛克、白平衡
    • 如果要显示彩色图像,应用层工作量比较大

SDK 自带了文件系统覆盖机制

build_firmware 最后会调用 post_overlay()build.sh 第 1922-1932 行):

function post_overlay() {
    # 读取 RK_POST_OVERLAY 变量
    # 把 project/cfg/overlay/<RK_POST_OVERLAY>/* rsync 到根文件系统
    rsync -a ... $tmp_path/overlay/$RK_POST_OVERLAY/* $RK_PROJECT_PACKAGE_ROOTFS_DIR/
}

1.在根目录下创overlay文件夹

  • 创建my_rootfs
7.png

2.配置.BoardConfig.mk

#新增
export RK_POST_OVERLAY=my_rootfs #my_rootfs必须与前面创建的对应

3.直接把文件丢进去overlay就好

4.开机自启脚本

  • overlay/my_rootfs/etc/init.d文件夹下存放Sxxx

  • 直接在 overlay 文件夹里用 chmod 设置就行,rsync 会保留权限。

  • 嵌入式 Linux 的 BusyBox init 系统(包括 RV1106 这种方案)中,/etc/init.d/ 目录下 S 开头的脚本都会在开机时自启

命名规则

前缀含义
S + 数字 + 名称开机自启,数字小 → 先执行(rcS调用)
K + 数字 + 名称关机/重启时执行(rcK 调用)
其他不会被自动执行

数字越小越早执行,建议:

脚本用途理由
S10udevudev 设备管理必须先启动,否则没有 /dev/video0
S20urandom随机数种子系统基础
S50usbdeviceUSB 配置gadget 相关
S99myapp自己 app等所有服务就绪再启动

原来的脚本

看构建流程:

sysdrv/tools/board/eudev/
├── S10udev                    ──► 复制到 /etc/init.d/S10udev
├── rules/
│   ├── 61-sd-cards-auto-mount.rules  ──► 复制到 /lib/udev/rules.d/
│   └── 61-usbdevice.rules            ──► 复制到 /lib/udev/rules.d/
└── eudev-3.2.7/  (源码)

SDK 的构建脚本在编译 rootfs 时,通过某个 Makefile 把这些文件拷贝到 rootfs 目录下(output/out/rootfs_uclibc_rv1106/),然后打包成文件系统镜像。

DRM驱动导致的加载慢

在 VOP bridge bind 时立即触发 modeset
drivers/gpu/drm/rockchip/rockchip_drm_drv.c 的 rockchip_drm_bind() 末尾主动调用一次 drm_atomic_helper_connector_hotplug():
	/*
	 * ★ 立即触发一次 hotplug → modeset,避免等待默认 ~10s 的 poll interval。
	 * 用户态 app(lvgl_demo)在 ~4s 时会通过 SET_MASTER 夺走 DRM 主控权,
	 * 此后 poll worker 被阻塞无法完成 modeset → CRTC 永不激活 → 黑屏。
	 * drm_fb_helper_hotplug_event() 会同步 probe connector +
	 * 执行 drm_client_modeset_commit() → vop_crtc_atomic_enable()
	 */
	drm_fb_helper_hotplug_event(private->fbdev_helper);

电源按钮

内核支持 rv1106-evb.config

CONFIG_KEYBOARD_GPIO=y

4个按钮用的是ADC功能来判断按下状态的

4个按钮通过串联分压连接到ADC接口,按下就分压。

ADC接口:GPIO4_C0_Z

引脚序号复用备注说明
23SARADC_IN0/GPIO4_C0_zADC判断4个按钮按下状态

设备树

只测试ADC接口

#include <dt-bindings/input/input.h>
/ {
	/* 覆盖掉 rv1106-evb.dtsi 继承来的 2 键 adc-keys */
	/delete-node/ adc-keys;

	adc-keys {
		compatible = "adc-keys";
		io-channels = <&saradc 0>;
		io-channel-names = "buttons";
		poll-interval = <100>;
		keyup-threshold-microvolt = <450000>;
	}
};

&saradc {
	status = "okay";
	vref-supply = <&vcc_1v8>;
};

测试ADC接口

# 确认 IIO 设备
ls /sys/bus/iio/devices/
# → iio:device0

# 读取原始值(比如不按、分别按 4 个键,各记一次)
cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw
# → 输出 0~4096 的整数

# 换算电压(微伏)
# microvolt = raw / 4096 * 1800000
# 例如 raw=2048 → 2048/4096*1800000 = 900000 uV

设备树完善

// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * 4 个按键 — 串联分压接入 SARADC_IN0 (GPIO4_C0_Z)
 *
 * 引脚#23: SARADC_IN0
 * 分压参考电压 1.8V
 *
 * 实测原始值(raw): 最大1024
 *   no press = 1020 (keyup) 
 *   KEY_UP   = 10 (0%)
 *   KEY_DOWN = 236 (25%)
 *   KEY_LEFT = 505 (50%)
 *   KEY_?    = 701 (75%)
 *
 */

#include <dt-bindings/input/input.h>
/ {
	gpio-keys {
		compatible = "gpio-keys";
		pinctrl-names = "default";
		pinctrl-0 = <&soc_gpio_bell>;

		bell-key {
			label = "SOC_GPIO_BELL"; //power-key 开机按钮
			linux,code = <KEY_POWER>;
			gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_HIGH>;
			debounce-interval = <10>;
			wakeup-source;
		};
	};
	adc-keys {
		compatible = "adc-keys";
		io-channels = <&saradc 0>;
		io-channel-names = "buttons";
		poll-interval = <100>;
		keyup-threshold-microvolt = <1800000>; //1.8v
        //烧录功能!不要改rk官方的
		key_volumeup-key {
			label = "key_volumeup";
			linux,code = <KEY_VOLUMEUP>;
			press-threshold-microvolt = <0>;
		};
		key_volumedown-key {
			label = "key_volumedown";
			linux,code = <KEY_VOLUMEDOWN>;
			press-threshold-microvolt = <400781>;
		};
		key-left {
			label = "KEY_LEFT";
			linux,code = <KEY_LEFT>;
			press-threshold-microvolt = <900000>;
		};
		key-camera {
			label = "KEY_CAMERA";
			linux,code = <KEY_CAMERA>;
			press-threshold-microvolt = <1350000>;
		};
	};

	restart-poweroff {
		compatible = "restart-poweroff";
	};
	
};

&saradc {
	status = "okay";
	vref-supply = <&vcc_1v8>;
};

&pinctrl {
	bell {
		soc_gpio_bell: soc-gpio-bell {
			rockchip,pins = <0 RK_PA3 RK_FUNC_GPIO &pcfg_pull_up>;
		};
	};
};

测试

# hexdump -e '16/1 "%02x " "\n"' /dev/input/event0
d4 a8 ef 5f 77 b1 09 00 01 00 72 00 01 00 00 00
d4 a8 ef 5f 77 b1 09 00 00 00 00 00 00 00 00 00
d4 a8 ef 5f e4 5a 0d 00 01 00 72 00 00 00 00 00
d4 a8 ef 5f e4 5a 0d 00 00 00 00 00 00 00 00 00
d6 a8 ef 5f a4 fa 02 00 01 00 73 00 01 00 00 00
d6 a8 ef 5f a4 fa 02 00 00 00 00 00 00 00 00 00
d6 a8 ef 5f 54 22 0c 00 01 00 73 00 00 00 00 00
d6 a8 ef 5f 54 22 0c 00 00 00 00 00 00 00 00 00

要做

官方提供的案例引脚冲突!!

vccio_sd: vccio-sd {
	compatible = "regulator-gpio";
	regulator-name = "vccio_sd";
	regulator-min-microvolt = <1800000>;
	regulator-max-microvolt = <3300000>;
	gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_HIGH>; #与电源开关检测引脚冲突
	states = <3300000 1
			  1800000 0>;
	pinctrl-names = "default";
	pinctrl-0 = <&sdmmc_volt>;
};

新增配置

&vccio_sd {
	status = "disabled";//与电源检测口冲突,禁用
};

&sdmmc {
	/delete-property/ vqmmc-supply;//关闭高速模式供电
};

识别到卡没有文件系统,需要弹出提示,是否格式化卡。然后执行mkfs.ext4 -F /dev/mmcblk1p1格式化卡

引入的#include "rv1106g-evb1-v10.dts"包含了TF卡相关的内容

所以只需要知道怎么个事就好。

sd卡作为u盘测试

  • USB Gadget支持(内核配置)

    CONFIG_USB_GADGET=y
    CONFIG_USB_CONFIGFS=y
    CONFIG_USB_CONFIGFS_MASS_STORAGE=y  #(UMS功能)
    
  • 修改usb脚本相关东西。

    echo usb_ums_en > /tmp/.usb_config
    echo ums_block=/dev/mmcblk1p1 >> /tmp/.usb_config #根据sd卡的块mmcblk1p1
    /etc/init.d/S50usbdevice restart #重启脚本
    
  • 回到adb模式

    echo usb_adb_en > /tmp/.usb_config
    /etc/init.d/S50usbdevice restart
    

1. S10udev → 启动 udev 守护进程

/root/sdk/rv1106/sysdrv/tools/board/eudev/S10udev

这个脚本启动 udevd,让内核的 hotplug 事件能被用户态处理。

2. 61-sd-cards-auto-mount.rules → 自动挂载规则

/root/sdk/rv1106/sysdrv/tools/board/eudev/rules/61-sd-cards-auto-mount.rules

规则逻辑:

插卡 → 内核发出 ADD 事件 → udev 检测到 mmcblk* 
    → blkid 识别文件系统类型 (vfat/ext4/ntfs...)
    → mount 到 /mnt/sdcard
拔卡 → 自动 umount /mnt/sdcard

位置: /lib/udev/rules.d/61-sd-cards-auto-mount.rules

执行时机: 每次插拔 SD 卡时,由 udevd 守护进程触发。

完整流程:

系统启动
  └─ S10udev 启动 udevd 守护进程(常驻后台)
        │
插卡 ──► 内核检测到新 mmcblk 设备
        │
        ▼ 发送 uevent 通知 udevd
        │
        ▼ udevd 按顺序扫描 /lib/udev/rules.d/*.rules
        │
        ▼ 匹配到 61-sd-cards-auto-mount.rules
        │   KERNEL=="mmcblk*[0-9]" → 是分区
        │   SUBSYSTEM=="block"     → 是块设备
        │   ATTRS{type}=="SD"      → 是 SD 卡
        │   └─ blkid 识别文件系统 → mount /dev/mmcblk1p1 /mnt/sdcard
        │
拔卡 ──► 内核检测到移除 → udev 匹配 ACTION=="remove"
        └─ umount /mnt/sdcard

如果要改挂载路径或参数,直接编辑 61-sd-cards-auto-mount.rules 里的 mount_options_* 和挂载点 /mnt/sdcard 就行。

相关芯片

功能IC型号特点
充电管理SLM6300
电量计CW2015CHBD
电源管理EA3036CQBR

电源开关检测按钮:62 ,SOC_GPIO_BELL/GPIO0_A3

电源按键管理

内核配置

只开启中断触发就好

CONFIG_KEYBOARD_GPIO=y #中断触发
CONFIG_KEYBOARD_GPIO_POLLED=y #轮询触发

设备树加上这个按钮

gpio-keys {
    compatible = "gpio-keys";         // 绑定内核 drivers/input/keyboard/gpio_keys.c
    pinctrl-names = "default";        // 使用 default 状态的 pinmux 配置
    pinctrl-0 = <&soc_gpio_bell>;     // 引用下面的 pinmux 定义

    bell-key {
        label = "SOC_GPIO_BELL";       // 名字,debug 时能看到
        linux,code = <KEY_POWER>;      // 按键码 116,上报给 input 子系统
        gpios = <&gpio0 RK_PA3 GPIO_ACTIVE_LOW>;  // GPIO0_A3,低电平触发
        debounce-interval = <10>;      // 10ms 消抖
        wakeup-source;                 // 休眠时这个键能唤醒 SoC
    };
};


&pinctrl {
    bell {                              // 分组名,方便管理
        soc_gpio_bell: soc-gpio-bell {  // 标签 + 节点名
            rockchip,pins = <0 RK_PA3 RK_FUNC_GPIO &pcfg_pull_up>;
        };
    };
};

电源保持

当内核启动时需要拉高power-hold引脚来给soc持续供电,拉低这个引脚关机

名称引脚号复用关系
power_hold60PWM3_IR_M0/GPIO0_A2_D

由于power-hold引脚和usb供电引脚一起。

  • 方式1:

    插上电usb供电,能保持供电开机。

  • 方式2:

    电池供电,如果usb配置了host模式(主机模式对外供电),就能保持一直供电开机。

  • 方式3:

    手动拉高保持power_hold引脚。保持一直供电开机。

    /* POW_HOLD: 内核启动后拉高 GPIO0_A2 持续供电 */
    gpio-poweroff {
    	compatible = "regulator-fixed";
    	regulator-name = "gpio-poweroff";
    	regulator-always-on;
    	regulator-boot-on;
    	enable-active-high;
    	gpio = <&gpio0 RK_PA2 GPIO_ACTIVE_HIGH>;
    	pinctrl-names = "default";
    	pinctrl-0 = <&power_hold>;
    };
    &pinctrl {
    	power_hold: power-hold {
    		rockchip,pins = <0 RK_PA2 RK_FUNC_GPIO &pcfg_output_high>;
    	};
    };
    

充电管理

引脚关系:查看

  • 内核驱动地址

    /kernel/drivers/power/supply/gpio-charger.c
    
  • 内核配置

    CONFIG_CHARGER_GPIO=y
    
  • 设备树配置

    charger: charger {
    	compatible = "gpio-charger";
    	charger-type = "mains";
    	gpios = <&gpio3 RK_PB6 GPIO_ACTIVE_LOW>;
    	pinctrl-names = "default";
    	pinctrl-0 = <&charger_pins>;
    };
    &pinctrl {
    	charger {
            charger_pins: charger-pins {
                rockchip,pins = <3 RK_PB6 RK_FUNC_GPIO &pcfg_pull_up>;
            };
        };
    };
    
  • 命令行测试

    # 无报错
    dmesg | grep cw2015
    
    # 充电器状态
    cat /sys/class/power_supply/charger/online
    

电量管理

引脚关系:查看

  • 内核已经有这个驱动了:
/root/sdk/rv1106/sysdrv/source/kernel/drivers/power/supply/cw2015_battery.c
  • 内核开启配置
CONFIG_BATTERY_CW2015=y
  • 直接在设备树增加配置
&i2c0 {
	status = "okay";

	cw2015@62 {
		compatible = "cellwise,cw2015";
		reg = <0x62>;

		/*
		 * 电池 profile(64 字节)由 CellWise 给出。
		 * 以下是一个典型 3.7V Li-Po / 18650 的参考值,
		 * 实测后需根据你们实际电芯校准。
		 */
		cellwise,battery-profile = /bits/ 8 <
			0x17 0x67 0x80 0x73 0x6E 0x6C 0x6B 0x63
			0x77 0x51 0x5C 0x58 0x50 0x4C 0x48 0x36
			0x15 0x0C 0x0C 0x19 0x5B 0x7D 0x6F 0x69
			0x69 0x5B 0x0C 0x29 0x20 0x40 0x52 0x59
			0x57 0x56 0x54 0x4F 0x3B 0x1F 0x7F 0x17
			0x06 0x1A 0x30 0x5A 0x85 0x93 0x96 0x2D
			0x48 0x77 0x9C 0xB3 0x80 0x52 0x94 0xCB
			0x2F 0x00 0x64 0xA5 0xB5 0x11 0xF0 0x11
		>;
		power-supplies = <&charger>;//与充电管理对应
		cellwise,monitor-interval-ms = <5000>;
	};
};
  • 电量管理充电检测日志

    /kernel/drivers/power/supply/cw2015_battery.c 第350行

    注释掉就不会报日志

ret = power_supply_am_i_supplied(cw_bat->rk_bat);
if (ret < 0) {
	// dev_warn(cw_bat->dev, "Failed to get supply state: %d\n", ret);
}
  • 命令行测试
# 电量百分比
cat /sys/class/power_supply/cw2015-battery/capacity

# 当前电压(微伏,除以 1000000 得伏特)
cat /sys/class/power_supply/cw2015-battery/voltage_now

# 充放电状态
cat /sys/class/power_supply/cw2015-battery/status

设备树开启即可

&rtc {
	status = "okay";
};

注意:没有必要直接操作/dev/rtc0这个节点

测试命令

  • 查看

    hwclock -r -f /dev/rtc0
    
  • 设置(不设置初始值,rtc不会自动跳动)

    date -s "2026-07-04 12:00:00"
    
  • 把系统时间写入 RTC

    hwclock -w -f /dev/rtc0
    
  • 把 RTC 时间读到系统

    hwclock -s -f /dev/rtc0
    
  • 直接读时间字符串

    cat /sys/class/rtc/rtc0/date  # 2026-07-06
    cat /sys/class/rtc/rtc0/time  # 04:30:53
    
  • 查看时间戳

    cat /sys/class/rtc/rtc0/since_epoch
    

用户usb_hub切换

电源保持io口拉高拉低。

SD卡检测。

新增panel-lh24030c50.c文件

配置makefile

  • 路径:/kernel/drivers/gpu/drm/panel/Makefile

  • 新增一行

    obj-$(CONFIG_DRM_PANEL_LH24030C50) += panel-lh24030c50.o
    
    • 当内核配置系统检测到 CONFIG_DRM_PANEL_LH24030C50 被赋值为 y(内建)或 m(模块)时,该行会生效。
    • .c 文件编译为对应的 .o 目标文件,并最终链接进内核镜像(y)或生成独立的 .ko 内核模块文件(m)。

配置Kconfig

  • 路径:/kernel/drivers/gpu/drm/panel/Kconfig

  • 新增

    config DRM_PANEL_LH24030C50
    	tristate "LH24030C50 RGB panel"
    	depends on OF && SPI
    	depends on BACKLIGHT_CLASS_DEVICE
    	help
    	  Say Y here if you want to enable support for the LH24030C50
    	  RGB panel module driven by an ST7789V-compatible controller.
    
    • tristate:表示该选项支持三种状态——Y(内建)、M(模块)、N(不编译)。对应Makefile中的 obj-*
    • depends on OF && SPI依赖约束。只有启用了设备树(OF)和SPI总线支持时,该选项才会出现在菜单中。这防止了非硬件平台误选,也保证了编译时能引用到SPI子系统的头文件和符号。
    • depends on BACKLIGHT_CLASS_DEVICE:强制依赖背光类设备。因为面板需要背光调节功能,若内核未开启背光支持,该驱动编译会因找不到 struct backlight_device 等定义而失败。
    • help:给开发者或用户看的说明文本,描述该驱动适用的硬件型号。

内核配置

开启内核配置

kernel_defconfig

CONFIG_DRM_PANEL_LH24030C50=y #与上面makefile中的 CONFIG_DRM_PANEL_LH24030C50 一致
# CONFIG_DRM_PANEL_SIMPLE=y 如果用设备树用simple-panel
CONFIG_BACKLIGHT_CLASS_DEVICE=y
CONFIG_BACKLIGHT_GPIO=y

关闭内核配置

#CONFIG_FB_TFT=y
#CONFIG_DRM_PANEL_SITRONIX_ST7789V=y
#CONFIG_FB_TFT_ST7735R=y
#CONFIG_FB_TFT_ST7789V=y

频率过高

rv1106g-luckfox-pico-pro-max.dts

/**********CRU**********/
&cru {
	assigned-clocks = <&cru 3>;
	assigned-clock-rates = <216000000>;
};

aa_rv1106-lcd.dtsi

&vop {
	assigned-clocks = <&cru 201>;
	assigned-clock-parents = <&cru 3>;
	status = "okay";
};
timing0: panel-timing {
	clock-frequency = <7000000>;
	hactive = <240>;
	vactive = <320>;
	hfront-porch = <1>;
	hback-porch = <20>;
	hsync-len = <10>;
	vfront-porch = <8>;
	vback-porch = <2>;
	vsync-len = <6>;
	hsync-active = <0>;
	vsync-active = <0>;
	de-active = <1>;
	pixelclk-active = <0>;
};

rv1106-luckfox-pico-pro-max-ipc.dtsi

注释
/*****************************PINCTRL********************************/
// SPI
// &spi0 {
// 	pinctrl-0 = <&spi0m0_clk &spi0m0_miso &spi0m0_mosi &spi0m0_cs0>;
// 	#address-cells = <1>;
// 	#size-cells = <0>;
// 	spidev@0 {
// 	compatible = "rockchip,spidev";
// 		spi-max-frequency = <50000000>;
// 		reg = <0>;
// 	};

// 	fbtft@0 {
// 		compatible = "sitronix,st7789v";
// 		reg = <0>;
// 		spi-max-frequency = <20000000>;
// 		fps = <30>;
// 		buswidth = <8>;
// 		debug = <0x7>;
// 		led-gpios = <&gpio2 RK_PB0 GPIO_ACTIVE_HIGH>;//BL
// 		dc-gpios = <&gpio2 RK_PB1 GPIO_ACTIVE_HIGH>;//DC
// 		reset-gpios = <&gpio1 RK_PC3 GPIO_ACTIVE_LOW>;//RES
// 	};
// };
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * Copyright (c) 2022 Rockchip Electronics Co., Ltd.
 */

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/display/media-bus-format.h>
#include <dt-bindings/clock/rv1106-cru.h>

/ {
	backlight: backlight {
		status = "okay";
		compatible = "gpio-backlight";
		gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
		default-on;
		power-supply = <&vcc_3v3>;
	};
	reserved-memory {
		#address-cells = <1>;
		#size-cells = <1>;
		ranges;

		drm_logo: drm-logo@00000000 {
			compatible = "rockchip,drm-logo";
			reg = <0x0 0x0>;
		};
	};
};

&spi1 {
	status = "okay";
	/delete-property/ pinctrl-0;
	/delete-property/ pinctrl-names;
	/* No pinctrl - pins used as GPIO for 9-bit SPI bitbang */

	panel: panel@0 {
		compatible = "lh,lh24030c50";
		reg = <0>;
		spi-max-frequency = <10000000>;
		reset-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_LOW>;
		spi-scl-gpios = <&gpio4 RK_PA7 GPIO_ACTIVE_HIGH>;
		spi-sdi-gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_HIGH>;
		spi-cs-gpios = <&gpio4 RK_PA5 GPIO_ACTIVE_HIGH>;
		power-supply = <&vcc_3v3>;
		backlight = <&backlight>;
		status = "okay";

		display-timings {
			native-mode = <&timing0>;
			timing0: panel-timing {
				clock-frequency = <7000000>;
				hactive = <240>;
				vactive = <320>;
				hfront-porch = <0>;
				hback-porch = <51>;
				hsync-len = <51>;
				vfront-porch = <0>;
				vback-porch = <12>;
				vsync-len = <12>;
				hsync-active = <0>;
				vsync-active = <0>;
				de-active = <1>;
				pixelclk-active = <1>;
			};
		};

		port {
			panel_in_rgb: endpoint {
				remote-endpoint = <&rgb_out_panel>;
			};
		};
	};
};

&display_subsystem {
	status = "okay";
	logo-memory-region = <&drm_logo>;
};

&rgb {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&lcd_pins>;
	ports {
		rgb_out: port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			rgb_out_panel: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&panel_in_rgb>;
			};
		};
	};
};

&rgb_in_vop {
	status = "okay";
};

&route_rgb {
	status = "okay";
};

&vop {
	assigned-clocks = <&cru PLL_CPLL>;
	assigned-clock-rates = <216000000>;
	status = "okay";
};

/*
 * LCD RGB data D8-D17 reuses the SDIO pin group.
 */
&sdio {
	status = "disabled";
};

&emmc {
	status = "disabled";
};

// SPDX-License-Identifier: GPL-2.0-only
/*
 * LH24030C50 RGB panel driver based on the ST7789V controller.
 */

#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/regulator/consumer.h>
#include <linux/spi/spi.h>
#include <video/display_timing.h>
#include <video/videomode.h>
#include <video/of_videomode.h>

#include <drm/drm_device.h>
#include <drm/drm_modes.h>
#include <drm/drm_panel.h>
#include <linux/media-bus-format.h>

struct st7789v {
	struct drm_panel panel;
	struct spi_device *spi;
	struct gpio_desc *reset;
	struct gpio_desc *sclk;
	struct gpio_desc *mosi;
	struct gpio_desc *cs;
	struct regulator *power;
};
static inline struct st7789v *panel_to_st7789v(struct drm_panel *panel)
{
	return container_of(panel, struct st7789v, panel);
}

static void st7789v_spi_bitbang(struct st7789v *ctx, int dc, u8 data)
{
	int i;

	gpiod_set_value_cansleep(ctx->cs, 0);
	udelay(1);
	gpiod_set_value_cansleep(ctx->sclk, 0);
	gpiod_set_value_cansleep(ctx->mosi, dc ? 1 : 0);
	udelay(2);
	gpiod_set_value_cansleep(ctx->sclk, 1);
	udelay(2);
	for (i = 0; i < 8; i++) {
		gpiod_set_value_cansleep(ctx->sclk, 0);
		gpiod_set_value_cansleep(ctx->mosi, (data >> (7 - i)) & 1);
		udelay(2);
		gpiod_set_value_cansleep(ctx->sclk, 1);
		udelay(2);
	}
	gpiod_set_value_cansleep(ctx->sclk, 0);
	udelay(1);
	gpiod_set_value_cansleep(ctx->cs, 1);
	udelay(1);
}

static int st7789v_write_command(struct st7789v *ctx, u8 cmd)
{
	st7789v_spi_bitbang(ctx, 0, cmd);
	return 0;
}

static int st7789v_write_data(struct st7789v *ctx, u8 data)
{
	st7789v_spi_bitbang(ctx, 1, data);
	return 0;
}

static void st7789v_spi_idle(struct st7789v *ctx)
{
	gpiod_set_value_cansleep(ctx->cs, 1);
	gpiod_set_value_cansleep(ctx->sclk, 0);
	gpiod_set_value_cansleep(ctx->mosi, 0);
}

static const struct drm_display_mode default_mode = {
	.clock = 7000,
	.hdisplay = 240,
	.hsync_start = 240,
	.hsync_end = 240 + 51,
	.htotal = 240 + 51 + 51,
	.vdisplay = 320,
	.vsync_start = 320,
	.vsync_end = 320 + 12,
	.vtotal = 320 + 12 + 12,
	.flags = DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_NVSYNC,
};

static int st7789v_get_modes(struct drm_panel *panel,
			     struct drm_connector *connector)
{
	struct drm_display_mode *mode;
	const u32 bus_format = MEDIA_BUS_FMT_RGB666_1X18;
	const struct drm_display_mode *src_mode = &default_mode;
	struct videomode vm;

	if (!of_get_videomode(panel->dev->of_node, &vm, 0)) {
		struct drm_display_mode *dt_mode;

		dt_mode = drm_mode_create(connector->dev);
		if (dt_mode) {
			drm_display_mode_from_videomode(&vm, dt_mode);
			dt_mode->type = DRM_MODE_TYPE_DRIVER |
					DRM_MODE_TYPE_PREFERRED;
			src_mode = dt_mode;
		}
	}

	mode = drm_mode_duplicate(connector->dev, src_mode);
	if (!mode) {
		dev_err(panel->dev, "failed to add mode %ux%ux@%u\n",
			src_mode->hdisplay, src_mode->vdisplay,
			drm_mode_vrefresh(src_mode));
		return -ENOMEM;
	}

	drm_mode_set_name(mode);
	mode->type = DRM_MODE_TYPE_DRIVER | DRM_MODE_TYPE_PREFERRED;
	drm_mode_probed_add(connector, mode);

	connector->display_info.width_mm = 43;
	connector->display_info.height_mm = 57;

	drm_display_info_set_bus_formats(&connector->display_info,
					  &bus_format, 1);
	connector->display_info.bus_flags = DRM_BUS_FLAG_DE_HIGH |
					    DRM_BUS_FLAG_PIXDATA_DRIVE_NEGEDGE;

	return 1;
}
static void LCD_Init(struct st7789v *ctx)
{
	st7789v_spi_idle(ctx);
	st7789v_write_command(ctx, 0x01);
	msleep(150);
	st7789v_write_command(ctx, 0x11);
	msleep(120);
	//--------------------------------Display and color format setting----------------------------//
	st7789v_write_command(ctx, 0x36);
	st7789v_write_data(ctx, 0x08);
	st7789v_write_command(ctx, 0x3a);
	st7789v_write_data(ctx, 0x06);

	st7789v_write_command(ctx, 0xB0);
	st7789v_write_data(ctx, 0x12); // 第1参数:RM=1, DM=01(RGB)
	st7789v_write_data(ctx, 0xC0); // 第2参数:EPF1 EPF0=11,其余控制位0

	st7789v_write_command(ctx, 0xB1);
	st7789v_write_data(ctx, 0x40);
	st7789v_write_data(ctx, 0x04);
	st7789v_write_data(ctx, 0x0a);
	//--------------------------------ST7789S Frame rate setting----------------------------------//
	st7789v_write_command(ctx, 0xb2);
	st7789v_write_data(ctx, 0x0c);
	st7789v_write_data(ctx, 0x0c);
	st7789v_write_data(ctx, 0x00);
	st7789v_write_data(ctx, 0x33);
	st7789v_write_data(ctx, 0x33);
	st7789v_write_command(ctx, 0xb7);
	st7789v_write_data(ctx, 0x35);
	//---------------------------------ST7789S Power setting--------------------------------------//
	st7789v_write_command(ctx, 0xbb);
	st7789v_write_data(ctx, 0x2b);
	st7789v_write_command(ctx, 0xc0);
	st7789v_write_data(ctx, 0x2c);
	st7789v_write_command(ctx, 0xc2);
	st7789v_write_data(ctx, 0x01);
	st7789v_write_command(ctx, 0xc3);
	st7789v_write_data(ctx, 0x11);
	st7789v_write_command(ctx, 0xc4);
	st7789v_write_data(ctx, 0x20);
	st7789v_write_command(ctx, 0xc6);
	st7789v_write_data(ctx, 0x0f);
	st7789v_write_command(ctx, 0xd0);
	st7789v_write_data(ctx, 0xa4);
	st7789v_write_data(ctx, 0xa1);
	//--------------------------------ST7789S gamma setting---------------------------------------//
	st7789v_write_command(ctx, 0xe0);
	st7789v_write_data(ctx, 0xd0);
	st7789v_write_data(ctx, 0x00);
	st7789v_write_data(ctx, 0x06);
	st7789v_write_data(ctx, 0x09);
	st7789v_write_data(ctx, 0x0b);
	st7789v_write_data(ctx, 0x2a);
	st7789v_write_data(ctx, 0x3c);
	st7789v_write_data(ctx, 0x55);
	st7789v_write_data(ctx, 0x4b);
	st7789v_write_data(ctx, 0x08);
	st7789v_write_data(ctx, 0x16);
	st7789v_write_data(ctx, 0x14);
	st7789v_write_data(ctx, 0x19);
	st7789v_write_data(ctx, 0x20);

	st7789v_write_command(ctx, 0xe1);
	st7789v_write_data(ctx, 0xd0);
	st7789v_write_data(ctx, 0x00);
	st7789v_write_data(ctx, 0x06);
	st7789v_write_data(ctx, 0x09);
	st7789v_write_data(ctx, 0x0b);
	st7789v_write_data(ctx, 0x29);
	st7789v_write_data(ctx, 0x36);
	st7789v_write_data(ctx, 0x54);
	st7789v_write_data(ctx, 0x4b);
	st7789v_write_data(ctx, 0x0d);
	st7789v_write_data(ctx, 0x16);
	st7789v_write_data(ctx, 0x14);
	st7789v_write_data(ctx, 0x21);
	st7789v_write_data(ctx, 0x20);
}

static int st7789v_prepare(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);
	int ret;

	ret = regulator_enable(ctx->power);
	if (ret)
		return ret;

	gpiod_set_value_cansleep(ctx->reset, 1);
	msleep(50);
	gpiod_set_value_cansleep(ctx->reset, 0);
	msleep(50);
	st7789v_spi_idle(ctx);
	msleep(150);

	LCD_Init(ctx);

	return 0;
}

static int st7789v_enable(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);
	msleep(20); // 等待第一帧脏画面刷新完毕
	st7789v_write_command(ctx, 0x29);
	msleep(50);
	return 0;
}

static int st7789v_disable(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);

	st7789v_write_command(ctx, 0x28);

	return 0;
}

static int st7789v_unprepare(struct drm_panel *panel)
{
	struct st7789v *ctx = panel_to_st7789v(panel);

	st7789v_write_command(ctx, 0x10);
	msleep(120);
	gpiod_set_value_cansleep(ctx->reset, 1);
	st7789v_spi_idle(ctx);

	regulator_disable(ctx->power);

	return 0;
}

static const struct drm_panel_funcs st7789v_drm_funcs = {
	.disable	= st7789v_disable,
	.enable		= st7789v_enable,
	.get_modes	= st7789v_get_modes,
	.prepare	= st7789v_prepare,
	.unprepare	= st7789v_unprepare,
};

static int st7789v_probe(struct spi_device *spi)
{
	struct st7789v *ctx;
	int ret;

	ctx = devm_kzalloc(&spi->dev, sizeof(*ctx), GFP_KERNEL);
	if (!ctx)
		return -ENOMEM;

	spi_set_drvdata(spi, ctx);
	ctx->spi = spi;

	drm_panel_init(&ctx->panel, &spi->dev, &st7789v_drm_funcs,
		       DRM_MODE_CONNECTOR_DPI);

	ctx->power = devm_regulator_get(&spi->dev, "power");
	if (IS_ERR(ctx->power))
		return PTR_ERR(ctx->power);

	ctx->reset = devm_gpiod_get(&spi->dev, "reset", GPIOD_OUT_HIGH);
	if (IS_ERR(ctx->reset)) {
		dev_err(&spi->dev, "Couldn't get our reset line\n");
		return PTR_ERR(ctx->reset);
	}

	ctx->sclk = devm_gpiod_get(&spi->dev, "spi-scl", GPIOD_OUT_LOW);
	if (IS_ERR(ctx->sclk))
		return PTR_ERR(ctx->sclk);
	ctx->mosi = devm_gpiod_get(&spi->dev, "spi-sdi", GPIOD_OUT_LOW);
	if (IS_ERR(ctx->mosi))
		return PTR_ERR(ctx->mosi);
	ctx->cs = devm_gpiod_get(&spi->dev, "spi-cs", GPIOD_OUT_HIGH);
	if (IS_ERR(ctx->cs))
		return PTR_ERR(ctx->cs);

	st7789v_spi_idle(ctx);

	ret = drm_panel_of_backlight(&ctx->panel);
	if (ret)
		return ret;

	drm_panel_add(&ctx->panel);

	return 0;
}

static int st7789v_remove(struct spi_device *spi)
{
	struct st7789v *ctx = spi_get_drvdata(spi);

	drm_panel_remove(&ctx->panel);

	return 0;
}

static const struct of_device_id st7789v_of_match[] = {
	{ .compatible = "lh,lh24030c50" },
	{ }
};
MODULE_DEVICE_TABLE(of, st7789v_of_match);

static struct spi_driver st7789v_driver = {
	.probe = st7789v_probe,
	.remove = st7789v_remove,
	.driver = {
		.name = "lh24030c50",
		.of_match_table = st7789v_of_match,
	},
};
module_spi_driver(st7789v_driver);

MODULE_AUTHOR("Maxime Ripard <maxime.ripard@free-electrons.com>");
MODULE_DESCRIPTION("LH24030C50 RGB LCD panel driver");
MODULE_LICENSE("GPL v2");

幸狐sdk

建议直接用配置好的docker环境

  • 直接在windows安装docker
  • 再加载提供的镜像。
  • 再使用vscode安装dev contarner插件,直接使用界面进行开发。

安装docker

省略

加载镜像

镜像文件在luckfox_pico_docker.tar

docker load -i xxx.tar

启动容器

  • windows下(必须使用绝对路径)
docker run -it -d --name rk-develop --privileged -v E:\rk-develop\v-share:/home 4a59acf452d5 /bin/bash
  • linux下(可以使用相对路径)
docker run -it -d --name rk-develop --privileged -v ./v-share:/home 4a59acf452d5 /bin/bash

复制SDK到容器

直接将文件复制到启动容器命令的路径:E:\rk-develop\v-share,./v-share

vscode操作

安装插件

安装插件

安装插件

省略

可以借鉴幸狐官方:[luckfox-sdk环境配置](SDK 镜像编译 | LUCKFOX WIKI)

幸狐的SDK和RK官方的SDK目录结构基本一致

SDK源码:SDK-Source

docker开发环境:docker开发环境

vm开发环境:vm开发环境

编译生成的文件放在:compile-files

步骤:

  1. 选择开发板,作用是生成一下步的**.BoardConfig.mk**文件

    详细步骤:002_幸狐sdk编译-选择开发板

  2. 修改.BoardConfig.mk文件,主要是注释/删除摄像头配置、修改env适配flash大小等操作

    详细步骤:003_幸狐sdk编译-修改.BoardConfig.mk

  3. 适配屏幕,这一步需要修改设备树驱动文件用来点亮屏幕。

    详细步骤:004_幸狐sdk编译-适配显示屏

  4. 适配摄像头,需要修改设备树驱动文件支持摄像头

作用:

生成.BoardConfig.mk,文件是所有配置信息的集合。

  • uboot的配置信息:
# Uboot defconfig
export RK_UBOOT_DEFCONFIG=luckfox_rv1106_uboot_defconfig
  • 内核配置信息:
# Kernel defconfig
export RK_KERNEL_DEFCONFIG=luckfox_rv1106_linux_defconfig
  • 内核设备树
# Kernel dts
export RK_KERNEL_DTS=rv1106g-luckfox-pico-pro-max.dts
  • oem分区配置
# enable install app to oem partition
export RK_BUILD_APP_TO_OEM_PARTITION=y
  • 等等...

步骤:

执行

./build.sh lunch 
You're building on Linux
  Lunch menu...pick the Luckfox Pico hardware version:
  选择 Luckfox Pico 硬件版本:
                [0] RV1103_Luckfox_Pico
                [1] RV1103_Luckfox_Pico_Mini
                [2] RV1103_Luckfox_Pico_Plus
                [3] RV1103_Luckfox_Pico_WebBee
                [4] RV1106_Luckfox_Pico_Pro_Max
                [5] RV1106_Luckfox_Pico_Ultra
                [6] RV1106_Luckfox_Pico_Pi
                [7] RV1106_Luckfox_Pico_86Panel
                [8] RV1106_Luckfox_Pico_Zero
                [9] custom
Which would you like? [0~9][default:0]: 4
  Lunch menu...pick the boot medium:
  选择启动媒介:
                [0] SD_CARD
                [1] SPI_NAND
Which would you like? [0~1][default:0]: 1
  Lunch menu...pick the system version:
  选择系统版本:
                [0] Buildroot 
Which would you like? [0][default:0]: 0 

配置分析

#misc image
export RK_MISC=wipe_all-misc.img

wipe_all 类型的 misc 镜像通常用于:

  • 首次烧录后:清除残留数据,确保设备以干净状态启动
  • 出厂重置:清除 userdata、oem 等用户数据分区

删除overlay文件的脚本

# specify pre.sh for delete/overlay files
export RK_PRE_BUILD_OEM_SCRIPT=luckfox-buildroot-oem-pre.sh

# specify post.sh for delete/overlay files
export RK_PRE_BUILD_USERDATA_SCRIPT=luckfox-userdata-pre.sh

配置overlay文件的脚本

# declare overlay directory
export RK_POST_OVERLAY="overlay-luckfox-config overlay-luckfox-buildroot-init overlay-luckfox-buildroot-shadow overlay-luckfox-wifibt-firmware"

修改flash占用

210M(rootfs)改成60M(rootfs)

export RK_PARTITION_CMD_IN_ENV="256K(env),256K@256K(idblock),512K(uboot),4M(boot),30M(oem),10M(userdata),60M(rootfs)"

关闭wifi以及rockchip测试

注释


# enable rockchip test
#export RK_ENABLE_ROCKCHIP_TEST=y

# enable rockchip wifi
#export RK_ENABLE_WIFI=y

# config wifi ssid and passwd
#export LF_WIFI_SSID="Your wifi ssid"
#export LF_WIFI_PSK="Your wifi password"

关闭摄像头相关配置

注释

# Config sensor IQ files
# RK_CAMERA_SENSOR_IQFILES format:
#     "iqfile1 iqfile2 iqfile3 ..."
# ./build.sh media and copy <SDK root dir>/output/out/media_out/isp_iqfiles/$RK_CAMERA_SENSOR_IQFILES
#export RK_CAMERA_SENSOR_IQFILES="sc4336_OT01_40IRC_F16.json sc3336_CMK-OT2119-PC1_30IRC-F16.json mis5001_CMK-OT2115-PC1_30IRC-F16.json"
#export RK_CAMERA_SENSOR_IQFILES="sc4336_OT01_40IRC_F16.json sc3336_CMK-OT2119-PC1_30IRC-F16.json sc530ai_CMK-OT2115-PC1_30IRC-F16.json"

# Config sensor lens CAC calibrattion bin files
#export RK_CAMERA_SENSOR_CAC_BIN="CAC_sc4336_OT01_40IRC_F16"
#export RK_CAMERA_SENSOR_CAC_BIN="CAC_sc4336_OT01_40IRC_F16 CAC_sc530ai_CMK-OT2115-PC1_30IRC-F16"

流程(在内核设备树)

  1. 首先确认使用的屏幕

    这里使用的屏幕:ST7789V + CTC2.8 3SPI_RGB18BIT 需要spi去做初始化。

    有些通用并口LCD屏编写设备树的方式又不一样。

  2. 查看PCB原理图,确认接线。

    屏幕接线

  3. 找到背光引脚,先点亮背光。

背光

  1. 修改背光设备树(GPIO控制背光)

    新建aa_rv1106-lcd.dtsi测试文件。在使用的dts中#include引入,注意不能有panel节点,panel节点会控制背光,如果panel初始化失败,背光就没有效果。

    1. 新增背光节点

      #include <dt-bindings/pinctrl/rockchip.h>
      
      / {
      	backlight: backlight {
      		status = "okay";
      		compatible = "gpio-backlight";
      		gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
      		default-on;
      		power-supply = <&vcc_3v3>;
      	};
      };
      
    2. 开启内核支持

      CONFIG_BACKLIGHT_GPIO=y
      
    3. 查看是否有配置

      ls /sys/class/backlight #下有 backlight
      
    4. 控制背光

      echo 0 > /sys/class/backlight/backlight/brightness
      echo 1 > /sys/class/backlight/backlight/brightness
      
  2. 修改背光设备树(PWM控制背光)

    1. 新增节点

      #include <dt-bindings/pinctrl/rockchip.h>
      / {
      	backlight: backlight {
      		status = "okay";
      		compatible = "pwm-backlight";
      		pwms = <&pwm3 0 25000 1>;
      		brightness-levels = <0 10 30 60 100 150 200 255>;
      		default-brightness-level = <7>;
      		power-supply = <&vcc_3v3>;
      	};
      };
      
      /* PWM3_M1 = GPIO1_B0, 用于背光 */
      &pwm3 {
      	status = "okay";
      	pinctrl-names = "active";
      	pinctrl-0 = <&pwm3m1_pins>;
      };
      
    2. 内核支持

      CONFIG_PWM=y
      #CONFIG_PWM_ROCKCHIP=y
      #CONFIG_BACKLIGHT_PWM=y
      

uboot使用的设备树

在uboot配置文件中声明

CONFIG_DEFAULT_DEVICE_TREE="rv1106-evb"
  • 路径:
/root/luckfox-dev/sdk/sysdrv/source/uboot/u-boot/arch/arm/dts/rv1106-evb.dts

新增一个dtsi

dtsi配置

  • rv1106-evb.dts引入这个配置

    #include "aa_rv1106-lcd.dtsi"
    

uboot配置文件开启

# Display / LCD
CONFIG_DM_VIDEO=y
CONFIG_DRM_ROCKCHIP=y
CONFIG_DRM_MEM_RESERVED_SIZE_MBYTES=32
CONFIG_DRM_ROCKCHIP_RGB=y
CONFIG_DRM_ROCKCHIP_PANEL=y
CONFIG_BACKLIGHT_GPIO=y
#CONFIG_I2C_EDID=y 看情况
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * LCD panel: ST7789V 2.8" 240x320 RGB 18-bit + 3SPI
 * Backlight: GPIO1_B0
 * LCD Reset: GPIO1_B1
 * SPI init:  GPIO4_A7(CLK) GPIO4_A5(CS) GPIO4_A1(MOSI)
 */

#include <linux/media-bus-format.h>

/ {
	backlight: backlight {
		compatible = "gpio-backlight";
		gpios = <&gpio1 RK_PB0 GPIO_ACTIVE_HIGH>;
		default-brightness-level = <200>;
		status = "okay";
	};

	panel: panel {
		compatible = "simple-panel-spi";
		bus-format = <MEDIA_BUS_FMT_RGB666_1X18>;
		backlight = <&backlight>;

		/* LCD_RST (GPIO1_B1) — release ST7789V reset */
		enable-gpios = <&gpio1 RK_PB1 GPIO_ACTIVE_HIGH>;
		enable-delay-ms = <10>;
		prepare-delay-ms = <10>;
		init-delay-ms = <120>;

		/* SPI bitbang for panel init (3-wire 9-bit protocol) */
		spi-sdi-gpios = <&gpio4 RK_PA1 GPIO_ACTIVE_HIGH>;  /* SPI1_MOSI */
		spi-scl-gpios = <&gpio4 RK_PA7 GPIO_ACTIVE_HIGH>;  /* SPI1_CLK */
		spi-cs-gpios  = <&gpio4 RK_PA5 GPIO_ACTIVE_HIGH>;  /* SPI1_CS0 */

		rockchip,cmd-type = "spi";
		rockchip,output = "rgb";
		rgb-mode = "p666";

		pinctrl-names = "default";
		pinctrl-0 = <&lcd_panel_gpios>;

		/* type(0=cmd,1=data) delay_ms num_bytes byte... */
		panel-init-sequence = [
			/* Sleep Out */
			00   78  01  11
			/* MADCTL — RGB orientation */
			00   00  01  36
			01   00  01  00
			/* COLMOD — 16bit (0x55) / 18bit (0x60) … use 0x06=RGB666 */
			00   00  01  3a
			01   00  01  06
			/* RAMCTRL */
			00   00  01  b0
			01   00  01  11
			01   00  01  00
			/* PORCTRK (Porch control) */
			00   00  01  b1
			01   00  01  40
			01   00  01  04
			01   00  01  0a
			/* GCTRL (Gate control) */
			00   00  01  b2
			01   00  01  0c
			01   00  01  0c
			01   00  01  00
			01   00  01  33
			01   00  01  33
			/* VCOMS */
			00   00  01  b7
			01   00  01  35
			/* VCOMS (VCOM setting) */
			00   00  01  bb
			01   00  01  2b
			/* Power Control 1 */
			00   00  01  c0
			01   00  01  2c
			/* VDV_VRH_EN */
			00   00  01  c2
			01   00  01  01
			/* VRH (VCOM Amplitude) */
			00   00  01  c3
			01   00  01  11
			/* VCIX_VGH */
			00   00  01  c4
			01   00  01  20
			/* VCOMS (VCOM) */
			00   00  01  c6
			01   00  01  0f
			/* Power Control 2 */
			00   00  01  d0
			01   00  01  a4
			01   00  01  a1
			/* Positive Gamma */
			00   00  01  e0
			01   00  01  d0
			01   00  01  00
			01   00  01  06
			01   00  01  09
			01   00  01  0b
			01   00  01  2a
			01   00  01  3c
			01   00  01  55
			01   00  01  4b
			01   00  01  08
			01   00  01  16
			01   00  01  14
			01   00  01  19
			01   00  01  20
			/* Negative Gamma */
			00   00  01  e1
			01   00  01  d0
			01   00  01  00
			01   00  01  06
			01   00  01  09
			01   00  01  0b
			01   00  01  29
			01   00  01  36
			01   00  01  54
			01   00  01  4b
			01   00  01  0d
			01   00  01  16
			01   00  01  14
			01   00  01  21
			01   00  01  20
			/* Display On */
			00   00  01  29
		];

		display-timings {
			native-mode = <&st7789v_timing>;

			st7789v_timing: timing0 {
				clock-frequency = <8500000>;
				hactive = <240>;
				vactive = <320>;
				hfront-porch = <10>;
				hback-porch  = <10>;
				hsync-len    = <10>;
				vfront-porch = <10>;
				vback-porch  = <10>;
				vsync-len    = <10>;
				hsync-active  = <0>;
				vsync-active  = <0>;
				de-active     = <0>;
				pixelclk-active = <0>;
			};
		};

		port {
			panel_in_rgb: endpoint {
				remote-endpoint = <&rgb_out_panel>;
			};
		};
	};
};

&display_subsystem {
	status = "okay";
	route {
		route_rgb: route-rgb {
			status = "okay";
			connect = <&panel_in_rgb>;
			logo,uboot = "logo.bmp";
			logo,kernel = "logo_kernel.bmp";
		};
	};
};

&vop {
	status = "okay";
};

&rgb {
	status = "okay";
	pinctrl-names = "default";
	pinctrl-0 = <&lcd_pins>;

	ports {
		rgb_out: port@1 {
			reg = <1>;
			#address-cells = <1>;
			#size-cells = <0>;

			rgb_out_panel: endpoint@0 {
				reg = <0>;
				remote-endpoint = <&panel_in_rgb>;
			};
		};
	};
};

&pinctrl {
	lcd-panel {
		lcd_panel_gpios: lcd-panel-gpios {
			rockchip,pins =
				/* SPI_MOSI (GPIO4_A1) */
				<4 RK_PA1 RK_FUNC_GPIO &pcfg_pull_none>,
				/* SPI_CLK (GPIO4_A7) */
				<4 RK_PA7 RK_FUNC_GPIO &pcfg_pull_none>,
				/* SPI_CS  (GPIO4_A5) */
				<4 RK_PA5 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};
};

为了方便查看笔记&流程!!!

  • 先下载Typora这个markdown编写和浏览工具
  • 下载破解版!!!
  • 笔记之间跳转比较方便

笔记&流程说明

先看000开头的文件。方便了解整体。

000_开发板硬件资料.md:硬件资料

000_开发导读.md :软件编译指南

build_all() 完整编译流程

build_all()
  ├── [可选] build_recovery     ← 只有 RK_ENABLE_RECOVERY=y 才执行
  ├── build_sysdrv              ← uboot + 内核 + 根文件系统 + busybox + 驱动模块
  ├── build_media               ← Rockchip 媒体库(rockit、mpi、isp、rga 等)
  ├── build_app                 ← 用户态应用程序(rkipc 等)
  ├── build_firmware            ← 打包所有镜像
  └── finish_build

第一步:build_sysdrv — 系统驱动层

# build.sh 第 541 行
function build_sysdrv(){
    make -C ${SDK_SYSDRV_DIR}    # → sysdrv/Makefile 的 all 目标
}

sysdrv/ 的 Makefile all 目标:

all: uboot kernel rootfs env

它依次调用 4 个子步骤:

1.1 make uboot — 编译 uboot

# sysdrv/Makefile 第 82-92 行
uboot: prepare
    make -C u-boot rv1106_defconfig          # 配置 uboot
    ./u-boot/make.sh --spl-new               # 编译 uboot + SPL
    cp uboot.img  output/image/
    cp idblock.img output/image/             # 一级引导
    cp download.bin output/image/            # 下载模式固件

产出文件:

output/out/sysdrv_out/ → 最终复制到 output/image/
  ├── uboot.img          ← uboot 主镜像
  ├── idblock.img        ← 一级引导(SPL + ddr init + 安全启动)
  └── download.bin       ← 烧录模式固件(loader)

1.2 make kernel — 编译内核

# sysdrv/Makefile 第 100-140 行
kernel: prepare
    make -C kernel ARCH=arm rv1106_defconfig     # 配置内核(含 fragment 合并)
    make -C kernel ARCH=arm zImage dtbs -jN      # 编译内核本身
    make -C kernel ARCH=arm boot.img             # 打包 boot.img(含内核 + dtb + resource)
    cp boot.img output/image/
    cp vmlinux   output/bin/                     # 带调试信息的完整内核
    cp *.dtb     output/bin/

关键点: 这里用的就是 make 命令,但通过 .config + rv1106-evb.config(fragment)合并生成最终配置。它不只是简单地执行 make,还做了:

  • fragment 合并:rv1106_defconfig + rv1106-evb.config → 最终 .config
  • 编译驱动模块(make modules
  • 调用 update_dtb_bootargs.sh 修改设备树中的启动参数(root=/dev/xxx)
  • 打包 FIT 格式的 boot.img

产出文件:

output/image/
  ├── boot.img          ← FIT 格式,内含 zImage + dtb + resource
output/out/sysdrv_out/
  ├── vmlinux           ← 完整内核(带调试符号)
  ├── rv1106g-evb1-v11.dtb
  └── kernel_drv_ko/    ← 内核驱动模块 .ko 文件

1.3 make rootfs — 构建根文件系统

# 这步最复杂,依赖链:rootfs_prepare → pctools → busybox → boardtools → drv → strip
rootfs: rootfs_prepare pctools busybox boardtools drv

子步骤详情:

子步骤做的事产出
rootfs_prepare解压 rootfs 脚本模板 + 拷贝工具链运行时库(libc, libm, libpthread 等) 到 rootfs_*/基础根文件系统骨架
pctools编译 PC 端工具(mkenvimage、mk-fitimage.sh、mkfs.ubifs 等)output/out/sysdrv_out/pc/
busybox编译 busybox(busybox-1.27.2),生成 _install/bin/busybox 及所有 symlink提供 sh, ls, cp, mv 等基础命令
boardtools编译板端工具(来自 sysdrv/tools/board/一些板级小工具
drvmake modules_install → 提取所有 .ko 到 kernel_drv_ko/内核模块拷贝
strip如果是 RELEASE 模式,strip 掉调试符号缩小根文件系统体积

最后根据存储介质打包根文件系统镜像:

  • spi_nand → rootfs_ubimkfs_ubi.shrootfs_base.img(UBIFS 格式)
  • emmc → rootfs_ext4mkfs_ext4.shrootfs_base.img(ext4 格式)
  • spi_nor → rootfs_jffs2mkfs_jffs2.shrootfs_base.img(JFFS2 格式)

产出文件:

output/out/sysdrv_out/
  ├── rootfs_glibc_rv1106/     ← 根文件系统目录(未打包)
  ├── rootfs_glibc_rv1106.tar  ← 根文件系统 tar 包
  ├── pc/                      ← PC 端工具
  ├── bin/                     ← 板端工具 + 调试文件
  └── kernel_drv_ko/           ← 内核驱动模块

第二步:build_media — 媒体库

# build.sh 第 523 行
function build_media(){
    make -C ${SDK_MEDIA_DIR}    # → media/Makefile
}
# media/Makefile
all: media_libs
    make -C ./samples                    # 编译 media 示例程序
    cp -rfa ... 到 output/out/media_out/ # 拷贝全部产出

media_libs:
    # 遍历所有子目录编译:
    # rockit/     — Rockchip 多媒体处理框架
    # isp3.x/     — 图像信号处理库
    # rv1106/     — 芯片级库(mpp、rga 等)
    # 等等

产出文件(到 output/out/media_out/):

media_out/
  ├── lib/              ← 所有 .so(librockit.so, libmpi.so, libisp.so, librga.so, libmpp.so...)
  ├── include/          ← 头文件(rockit 等 API 头)
  ├── bin/              ← 示例程序
  ├── share/isp_iqfiles/ ← 各传感器 IQ 调优文件(*.bin)
  ├── usr/              ← 额外资源
  └── root/             ← 需要放到根文件系统 / 下的文件

这是给 app 层提供依赖build_app 会链接 media_out 下的 .so 和头文件。


第三步:build_app — 用户态应用程序

# build.sh 第 461 行
function build_app(){
    check_config RK_APP_TYPE || return 0         # RK_APP_TYPE 为空则跳过
    build_meta --export --media_dir ...           # 导出 meta 头文件
    make -C ${SDK_APP_DIR}                       # → project/app/Makefile
}

会被跳过的条件: RK_APP_TYPE 未设置或为空 → 不编译

# project/app/Makefile
all:
    # 遍历所有子目录(rkipc/、ipcweb/、uvc_app_tiny/ 等)
    # 每个子目录根据 RK_APP_TYPE 决定是否编译
    make -C component/rkadk/     # 条件编译
    make -C component/lvgl/      # 条件编译
    make -C rkipc/               # RK_APP_TYPE=RKIPC_RV1106 时编译
    make -C uvc_app_tiny/        # RK_APP_TYPE=UVC_TINY 时编译
    # ...
    # 最后把所有产出拷贝到 out/
    MAROC_COPY_PKG_TO_APP_OUTPUT  # → project/app/out/

产出文件(到 project/app/out/):

app/out/
  ├── bin/rkipc         ← 主程序(IP Camera 守护进程)
  ├── lib/              ← .so(librkfsmk.so, libwpa_client.so 以及第三方库)
  └── share/            ← 配置文件(*.ini)、字体、测试音频

第四步:build_firmware — 打包固件镜像

# build.sh 第 1943 行
function build_firmware(){
    build_env                    # 生成 env.img(uboot 环境变量分区)
    build_meta                   # 生成 meta 分区(摄像头参数、IQ 文件等)

    __PACKAGE_ROOTFS             # 解压 rootfs.tar → 合并 app_out/media_out 内容
    __PACKAGE_OEM                # 打包 OEM 分区(/oem,放 app 和 media 的 bin/lib/share)
    __PACKAGE_USERDATA           # 打包空 userdata 分区

    build_mkimg rootfs ...       # 制作 rootfs 分区镜像(UBIFS/ext4/JFFS2)
    build_mkimg oem ...          # 制作 OEM 分区镜像
    build_mkimg userdata ...     # 制作 userdata 分区镜像

    build_updateimg              # 打包 update.img(统一烧录镜像)
}

最终固件产出(到 output/image/):

output/image/
  ├── uboot.img          ← uboot 镜像
  ├── idblock.img        ← 一级引导
  ├── download.bin       ← 烧录 loader
  ├── boot.img           ← 内核 + dtb + resource
  ├── rootfs.img         ← 根文件系统(UBIFS/ext4/JFFS2)
  ├── oem.img            ← OEM 分区(app + media 的 bin/lib/share + IQ 文件)
  ├── userdata.img       ← 空 userdata 分区
  ├── env.img            ← uboot 环境变量(分区表、启动参数)
  ├── misc.img           ← misc 分区(恢复模式标记)
  └── update.img         ← 统一烧录包(包含以上所有)

编译流程全局图

                 .BoardConfig.mk  ← 您在这里配置芯片、分区、APP_TYPE 等
                        │
                  build.sh all
                        │
         ┌──────────────┼──────────────┐
         │              │              │
    build_sysdrv   build_media    build_app
         │              │              │
    ┌────┼────┐    media/ 下     app/ 下各子目录
    │    │    │    各库源码      根据 RK_APP_TYPE 选择编译
   uboot kernel rootfs
    │    │    │         │              │
    │    │    │    media_out/     app/out/
    │    │    │    (.so + .h)    (rkipc + .so + .ini)
    │    │    │         │              │
    └────┼────┼─────────┼──────────────┘
         │    │         │              │
         │    │    build_firmware      │
         │    │    __PACKAGE_ROOTFS ◄──┘
         │    │    __PACKAGE_OEM    ◄──┘(合并到 oem 分区)
         │    │         │
         │    │    output/image/
         │    │    ├── boot.img
         │    │    ├── rootfs.img
         │    │    ├── oem.img
         │    │    └── update.img
         │    │
    ┌────┘    └──────────────────────────┐
    │                                    │
  uboot 阶段编译              kernel 阶段编译
  只是 make + 脚本拷贝       不只是 make:
                              - fragment 合并配置
                              - 修改 dtb 启动参数
                              - 打包 FIT boot.img
                              - 编译驱动模块

数据引脚

引脚序号信号名称备注说明
117MIPI_CSI_RX_D3NMIPI CSI 差分数据 3 负端
118MIPI_CSI_RX_D3PMIPI CSI 差分数据 3 正端
119MIPI_CSI_RX_CK1NMIPI CSI 差分时钟 1 负端
120MIPI_CSI_RX_CK1PMIPI CSI 差分时钟 1 正端
121MIPI_CSI_RX_D2NMIPI CSI 差分数据 2 负端
122MIPI_CSI_RX_D2PMIPI CSI 差分数据 2 正端
PHY 组配套高速时钟可用数据 Lane用途
PHY0CK0P/CK0ND0P/D0N、D1P/D1N摄像头 1(2Lane MIPI)
PHY1CK1P/CK1ND2P/D2N、D3P/D3N摄像头 2(2Lane MIPI,当前接线)

其他引脚

引脚序号信号名称备注说明复用关系
5I2C4_SCL_M2_1V8I2C时钟VI_CIF_D11/UART5_TX_M2/I2C4_SCL_M2/GPIO3_C7_d
7I2C4_SDA_M2_1V8I2C数据引脚VI_CIF_D12/UART5_RX_M2**/I2C4_SDA_M2**/GPIO3_D0_d
2MIPI_CLK0_OUT第0路摄像头时钟线24MHzVI_CIF_CLKO_M0/MIPI_CLK0_OUT/GPIO3_C4_d
3MIPI_RSTVI_CIF_VSYNC_M0/GPIO3_C5_d
4MIPI_PWDNSensor 电源使能 / 待机开关,唤醒芯片输出图像VI_CIF_D10/PWM7_IR_M2/MIPI_CLK1_OUT/GPIO3_C6_d

RV1106 引脚复用功能表

引脚号引脚复用功能
1MIPI_AVDD1V8/GPIO7_VCC1V8
2VI_CIF_CLKO_M0/MIPI_CLK0_OUT/GPIO3_C4_d
3VI_CIF_VSYNC_M0/GPIO3_C5_d
4VI_CIF_D10/PWM7_IR_M2/MIPI_CLK1_OUT/GPIO3_C6_d
5VI_CIF_D11/UART5_TX_M2/I2C4_SCL_M2/GPIO3_C7_d
6VI_CIF_D13/UART5_RTS_M2/I2C3_SCL_M2/GPIO3_D1_d
7VI_CIF_D12/UART5_RX_M2/I2C4_SDA_M2/GPIO3_D0_d
8VI_CIF_D14/UART5_CTS_M2/I2C3_SDA_M2/GPIO3_D2_d
9VI_CIF_D15/PWM1_M2/GPIO3_D3_d
10DVDD_1
11SDMMC0_DET/GPIO3_A1_u
12SDMMC0_D1/UART2_TX_M0/PWM9_M0/GPIO3_A2_u
13GPIO4_VCC
14SDMMC0_D0/UART2_RX_M0/PWM8_M0/GPIO3_A3_u
15SDMMC0_CLK/UART5_RTS_M0/I2C0_SCL_M2/JTAG_LPMCU_TCK_M1/PWM10_M0/GPIO3_A4_d
16SDMMC0_CMD/UART5_CTS_M0/I2C0_SDA_M2/JTAG_LPMCU_TMS_M1/PWM11_IR_M0/GPIO3_A5_u
17SDMMC0_D3/UART5_TX_M0/JTAG_CPU_TMS_M0/JTAG_HPMCU_TMS_M1/GPIO3_A6_u
18SDMMC0_D2/UART5_RX_M0/JTAG_CPU_TCK_M0/JTAG_HPMCU_TCK_M1/GPIO3_A7_u
19RTC_AVDD3V3
20RTC_XOUT
21RTC_XIN
22SARADC_IN1/PWM1_M1/GPIO4_C1_z
23SARADC_IN0/GPIO4_C0_z
24SARADC_USB_AVDD1V8
25USB_VBUSDET
26USB_DM
27USB_DP
28USB_AVDD3V3
29CODEC_LINEOUT
30CODEC_VCM
31CODEC_AVDD1V8
32CODEC_MICBIAS
33CODEC_MIC0N
34CODEC_MIC0P
35CODEC_MICIN
36CODEC_MIC1P
37CODEC_AVSS
38EMMC_D5/SPI1_CLK_M0/UART1_RX_M2/I2C2_SCL_M1/GPIO4_A7_u
39EMMC_D3/FSPI_D3/GPIO4_A6_u
40EMMC_D4/SPI1_CS0_M0/UART1_TX_M2/I2C2_SDA_M1/GPIO4_A5_u
41EMMC_D0/FSPI_D0/GPIO4_A4_u
42EMMC_D1/FSPI_D1/GPIO4_A3_u
43GPIO3_VCC
44EMMC_D2/FSPI_D2/GPIO4_A2_u
45EMMC_D6/SPI1_MOSI_M0/UART0_TX_M2/I2C0_SCL_M1/GPIO4_A1_u
46EMMC_D7/SPI1_MISO_M0/UART0_RX_M2/I2C0_SDA_M1/GPIO4_A0_u
47EMMC_CMD/FSPI_CS0/GPIO4_B0_u
48EMMC_CLK/FSPI_CLK/GPIO4_B1_d
49DDR_VDDQ_1
50DDR_VDDQ_2
51DVDD_2
52DRAM_ZQ
53DDR_PLL_AVDD1V8
54DVDD_3
55DVDD_4
56DDR_VDDQ_3
57TVSS
58UART0_RX_M0/CLK_32K/CLK_REFOUT/RTC_CLKO/GPIO0_A0_z
59UART0_TX_M0/PWM2_M0/GPIO0_A1_d
60PWM3_IR_M0/GPIO0_A2_d
61PMU_VCC3V3
62PMIC_PWR_CTRL_M1/GPIO0_A3_u
63PMIC_PWR_CTRL_M0/PWM1_M0/GPIO0_A4_d
64I2C1_SCL_M0/UART1_RTS_M0/PWM5_M0/GPIO0_A5_d
65I2C1_SDA_M0/UART1_CTS_M0/PWM6_M0/GPIO0_A6_d
66nPOR
67PMU_DVDD0V9
68OSC_XIN
69OSC_XOUT
70OSC_AVDD1V8/PLL_AVDD1V8
71OSC_PLL_DVDD
72UART3_TX_M0/I2C2_SCL_M0/PWM7_IR_M0/GPIO1_A0_d
73UART3_RX_M0/I2C2_SDA_M0/PWM4_M0/GPIO1_A1_d
74PWM0_M0/CPU_AVS/VI_CIF_D0_M1/GPIO1_A2_d
75UART1_TX_M0/I2C0_SCL_M0/GPIO1_A3_d
76UART1_RX_M0/I2C0_SDA_M0/GPIO1_A4_d
77UART4_RX_M0/PWM3_IR_M1/GPIO1_B0_d
78UART4_TX_M0/PWM7_IR_M1/SPI1_CS1_M0/VI_CIF_D1_M1/GPIO1_B1_d
79JTAG_CPU_TCK_M1/UART2_TX_M1/JTAG_HPMCU_TCK_M0/JTAG_LPMCU_TCK_M0/GPIO1_B2_d
80JTAG_CPU_TMS_M1/UART2_RX_M1/JTAG_HPMCU_TMS_M0/JTAG_LPMCU_TMS_M0/GPIO1_B3_u
81GPIO1_VCC3V3
82DVDD_5
83VO_LCDC_D1/VI_CIF_D8_M1/PWM10_M1/UART4_RTS_M1/GPIO1_C6_d
84VO_LCDC_D0/VI_CIF_D9_M1/PWM11_IR_M1/UART4_CTS_M1/GPIO1_C7_d
85VO_LCDC_CLK/VI_CIF_CLKO_M1/I2C3_SCL_M1/UART5_TX_M1/PWM11_IR_M2/AUD_DSM_N/GPIO1_D3_d
86VO_LCDC_VSYNC/VI_CIF_VSYNC_M1/I2C3_SDA_M1/UART5_RX_M1/SPI0_CS1_M0/PWM0_M1/AUD_DSM_P/GPIO1_D2_d
87VO_LCDC_HSYNC/VI_CIF_HREF_M1/PWM10_M2/UART5_CTS_M1/UART3_RX_M1/GPIO1_D1_d
88GPIO6_VCC
89VO_LCDC_DEN/VI_CIF_CLKI_M1/PWM3_IR_M2/UART5_RTS_M1/UART3_TX_M1/GPIO1_D0_d
90VO_LCDC_D2/VI_CIF_D7_M1/PWM9_M1/UART4_TX_M1/SDMMC1_D2_M1/GPIO1_C5_d
91VO_LCDC_D3/VI_CIF_D6_M1/PWM8_M1/UART4_RX_M1/SDMMC1_D3_M1/GPIO1_C4_d
92VO_LCDC_D4/VI_CIF_D5_M1/PWM6_M2/I2C4_SDA_M1/SDMMC1_CMD_M1/SPI0_MISO_M0/GPIO1_C3_d
93VO_LCDC_D5/VI_CIF_D4_M1/PWM5_M2/I2C4_SCL_M1/SDMMC1_CLK_M1/SPI0_MOSI_M0/GPIO1_C2_d
94VO_LCDC_D6/VI_CIF_D3_M1/PWM4_M2/SPI0_CLK_M0/SDMMC1_D0_M1/GPIO1_C1_d
95VO_LCDC_D7/VI_CIF_D2_M1/PWM2_M2/SPI0_CS0_M0/SDMMC1_D1_M1/GPIO1_C0_d
96OTP_AVDD1V8/ETH_AVDD1V8/TSADC_AVDD1V8
97ETH_PHY_RXN
98ETH_PHY_RXP
99ETH_PHY_TXN
100ETH_PHY_TXP
101ETH_AVDD3V3
102ETH_EXTR
103DVDD_6
104UART0_TX_M1/I2C1_SDA_M1/VO_LCDC_D17/PWM6_M1/GPIO2_B1_d
105UART0_RX_M1/I2C1_SCL_M1/VO_LCDC_D16/PWM5_M1/GPIO2_B0
106UART0_CTS_M1/I2S0_SDO1_SDI3/VO_LCDC_D15/PWM4_M1/I2C3_SDA_M0/PRELIGHT_TRIG_OUT/GPIO2_A7_d
107UART0_RTS_M1/I2S0_SDO2_SDI2/VO_LCDC_D14/PWM2_M1/I2C3_SCL_M0/FLASH_TRIG_OUT/GPIO2_A6_d
108GPIO5_VCC
109SDMMC1_D1_M0/I2S0_SCLK/VO_LCDC_D8/UART1_CTS_M1/I2C4_SDA_M0/GPIO2_A0_d
110SDMMC1_D0/I2S0_LRCK/VO_LCDC_D9/UART1_RTS_M1/I2C4_SCL_M0/GPIO2_A1
111SDMMC1_CLK_M0/I2S0_MCLK/VO_LCDC_D10/GPIO2_A2_d
112SDMMC1_CMD_M0/I2S0_SDO3_SDI1/VO_LCDC_D11/GPIO2_A3_d
113SDMMC1_D3_M0/I2S0_SDO0/VO_LCDC_D12/UART1_TX_M1/GPIO2_A4_d
114SDMMC1_D2_M0/I2S0_SDI0/VO_LCDC_D13/UART1_RX_M1/GPIO2_A5_d
115CPU_DVDD
116DVDD_7
117VI_CIF_D0_M0/MIPI_CSI_RX_D3N/LVDS_RX_D3N/GPIO3_B0_d
118VI_CIF_D1_M0/MIPI_CSI_RX_D3P/LVDS_RX_D3P/GPIO3_B1_d
119VI_CIF_D2_M0/MIPI_CSI_RX_CK1N/LVDS_RX_CK1N/GPIO3_B2_d
120VI_CIF_D3_M0/MIPI_CSI_RX_CK1P/LVDS_RX_CK1P/GPIO3_B3_d
121VI_CIF_D4_M0/MIPI_CSI_RX_D2N/LVDS_RX_D2N/GPIO3_B4_d
122VI_CIF_D5_M0/MIPI_CSI_RX_D2P/LVDS_RX_D2P/GPIO3_B5_d
123VI_CIF_D6_M0/MIPI_CSI_RX_D1N/LVDS_RX_D1N/GPIO3_B6_d
124VI_CIF_D7_M0/MIPI_CSI_RX_D1P/LVDS_RX_D1P/GPIO3_B7_d
125VI_CIF_D8_M0/MIPI_CSI_RX_CK0N/LVDS_RX_CK0N/GPIO3_C0_d
126VI_CIF_D9_M0/MIPI_CSI_RX_CK0P/LVDS_RX_CK0P/GPIO3_C1_d
127VI_CIF_CLKI_M0/MIPI_CSI_RX_D0N/LVDS_RX_D0N/GPIO3_C2_d
128VI_CIF_HREF_M0/MIPI_CSI_RX_D0P/LVDS_RX_D0P/GPIO3_C3_d
E-PADVSS
引脚序号信号名称
11SDMMSC0_DET
14SDMMSC0_D0
12SDMMSC0_D1
18SDMMSC0_D2
17SDMMSC0_D3
15SDMMSC0_CLK
16SDMMSC0_CMD0p

相关芯片

功能IC型号特点
充电管理SLM6300
电量计CW2015CHBD
电源管理EA3036CQBR

充电检测

  • 引脚
名称引脚号复用关系
NCHRG123VI_CIF_D6_M0/MIPI_CSI_RX_D1N/LVDS_RX_D1N/GPIO3_B6_d
NSTDBY124VI_CIF_D7_M0/MIPI_CSI_RX_D1P/LVDS_RX_D1P/GPIO3_B7_d
  • 电平情况
引脚/状态充电充满移除
NCHRGLHH
NSTDBYHLH

充电检测2

说明:通过判断usb插入,来去确定是否在充电

  • 引脚

    名称引脚号复用关系
    USB_VBUSDET/USB_DET25USB_VBUSDET/GPIO1_D0
  • 默认0,插入之后1.8v

电量查看

名称引脚号复用关系
I2C数据线 SCL75UART1_TX_M0/I2C0_SCL_M0/GPIO1_A3_d
I2C时钟线 SDA76UART1_RX_M0/I2C0_SDA_M0/GPIO1_A4_d
模块名称引脚序号引脚名
USB切换58GPIO0_A0
SD卡使能59GPIO0_A1
电源控制60POW_HOLD/GPIO0_A2
电源开关检测按钮62SOC_GPIO_BELL/GPIO0_A3
红外摄像头电源开关63GPIO0_A4
照明灯光74PWM0_M0
LCD背光77LCD_BL_EN

电源控制:SOC手动拉高,外围电路就能持续供电,详细看下图

电源开关检测按钮:kernel启动后就会拉高电源控制引脚持续给soc供电。

8

ST7789V屏幕

数字是SOC的引脚号,字符是LCD的引脚名称。引脚复用关系

{
  "38": "SPI1_CLK",
  "40": "SPI1_CS0",
  "45": "SPI1_MOSI",
  "77": "LCD_BL_EN",
  "78": "LCD_RST",
  "83": "LCD_D1",
  "84": "LCD_D0",
  "85": "LCD_CLK",
  "86": "LCD_VSYNC",
  "87": "LCD_HSYNC",
  "89": "LCD_DEN",
  "90": "LCD_D2",
  "91": "LCD_D3",
  "92": "LCD_D4",
  "93": "LCD_D5",
  "94": "LCD_D6",
  "95": "LCD_D7",
  "104": "LCD_D17",
  "105": "LCD_D16",
  "106": "LCD_D15",
  "107": "LCD_D14",
  "109": "LCD_D8",
  "110": "LCD_D9",
  "111": "LCD_D10",
  "112": "LCD_D11",
  "113": "LCD_D12",
  "114": "LCD_D13"
}

SOC

  • rv1106G3
对比维度🟢 RV1106 G2🔵 RV1106 G3
AI算力0.5 TOPS1.0 TOPS
NPU精度支持 INT4/INT8/INT16 混合运算支持 INT4/INT8/INT16 混合运算
内存大小128MB DDR3L(部分版本可达256MB)256MB DDR3L
无线连接常见版本不集成,需外接无线模块常见版本集成 2.4GHz WiFi6 和 Bluetooth 5.2/BLE

FLASH(nand)

  • C19193269_NAND,具体型号:GD5F1GM7UEYIGR

  • 1Gb SLC NAND Flash (128M)

  • 页大小 2048(针对文件系统修改有需求)

详细可前往GD5F1GM7UEYIGR

屏幕

  • 320x240分辨率
  • spi+18并口
  • 40p引脚

详细文档前往ST7789V+CTC2.8_3SPI_RGB18BIT

红外摄像头

可见光摄像头

SDK build.sh使用说明

选择参考的板级配置

./build.sh lunch

You're building on Linux
Lunch menu...pick a combo:

BoardConfig-*.mk naming rules:
BoardConfig-"启动介质"-"电源方案"-"硬件版本"-"应用场景".mk
BoardConfig-"boot medium"-"power solution"-"hardware version"-"applicaton".mk

----------------------------------------------------------------
0. BoardConfig-EMMC-ALL-2xRK806-HW_V10-IPC_MULTI_SENSOR.mk
                             boot medium(启动介质): EMMC
                          power solution(电源方案): 2xRK806
                        hardware version(硬件版本): HW_V10
                              applicaton(应用场景): IPC_MULTI_SENSOR
----------------------------------------------------------------

----------------------------------------------------------------
1. BoardConfig-SPI_NAND-ALL-RK806-HW_V10-IPC_SINGLE_SENSOR.mk
                             boot medium(启动介质): SPI_NAND
                          power solution(电源方案): RK806
                        hardware version(硬件版本): HW_V10
                              applicaton(应用场景): IPC_SINGLE_SENSOR
----------------------------------------------------------------

Which would you like? [0]:

输入对应的序号选择对应的参考板级。

一键自动编译

./build.sh lunch # 选择参考板级 ./build.sh # 一键自动编译

编译U-Boot

./build.sh clean uboot ./build.sh uboot

生成镜像文件: output/image/MiniLoaderAll.bin output/image/uboot.img

编译kernel

./build.sh clean kernel ./build.sh kernel

生成镜像文件: output/image/boot.img

编译rootfs

./build.sh clean rootfs ./build.sh rootfs

编译后使用./build.sh firmware命令打包成rootfs.img 生成镜像文件:output/image/rootfs.img

编译media

./build.sh clean media ./build.sh media

生成文件的存放目录: output/out/media_out

编译参考应用

./build.sh clean app ./build.sh app

生成文件的存放目录: output/out/app_out 注:app依赖media

固件打包

./build.sh firmware

生成文件的存放目录: output/image

SDK目录结构说明:

├── build.sh -> project/build.sh ---- SDK编译脚本
├── media --------------------------- 多媒体编解码、ISP等算法相关(可独立SDK编译)
├── sysdrv -------------------------- U-Boot、kernel、rootfs目录(可独立SDK编译)
├── project ------------------------- 参考应用、编译配置以及脚本目录
├── output -------------------------- SDK编译后镜像文件存放目录
├── docs ---------------------------- SDK文档目录
└── tools --------------------------- 烧录镜像打包工具以及烧录工具

镜像存放目录说明

编译完的文件存放在output目录下

output/
├── image
│   ├── download.bin ---------------- 烧录工具升级通讯的设备端程序,只会下载到板子内存
│   ├── env.img --------------------- 包含分区表和启动参数
│   ├── uboot.img ------------------- uboot镜像
│   ├── idblock.img ----------------- loader镜像
│   ├── boot.img -------------------- kernel镜像
│   ├── rootfs.img ------------------ kernel镜像
│   └── userdata.img ---------------- userdata镜像
└── out
    ├── app_out --------------------- 参考应用编译后的文件
    ├── media_out ------------------- media相关编译后的文件
    ├── rootfs_xxx ------------------ 文件系统打包目录
    ├── S20linkmount ---------------- 分区挂载脚本
    ├── sysdrv_out ------------------ sysdrv编译后的文件
    └── userdata -------------------- userdata

注意事项

在windows下复制源码包时,linux下的可执行文件可能变为非可执行文件,或者软连接失效导致无法编译使用。
因此使用时请注意不要在windows下复制源代码包。

Stc8g1k08a芯片io扩展

STC8G1K08A

stc8引脚

引脚使用说明:

  • P3.0、P3.1,烧录引脚,设计为端子形式:P3.0、P3.1、VCC、GND,引出为端子。烧录过程连接端子。烧录完成之后连接通信模块,作为数据上报。同时可以作为后续的调试引脚,正式投产使用之后,可通过这个串口进行日志查看等。
  • P3.2、P3.3,作为iic引脚。设置成2p端子形式。与PCA9555芯片做通信。
  • P5.4,作为PCA9555的中断(低电平有效)触发引脚。当PCA9555读到的电平状态发生变化时触发这个中断进行数据上报。
  • P5.5,预留引脚。可以作为led提示灯等功能。

PCA9555

PCA9555引脚

  • **A0-A2:**全部拉低。此时通讯读写地址为:0x40 (write),0x41(read)
  • **INT:**中断引脚,当P00-P17状态发生改变触发。与stc8g1k08a的P54引脚相连。
  • **SDL、SDA:**通讯引脚。与stc8g1k08a的P32、P33引脚相连。
  • **P00-P07、P10-P17:**与微动开关相连。用于读取微动开关状态。每4个作为一组,使用端子引出来。p00-p03、p04-p07、p10-p13、p14-p17

STC8G1K08A

​ **说明:**1.9V~5.5V、温度范围:-40°C ~ +85°C。

​ **引脚:**8个引脚。VCC、GND、P3.0、P3.1、P3.2、P3.3、P5.4、P5.5

价格:

​ 立创商城千份单价0.7464元。

stc8g芯片立创商城价格

​ 淘宝价格千份单价0.62065元。

stc8g芯片淘宝价格

PCA9555

​ **说明:**2.3V~5.5V、温度范围:-40°C ~ +85°C。

​ **引脚:**24引脚。P00-P7、P10-P17、INT、SCL、SDA、A0、A1、A2。可检测16路

价格:

​ 使用国产芯片立创商城价格千份1.27元。

pca9555立创商城价格

通信模块

​ 4g模块还没确定。上面两个模块完成搭建后,数据上传只需要通过串口进行上传,既通信模块可以使用4g模块、wifi模块、蓝牙、lora等。

模块

蓝牙模块

蓝牙类型标志特性
经典蓝牙SPP,蓝牙3.0及以下功耗高,速率较快,先配对后连接
低功耗蓝牙BLE,蓝牙4.0及以上功耗低,速率较慢,快速连接,有组网,广播,定位等新功能
双模蓝牙SPP+BLE同时集成经典蓝牙和低功耗蓝牙,兼容性强
主从类别特性
单从机仅能被动等待设备连接,无法主动发起连接请求
主从一体可以切换为主设备和从设备,主设备时可以发起连接请求
  • 测温头固定4字节,解析方式:4字节转成float;

  • 2字节全屏测温数据:2字节全屏测温数据/64 – 50;

  • YUV流:按照YUYV格式解析;

/* 温度换算: ℃ = raw / 64 - 50 */
#define TEMP_RAW_TO_C(raw)  ((raw) / 64.0f - 50.0f)

即得到的:帧温度原始值 除以 64.0f - 50.0f

官方SDK提供的demo是C++的示例

x86_64的linux

  • 使用官方c++示例

    • 修改demo/linux下makefile名字

    • 执行make clean

    • 执行make

  • 使用c自己写

    • 删除无用文件,libusb-1.0.so 才是真正使用的,libusb-1.0.so.0,libusb-1.0.2.0用不到也能删

      下载
    • 修改include/HCUsbSDK.h

      #if defined _WIN32 || defined _WIN64
      #define USB_SDK_API  extern "C" __declspec(dllimport)
      #elif defined __linux__ || defined __APPLE__
      #define USB_SDK_API  extern "C"
      #endif
      

      改成

      #if defined _WIN32 || defined _WIN64
      #define USB_SDK_API  extern "C" __declspec(dllimport)
      #elif defined __linux__ || defined __APPLE__
        #ifdef __cplusplus
          #define USB_SDK_API  extern "C"
        #else
          #define USB_SDK_API //extern "C"  是c++的语法,c不支持
        #endif
      #endif
      
    • 编写makefile

      CC = gcc
      CFLAGS = -g -O0 -Wall
      INCLUDE = -I. -I../../include
      LDLIBS = -lHCUSBSDK -lpthread
      LIBPATH = ../../library/linux64 
      
      SRCS = $(wildcard *.c)
      OBJS = $(SRCS:.c=.o)
      TARGET = usb_demo
      
      $(TARGET): $(OBJS)
      	$(CC) $(CFLAGS) $^ -o $@ -L$(LIBPATH) $(LDLIBS) -Wl,-rpath=$(LIBPATH)
      	cp $@ $(LIBPATH)
      
      %.o: %.c
      	$(CC) $(CFLAGS) $(INCLUDE) -c $< -o $@
      
      all: $(TARGET)
      	@echo "Build $(TARGET) Finished"
      
      clean:
      	rm -f $(OBJS) $(TARGET) $(LIBPATH)/$(TARGET)
      
    • 编写测试

      #include <stdio.h>
      #include "../../include/HCUsbSDK.h"
      int main(void)
      {
          USB_Init();
          
          printf("SDK_version=%d\n",USB_GetSDKVersion());
          printf("DeviceCount=%d\n", USB_GetDeviceCount());
          return 0;
      }
      
    • 注意点

      1. 编写的应用程序必须在.so依赖同级目录。

        library/linux64/
        ├── usb_demo              ← 程序
        ├── libHCUSBSDK.so        ← SDK
        ├── libhpr.so
        ├── libusb-1.0.so         ← 被  libHCUSBSDK.so dlopen
        └── libuvc.so             ← 被  libHCUSBSDK.so dlopen
        
  • 其他示例

    前往其他地方

  • 设备初始化

    #include "../../include/HCUsbSDK.h"
    if (!USB_Init()) { printf("USB_Init failed\n"); return -1; }
    
  • 获取已连接的设备数量

    int count = USB_GetDeviceCount();
    printf("DeviceCount=%d\n", count);
    if (count <= 0) 
    {     
        USB_Cleanup();
    	return -1;
    }
    
  • 遍历设备

    USB_DEVICE_INFO *devices = malloc(sizeof(USB_DEVICE_INFO) * count);
    if (!USB_EnumDevices(count, devices))
    {
        free(devices);
        USB_Cleanup();
        return -1;
    }
    printf("Device: type=%d name=%s\n",devices[0].byProtocolType,devices[0].szDeviceName);
    
  • 登录设备0

    /* ========== 登录设备 ========== */
    USB_USER_LOGIN_INFO li = {0};
    USB_DEVICE_REG_RES  lr = {0};
    li.dwSize = sizeof(li);
    li.dwTimeout = 5000;
    li.dwVID = devices[0].dwVID;
    li.dwPID = devices[0].dwPID;
    memcpy(li.szSerialNumber, devices[0].szSerialNumber, MAX_SERIAL_NUMBER_LEN);
    memcpy(li.szUserName, "admin", 5);
    memcpy(li.szPassword, "12345", 5);
    li.byLoginMode = 1;
    lr.dwSize = sizeof(lr);
    
    LONG userId = USB_Login(&li, &lr);
    if (userId < 0)
    {
        printf("Login failed, err=%d\n", USB_GetLastError());
        free(devices);
        USB_Cleanup();
        return -1;
    }
    

整体代码

#include "../../include/HCUsbSDK.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
    if (!USB_Init())
    {
        printf("USB_Init failed\n");
        return -1;
    }

    int count = USB_GetDeviceCount();
    printf("DeviceCount=%d\n", count);
    if (count <= 0)
    {
        USB_Cleanup();
        return -1;
    }

    USB_DEVICE_INFO *devices = malloc(sizeof(USB_DEVICE_INFO) * count);
    if (!USB_EnumDevices(count, devices))
    {
        free(devices);
        USB_Cleanup();
        return -1;
    }
    printf("Device: type=%d name=%s\n", devices[0].byProtocolType, devices[0].szDeviceName);

    /* ========== 登录设备 ========== */
    USB_USER_LOGIN_INFO li = {0};
    USB_DEVICE_REG_RES  lr = {0};
    li.dwSize = sizeof(li);
    li.dwTimeout = 5000;
    li.dwVID = devices[0].dwVID;
    li.dwPID = devices[0].dwPID;
    memcpy(li.szSerialNumber, devices[0].szSerialNumber, MAX_SERIAL_NUMBER_LEN);
    memcpy(li.szUserName, "admin", 5);
    memcpy(li.szPassword, "12345", 5);
    li.byLoginMode = 1;
    lr.dwSize = sizeof(lr);

    LONG userId = USB_Login(&li, &lr);
    if (userId < 0)
    {
        printf("Login failed, err=%d\n", USB_GetLastError());
        free(devices);
        USB_Cleanup();
        return -1;
    }
    printf("Login OK, UserID=%ld\n", (long)userId);
    printf("  DeviceName=%s\n", lr.szDeviceName);
    printf("  SerialNumber=%s\n", lr.szSerialNumber);
    printf("  SoftwareVersion=%d.%d\n",
           lr.dwSoftwareVersion >> 16, lr.dwSoftwareVersion & 0xFFFF);
    printf("  RetryLoginTimes=%d\n", lr.byRetryLoginTimes);
    printf("  SurplusLockTime=%u\n", lr.dwSurplusLockTime);

    /* ========== 登出并清理 ========== */
    USB_Logout(userId);
    USB_Cleanup();
    free(devices);
    printf("Done.\n");
    return 0;
}
typedef struct tagUSB_COMMON_COND
{
    DWORD dwSize;
    BYTE  byChannelID;     // 通道号
    BYTE  bySID;           // 场景ID
    BYTE  byRes[6];
} USB_COMMON_COND, *LPUSB_COMMON_COND;

操作的是哪个通道、哪个场景,以及这个条件结构体本身有多大

  • dwSize
    • 结构体大小,给 SDK 做参数校验和版本兼容。
  • byChannelID
    • 指定“对哪个通道操作”。
    • 设成 USB_CHANNEL_IR ,表示红外通道。
  • bySID
    • 场景 ID。
    • 某些功能可能按场景区分配置,不一定每个接口都用得到。
  • byRes[6]
    • 预留字节,通常清零,不用你管。

USB_THERMAL_STREAM_PARAM

  • USB_CONFIG_INPUT_INFO 下发的码流参数
  • USB_CONFIG_OUTPUT_INFO 用来接收查询结果

查询

/* ---- 能力查询 ---- */
void print_capabilities(void)
{
    USB_COMMON_COND cond = {0};
    cond.dwSize = sizeof(cond);
    cond.byChannelID = USB_CHANNEL_IR; //配置红外通道

    /* 查询当前热成像码流参数 */
    USB_THERMAL_STREAM_PARAM tsp = {0};
    tsp.dwSize = sizeof(tsp);

    USB_CONFIG_INPUT_INFO in = {0};
    USB_CONFIG_OUTPUT_INFO out = {0};
    in.lpCondBuffer = &cond;
    in.dwCondBufferSize = sizeof(cond);
    out.lpOutBuffer = &tsp;
    out.dwOutBufferSize = sizeof(tsp);

	if (USB_GetDeviceConfig(g_userId, USB_GET_THERMAL_STREAM_PARAM_CAPABILITIES, &in, &out))
    {
        printf("  cap.dwVideoCodingType=%u (0x%08x)\n",
               (unsigned)cap.dwVideoCodingType, (unsigned)cap.dwVideoCodingType);
        static const char *desc[] = {
            "热成像裸数据", "全屏测温数据", "实时裸数据",
            "热图数据", "热成像实时流", "YUV实时数据",
            "PS封装MJPEG", "全屏测温+YUV实时流", "YUV+裸数据", "YUV+裸数据","仅YUV不含测温头","测温头 + 离线测温信息 + 裸数据 + YUV"
        };
        for (int i = 0; i < 10+1; i++)
            printf("    bit%d=%d %s\n", i,
                   (cap.dwVideoCodingType >> i) & 1, desc[i]);
    }
}
  • 初始化和登录

    可以借鉴:002_usb红外摄像头初始化登录.md

  • 查看摄像头的能力

    可以借鉴:003_usb红外摄系统能力查询.md

  • 配置参数

    int configure_device(void)
    {
        USB_COMMON_COND cond = {0};
        cond.dwSize = sizeof(cond);
        cond.byChannelID = USB_CHANNEL_IR;
    
        USB_THERMAL_STREAM_PARAM tsp = {0};
        tsp.dwSize = sizeof(tsp);
        tsp.byVideoCodingType = 8; //
        tsp.dwWidth = 256; tsp.dwHeight = 192; tsp.dwFrameRate = 25;
    
        USB_CONFIG_INPUT_INFO in = {0};
        USB_CONFIG_OUTPUT_INFO out = {0};
        in.lpCondBuffer = &cond; in.dwCondBufferSize = sizeof(cond);
        in.lpInBuffer = &tsp; in.dwInBufferSize = sizeof(tsp);
        if (!USB_SetDeviceConfig(g_userId, USB_SET_THERMAL_STREAM_PARAM, &in, &out)) return -1;
    
        USB_VIDEO_PARAM vp = {0};
        vp.dwVideoFormat = USB_STREAM_YUY2;
        vp.dwWidth = 256; vp.dwHeight = 192; vp.dwFramerate = 25;
        in.lpInBuffer = &vp; in.dwInBufferSize = sizeof(vp);
        if (!USB_SetDeviceConfig(g_userId, USB_SET_VIDEO_PARAM, &in, &out)) return -1;
        printf("Config: type=8 YUY2 256x192@25\n");
        return 0;
    }
    
    • 参数解析
  • 开启流&设置流回调

    static void __stdcall cb(LONG h, USB_FRAME_INFO *f, void *u)
    {
        (void)h; (void)u;
        if (!f || !f->pBuf || f->dwStreamType != USB_STREAM_YUY2) return;
        g_frameCnt++;
    
        if (g_frameCnt == 1)
        {
            printf("[CB#1] %lux%lu buf=%lu\n",
                   (unsigned long)f->dwWidth, (unsigned long)f->dwHeight,
                   (unsigned long)f->dwBufSize);
            printf("  raw: ");
            for (int i = 0; i < f->dwBufSize; i++) printf("%02x ", f->pBuf[i]);
            printf("\n");
        }
    
        if (g_frameCnt <= 3 || (g_frameCnt % 25) == 0)
            printf("[CB#%d] frame\n", g_frameCnt);
    }
    
    int start_stream(int seconds)
    {
        USB_STREAM_CALLBACK_PARAM cp = {0};
        cp.dwSize = sizeof(cp);
        cp.dwStreamType = USB_STREAM_YUY2;
        cp.funcStreamCallBack = cb;
    
        LONG h = USB_StartStreamCallback(g_userId, &cp);
        if (h < 0) return -1;
    
        sleep(seconds);
        USB_StopChannel(g_userId, (DWORD)h);
        return 0;
    }
    

海康有很多种摄像头

  • usb摄像头
  • 网络摄像头
  • ...

共用一套SDK。

usb红外摄像头请看

  • 下载对应的SDK

海康开放平台

下载

原生示例

#include <Arduino.h>
#include <SPI.h>
// ESP-12E (NodeMCU) 引脚定义
#define LED   D0      // GPIO16
#define CS    D8      // GPIO15
#define RS    D1      // GPIO5
#define RESET D2      // GPIO4

void SPI_Init(void)
{
    SPI.begin();
    SPI.setClockDivider(40000000); // 设置SPI速度为40MHz
    SPI.setBitOrder(MSBFIRST);
    SPI.setDataMode(SPI_MODE0);  
}
void Lcd_Writ_Bus(uint8_t d) {
  SPI.transfer(d);
}

void Lcd_Write_Com(uint8_t VH) {   
  digitalWrite(RS, LOW);  // 使用digitalWrite替代直接端口操作
  Lcd_Writ_Bus(VH);
}

void Lcd_Write_Data(uint8_t VH) {
  digitalWrite(RS, HIGH); // 使用digitalWrite替代直接端口操作
  Lcd_Writ_Bus(VH);
}

void Lcd_Write_Com_Data(uint8_t com, uint8_t dat) {
  Lcd_Write_Com(com);
  Lcd_Write_Data(dat);
}

void Address_set(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) {
  Lcd_Write_Com(0x2a);
  Lcd_Write_Data(x1>>8);
  Lcd_Write_Data(x1);
  Lcd_Write_Data(x2>>8);
  Lcd_Write_Data(x2);
  Lcd_Write_Com(0x2b);
  Lcd_Write_Data(y1>>8);
  Lcd_Write_Data(y1);
  Lcd_Write_Data(y2>>8);
  Lcd_Write_Data(y2);
  Lcd_Write_Com(0x2c); 							 
}

void Lcd_Init(void)
{
  digitalWrite(RESET,HIGH);
  delay(5); 
  digitalWrite(RESET,LOW);
  delay(15);
  digitalWrite(RESET,HIGH);
  delay(15);

  digitalWrite(CS,LOW);  //CS

    Lcd_Write_Com(0xF7);  
    Lcd_Write_Data(0xA9); 
    Lcd_Write_Data(0x51); 
    Lcd_Write_Data(0x2C); 
    Lcd_Write_Data(0x82);  

    Lcd_Write_Com(0xC0);  
    Lcd_Write_Data(0x11); 
    Lcd_Write_Data(0x09); 

    Lcd_Write_Com(0xC1);  
    Lcd_Write_Data(0x41); 

    Lcd_Write_Com(0xC5);  
    Lcd_Write_Data(0x00); 
    Lcd_Write_Data(0x0A); 
    Lcd_Write_Data(0x80);
 
    Lcd_Write_Com(0xB1);  
    Lcd_Write_Data(0xB0); 
    Lcd_Write_Data(0x11); 

    Lcd_Write_Com(0xB4);  
    Lcd_Write_Data(0x02); 
  
    Lcd_Write_Com(0xB6);    
    Lcd_Write_Data(0x02);
    Lcd_Write_Data(0x22);  
 
    Lcd_Write_Com(0xB7);    
    Lcd_Write_Data(0xC6);  

    Lcd_Write_Com(0xBE);    
    Lcd_Write_Data(0x00);   
    Lcd_Write_Data(0x04); 
 
    Lcd_Write_Com(0xE9);    
    Lcd_Write_Data(0x00);   
 
    Lcd_Write_Com(0x36);  
    Lcd_Write_Data(0x08);   

    Lcd_Write_Com(0x3A);    
    Lcd_Write_Data(0x66); 

    Lcd_Write_Com(0xE0);    
    Lcd_Write_Data(0x00);  
    Lcd_Write_Data(0x07); 
    Lcd_Write_Data(0x10); 
    Lcd_Write_Data(0x09); 
    Lcd_Write_Data(0x17); 
    Lcd_Write_Data(0x0B); 
    Lcd_Write_Data(0x41); 
    Lcd_Write_Data(0x89); 
    Lcd_Write_Data(0x4B); 
    Lcd_Write_Data(0x0A); 
    Lcd_Write_Data(0x0C); 
    Lcd_Write_Data(0x0E); 
    Lcd_Write_Data(0x18); 
    Lcd_Write_Data(0x1B); 
    Lcd_Write_Data(0x0F); 

    Lcd_Write_Com(0xE1);    
    Lcd_Write_Data(0x00);  
    Lcd_Write_Data(0x17); 
    Lcd_Write_Data(0x1A); 
    Lcd_Write_Data(0x04); 
    Lcd_Write_Data(0x0E); 
    Lcd_Write_Data(0x06); 
    Lcd_Write_Data(0x2F); 
    Lcd_Write_Data(0x45); 
    Lcd_Write_Data(0x43); 
    Lcd_Write_Data(0x02); 
    Lcd_Write_Data(0x0A); 
    Lcd_Write_Data(0x09); 
    Lcd_Write_Data(0x32); 
    Lcd_Write_Data(0x36); 
    Lcd_Write_Data(0x0F); 

    Lcd_Write_Com(0x11);    //Exit Sleep 
    delay(120); 			
    Lcd_Write_Com(0x29);    //Display on 

    digitalWrite(CS,HIGH);
}
void H_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c)                   
{	
  unsigned int i,j;
  digitalWrite(CS,LOW);
  Lcd_Write_Com(0x02c); //write_memory_start
  //digitalWrite(RS,HIGH);
  l=l+x;
  Address_set(x,y,l,y);
  j=l*2;
  for(i=1;i<=j;i++)
  {
      Lcd_Write_Data((c>>8)&0xF8);
      Lcd_Write_Data((c>>3)&0xFC);
      Lcd_Write_Data(c<<3);
  }
  digitalWrite(CS,HIGH);   
}

void V_line(unsigned int x, unsigned int y, unsigned int l, unsigned int c)                   
{	
  unsigned int i,j;
  digitalWrite(CS,LOW);
  Lcd_Write_Com(0x02c); //write_memory_start
  //digitalWrite(RS,HIGH);
  l=l+y;
  Address_set(x,y,x,l);
  j=l*2;
  for(i=1;i<=j;i++)
  { 
      Lcd_Write_Data((c>>8)&0xF8);
      Lcd_Write_Data((c>>3)&0xFC);
      Lcd_Write_Data(c<<3);
  }
  digitalWrite(CS,HIGH);   
}

void Rect(unsigned int x,unsigned int y,unsigned int w,unsigned int h,unsigned int c)
{
  H_line(x  , y  , w, c);
  H_line(x  , y+h, w, c);
  V_line(x  , y  , h, c);
  V_line(x+w, y  , h, c);
}

void Rectf(unsigned int x,unsigned int y,unsigned int w,unsigned int h,unsigned int c)
{
  unsigned int i;
  for(i=0;i<h;i++)
  {
    H_line(x  , y  , w, c);
    H_line(x  , y+i, w, c);
  }
}

int RGB(int r,int g,int b)
{
  return r << 16 | g << 8 | b;
}

void LCD_Clear(unsigned int j)                   
{	
  unsigned int i,m;
  digitalWrite(CS,LOW);
  Address_set(0,0,320,480);
  for(i=0;i<320;i++)
    for(m=0;m<480;m++)
    {
      Lcd_Write_Data((j>>8)&0xF8);
      Lcd_Write_Data((j>>3)&0xFC);
      Lcd_Write_Data(j<<3);
    }
  digitalWrite(CS,HIGH);   
}
void setup() {
  // 初始化引脚模式
  pinMode(LED, OUTPUT);
  pinMode(RS, OUTPUT);
  pinMode(RESET, OUTPUT);
  pinMode(CS, OUTPUT);
  
  // 设置初始状态
  digitalWrite(LED, HIGH);  // 背光开启
  digitalWrite(CS, HIGH);   // 初始不选中
  digitalWrite(RS, HIGH);   // 初始数据模式
  
  SPI_Init();
  Lcd_Init();
}

void loop() {
  LCD_Clear(0xf800);  // 红色
  LCD_Clear(0x07E0);  // 绿色
  LCD_Clear(0x001F);  // 蓝色
  LCD_Clear(0x0000);  // 黑色
  
  for(int i=0; i<100; i++) {  // 减少测试图形数量
    Rect(random(300), random(300), random(100), random(100), random(65535));
  }
}

GFX示例

/*******************************************************************************
 * Start of Arduino_GFX setting
 *
 * Arduino_GFX try to find the settings depends on selected board in Arduino IDE
 * Or you can define the display dev kit not in the board list
 * Defalult pin list for non display dev kit:
 * Arduino Nano, Micro and more: CS:  9, DC:  8, RST:  7, BL:  6, SCK: 13, MOSI: 11, MISO: 12
 * ESP32 various dev board     : CS:  5, DC: 27, RST: 33, BL: 22, SCK: 18, MOSI: 23, MISO: nil
 * ESP32-C3 various dev board  : CS:  7, DC:  2, RST:  1, BL:  3, SCK:  4, MOSI:  6, MISO: nil
 * ESP32-S2 various dev board  : CS: 34, DC: 38, RST: 33, BL: 21, SCK: 36, MOSI: 35, MISO: nil
 * ESP32-S3 various dev board  : CS: 40, DC: 41, RST: 42, BL: 48, SCK: 36, MOSI: 35, MISO: nil
 * ESP8266 various dev board   : CS: 15, DC:  4, RST:  2, BL:  5, SCK: 14, MOSI: 13, MISO: 12
 * Raspberry Pi Pico dev board : CS: 17, DC: 27, RST: 26, BL: 28, SCK: 18, MOSI: 19, MISO: 16
 * RTL8720 BW16 old patch core : CS: 18, DC: 17, RST:  2, BL: 23, SCK: 19, MOSI: 21, MISO: 20
 * RTL8720_BW16 Official core  : CS:  9, DC:  8, RST:  6, BL:  3, SCK: 10, MOSI: 12, MISO: 11
 * RTL8722 dev board           : CS: 18, DC: 17, RST: 22, BL: 23, SCK: 13, MOSI: 11, MISO: 12
 * RTL8722_mini dev board      : CS: 12, DC: 14, RST: 15, BL: 13, SCK: 11, MOSI:  9, MISO: 10
 * Seeeduino XIAO dev board    : CS:  3, DC:  2, RST:  1, BL:  0, SCK:  8, MOSI: 10, MISO:  9
 * Teensy 4.1 dev board        : CS: 39, DC: 41, RST: 40, BL: 22, SCK: 13, MOSI: 11, MISO: 12
 ******************************************************************************/
#include <Arduino_GFX_Library.h>

// ESP12E 引脚定义
#define TFT_CS   15   // GPIO15
#define TFT_DC   4    // GPIO4
#define TFT_RST  2    // GPIO2
#define TFT_BL   5    // GPIO5(背光控制)
void testDisplay();
// 使用硬件SPI
Arduino_DataBus *bus = new Arduino_ESP8266SPI(TFT_DC, TFT_CS);
Arduino_GFX *gfx = new Arduino_ILI9488_18bit(bus, TFT_RST); // 使用18位模式

void setup(void) {
  Serial.begin(115200);
  Serial.println("\n\nILI9488 3.5\" Display Test");
  
  // 先控制复位引脚
  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, LOW);
  delay(100);
  digitalWrite(TFT_RST, HIGH);
  delay(200);  // 复位后等待
  
  // 初始化背光(尝试两种逻辑)
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  // 尝试高电平
  delay(100);
  digitalWrite(TFT_BL, LOW);   // 尝试低电平
  delay(100);
  
  Serial.println("Initializing display...");
  
  // 初始化显示屏(降低SPI速度)
  if (!gfx->begin(27000000)) { // 27MHz SPI速度
    Serial.println("Display init failed!");
  } else {
    Serial.println("Display init success!");
  }
  
  // 执行硬件测试
  testDisplay();
  
  Serial.println("Setup complete");
}

void testDisplay() {
  // 测试背光
  Serial.println("Testing backlight...");
  for (int i = 0; i < 3; i++) {
    digitalWrite(TFT_BL, HIGH);
    delay(300);
    digitalWrite(TFT_BL, LOW);
    delay(300);
  }
  digitalWrite(TFT_BL, HIGH); // 保持开启
  
  // 颜色填充测试
  Serial.println("Color fill test...");
  gfx->fillScreen(RED);
  delay(1000);
  gfx->fillScreen(GREEN);
  delay(1000);
  gfx->fillScreen(BLUE);
  delay(1000);
  
  // 显示文本
  gfx->fillScreen(BLACK);
  gfx->setTextColor(WHITE, BLACK);
  gfx->setTextSize(2, 2);
  gfx->setCursor(30, 140);
  gfx->println("ILI9488 3.5\"");
  
  gfx->setCursor(50, 180);
  gfx->println("480x320");
  
  gfx->setCursor(80, 220);
  gfx->setTextColor(YELLOW, BLACK);
  gfx->println("WORKING!");
}

void loop() {
  // 显示动态内容
  static int counter = 0;
  gfx->fillRect(0, 0, 480, 40, BLACK); // 清空顶部区域
  
  gfx->setTextSize(2, 2);
  gfx->setCursor(10, 10);
  gfx->setTextColor(random(0xFFFF), BLACK);
  gfx->printf("Counter: %d", counter++);
  
  delay(500);
}

GFX结合LVGL

/*******************************************************************************
 * [原有Arduino_GFX设置代码保持不变]
 ******************************************************************************/
#include <Arduino_GFX_Library.h>
#include <lvgl.h> // 添加LVGL库

void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p);
void create_test_ui();
// ESP12E 引脚定义
#define TFT_CS   15   // GPIO15
#define TFT_DC   4    // GPIO4
#define TFT_RST  2    // GPIO2
#define TFT_BL   5    // GPIO5(背光控制)

// 使用硬件SPI
Arduino_DataBus *bus = new Arduino_ESP8266SPI(TFT_DC, TFT_CS);
Arduino_GFX *gfx = new Arduino_ILI9488_18bit(bus, TFT_RST); // 使用18位模式

/* LVGL配置 */
#define LV_HOR_RES_MAX 480   // 水平分辨率
#define LV_VER_RES_MAX 320   // 垂直分辨率

#define LV_BUF_SIZE (LV_HOR_RES_MAX * 2) // 缓冲区大小(行数)

static lv_disp_draw_buf_t draw_buf;     // LVGL绘制缓冲区
static lv_color_t buf1[LV_BUF_SIZE];    // 第一缓冲区
static lv_color_t buf2[LV_BUF_SIZE];    // 第二缓冲区(双缓冲)

// 测试界面相关变量
static lv_obj_t *main_screen;
static lv_obj_t *time_label;
static lv_obj_t *counter_label;
static lv_obj_t *anim_bar;
static lv_obj_t *arc;
static uint32_t counter = 0;

void setup(void) {
  Serial.begin(115200);
  Serial.println("\n\nILI9488 with LVGL Integration");
  
  // 复位显示屏
  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, LOW);
  delay(100);
  digitalWrite(TFT_RST, HIGH);
  delay(200);
  
  // 初始化背光
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  // 保持常亮

  // 初始化显示屏
  if (!gfx->begin(27000000)) { // 27MHz SPI速度
    Serial.println("Display init failed!");
    while(1); // 停止执行
  }
  // 设置横屏模式
  gfx->setRotation(1); // 1表示90度旋转,通常是横屏
  Serial.println("Display init success!");

  /* 初始化LVGL */
  lv_init();
  
  /* 初始化双缓冲区 */
  lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_BUF_SIZE);
  
  /* 注册显示驱动 */
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv);
  disp_drv.hor_res = LV_HOR_RES_MAX;
  disp_drv.ver_res = LV_VER_RES_MAX;
  disp_drv.flush_cb = my_disp_flush;  // 注册刷新回调
  disp_drv.draw_buf = &draw_buf;
  lv_disp_drv_register(&disp_drv);

  /* 创建测试界面 */
  create_test_ui();
  
  Serial.println("LVGL test interface initialized");
}

/* 显示刷新回调函数 */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p) {
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);
  
  gfx->draw16bitRGBBitmap(
    area->x1,        // x起始位置
    area->y1,        // y起始位置
    (uint16_t *)color_p, // 数据指针
    w,               // 宽度
    h                // 高度
  );
  
  lv_disp_flush_ready(disp); // 通知LVGL刷新完成
}

/* 创建测试界面 */
void create_test_ui() {
  // 创建主屏幕
  main_screen = lv_scr_act();
  lv_obj_set_style_bg_color(main_screen, lv_color_hex(0x003060), 0);
  
  // 标题
  lv_obj_t *title = lv_label_create(main_screen);
  lv_label_set_text(title, "LVGL Test Interface");
  // 移除字体设置,使用默认字体
  lv_obj_set_style_text_color(title, lv_color_white(), 0);
  lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
  
  // 时间显示标签
  time_label = lv_label_create(main_screen);
  lv_label_set_text(time_label, "Time: 0s");
  // 移除字体设置,使用默认字体
  lv_obj_set_style_text_color(time_label, lv_color_white(), 0);
  lv_obj_align(time_label, LV_ALIGN_TOP_MID, 0, 50);
  
  // 计数器显示标签
  counter_label = lv_label_create(main_screen);
  lv_label_set_text(counter_label, "Counter: 0");
  // 移除字体设置,使用默认字体
  lv_obj_set_style_text_color(counter_label, lv_color_white(), 0);
  lv_obj_align(counter_label, LV_ALIGN_TOP_MID, 0, 80);
  
  // 弧形进度条
  arc = lv_arc_create(main_screen);
  lv_obj_set_size(arc, 150, 150);
  lv_arc_set_rotation(arc, 180);
  lv_arc_set_bg_angles(arc, 0, 360);
  lv_arc_set_value(arc, 0);
  lv_obj_align(arc, LV_ALIGN_CENTER, 0, -20);
  
  // 弧形标签
  lv_obj_t *arc_label = lv_label_create(main_screen);
  lv_label_set_text(arc_label, "Arc Progress");
  lv_obj_set_style_text_color(arc_label, lv_color_white(), 0);
  lv_obj_align_to(arc_label, arc, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
  
  // 进度条
  anim_bar = lv_bar_create(main_screen);
  lv_obj_set_size(anim_bar, 300, 20);
  lv_obj_align(anim_bar, LV_ALIGN_BOTTOM_MID, 0, -50);
  lv_bar_set_range(anim_bar, 0, 100);
  lv_bar_set_value(anim_bar, 0, LV_ANIM_ON);
  
  // 进度条标签
  lv_obj_t *bar_label = lv_label_create(main_screen);
  lv_label_set_text(bar_label, "Animated Bar");
  lv_obj_set_style_text_color(bar_label, lv_color_white(), 0);
  lv_obj_align_to(bar_label, anim_bar, LV_ALIGN_OUT_TOP_MID, 0, -5);
  
  // 创建动画
  lv_anim_t a;
  lv_anim_init(&a);
  lv_anim_set_var(&a, anim_bar);
  lv_anim_set_values(&a, 0, 100);
  lv_anim_set_time(&a, 3000);
  lv_anim_set_playback_time(&a, 3000);
  lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
  lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_bar_set_value);
  lv_anim_start(&a);
}

void loop() {
  // 更新时间显示
  static uint32_t last_update = 0;
  uint32_t current_time = millis() / 1000;
  
  if (current_time != last_update) {
    last_update = current_time;
    char time_buf[32];
    sprintf(time_buf, "Time: %ds", (int)current_time);
    lv_label_set_text(time_label, time_buf);
    
    // 更新计数器
    counter++;
    char counter_buf[32];
    sprintf(counter_buf, "Counter: %d", (int)counter);
    lv_label_set_text(counter_label, counter_buf);
    
    // 更新弧形进度条
    lv_arc_set_value(arc, counter % 101);
  }
  
  lv_timer_handler(); // 处理LVGL任务
  delay(5); // 短暂延时
}

触摸屏驱动解决方案 (ILI9488 + MSP3520)

有问题

#include <Arduino_GFX_Library.h>
#include <XPT2046_Touchscreen.h> // MSP3520兼容XPT2046库

// 显示屏引脚配置
#define TFT_CS   15   // GPIO15
#define TFT_DC   4    // GPIO4
#define TFT_RST  2    // GPIO2
#define TFT_BL   5    // GPIO5

// 触摸屏引脚配置
#define TOUCH_CS 16   // GPIO16 (触摸片选)
#define TOUCH_IRQ -1  // 不使用中断

// 使用硬件SPI
Arduino_DataBus *bus = new Arduino_ESP8266SPI(TFT_DC, TFT_CS);
Arduino_GFX *gfx = new Arduino_ILI9488_18bit(bus, TFT_RST);

// 触摸屏对象
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);

// 触摸校准参数 (根据实际调整)
const int TS_MINX = 200;
const int TS_MINY = 200;
const int TS_MAXX = 3800;
const int TS_MAXY = 3800;

void setup() {
  Serial.begin(115200);
  Serial.println("\nILI9488 + MSP3520 Test");
  
  // 显示屏复位
  pinMode(TFT_RST, OUTPUT);
  digitalWrite(TFT_RST, LOW);
  delay(50);
  digitalWrite(TFT_RST, HIGH);
  delay(150);
  
  // 背光初始化(固定高电平)
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  // 保持常亮

  // 降低SPI速度确保稳定性
  if (!gfx->begin(15000000)) { // 15MHz
    Serial.println("Display init failed!");
    while(1); // 卡死检测
  }
  Serial.println("Display ready");
  
  // 触摸屏初始化
  touch.begin();
  touch.setRotation(1); // 根据屏幕方向调整
  Serial.println("Touch ready");

  // 显示初始界面
  gfx->fillScreen(BLACK);
  gfx->setTextSize(2, 2);
  gfx->setTextColor(WHITE);
  gfx->setCursor(60, 140);
  gfx->print("TOUCH TEST");
}

void loop() {
  // 触摸检测
  if (touch.touched()) {
    TS_Point p = touch.getPoint();
    
    // 坐标转换 (480x320)
    int x = map(p.y, TS_MINY, TS_MAXY, 0, 479);
    int y = map(p.x, TS_MINX, TS_MAXX, 319, 0);
    
    Serial.printf("Touch: X=%d, Y=%d, Pressure=%d\n", x, y, p.z);
    
    // 视觉反馈:触摸点
    gfx->fillCircle(x, y, 5, RED);
    delay(50);
    gfx->fillCircle(x, y, 5, BLACK);
  }
  
  // 显示帧率计数器
  static uint32_t lastUpdate = 0;
  static int counter = 0;
  
  if (millis() - lastUpdate > 1000) {
    gfx->fillRect(0, 0, 100, 30, BLACK);
    gfx->setCursor(10, 10);
    gfx->setTextColor(GREEN);
    gfx->printf("FPS:%2d", counter);
    
    counter = 0;
    lastUpdate = millis();
  }
  counter++;
}

使用TFT_eSPI库驱动

需要配置TFT_eSPI

  1. 打开Arduino库文件夹中的TFT_eSPI目录
  2. 编辑User_Setup.h文件
  3. 确保有以下配置:


#define USER_SETUP_INFO "User_Setup"

#define TFT_DRIVER      ILI9488

// ESP8266引脚配置
#define TFT_CS   15   // GPIO15
#define TFT_DC   4    // GPIO4
#define TFT_RST  2    // GPIO2

// 使用SPI
#define TFT_SPI_FREQUENCY  27000000

// 屏幕尺寸
#define TFT_WIDTH  480
#define TFT_HEIGHT 320

// 颜色格式
#define TFT_RGB_ORDER TFT_BGR

// 字体设置
#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#include <TFT_eSPI.h>      // 包含TFT_eSPI库
#include <SPI.h>           // SPI通信库
#include <XPT2046_Touchscreen.h> // MSP3520触摸屏驱动

// 屏幕尺寸定义
#define SCREEN_WIDTH  480
#define SCREEN_HEIGHT 320

// 触摸屏引脚定义
#define TOUCH_CS  16  // GPIO16 (触摸片选)
#define TOUCH_IRQ -1  // 不使用中断

// 创建TFT和触摸对象
TFT_eSPI tft = TFT_eSPI();  // 创建TFT对象
XPT2046_Touchscreen touch(TOUCH_CS, TOUCH_IRQ);  // 创建触摸对象

// 触摸校准参数 (根据实际情况调整)
const int TS_MINX = 250;
const int TS_MINY = 280;
const int TS_MAXX = 3750;
const int TS_MAXY = 3780;

// 颜色定义
#define BACKGROUND_COLOR 0x18E3  // 深蓝色背景
#define UI_COLOR         0x07FF  // 青色UI元素
#define TEXT_COLOR       0xFFFF  // 白色文本
#define BUTTON_COLOR     0xF800  // 红色按钮
#define BUTTON_TEXT      0xFFFF  // 白色按钮文本

// 按钮结构
struct Button {
  int x, y, width, height;
  String label;
};

// 创建按钮数组
Button buttons[] = {
  {50, 120, 120, 50, "Button 1"},
  {190, 120, 120, 50, "Button 2"},
  {330, 120, 120, 50, "Button 3"}
};

const int buttonCount = sizeof(buttons) / sizeof(Button);

void setup() {
  Serial.begin(115200);
  Serial.println("\nILI9488 + MSP3520 with TFT_eSPI");
  
  // 初始化显示屏
  tft.init();
  tft.setRotation(1);  // 设置屏幕方向 (0-3)
  tft.fillScreen(TFT_BLACK);
  
  // 初始化背光 (如果连接了背光引脚)
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, HIGH);  // 开启背光
  
  // 初始化触摸屏
  touch.begin();
  touch.setRotation(1);  // 设置触摸方向与屏幕一致
  
  // 创建UI界面
  createUI();
  
  Serial.println("Setup complete");
}

void createUI() {
  // 绘制背景
  tft.fillScreen(BACKGROUND_COLOR);
  
  // 绘制标题
  tft.setTextColor(TEXT_COLOR, BACKGROUND_COLOR);
  tft.setTextSize(3);
  tft.setTextDatum(TC_DATUM);  // 顶部居中
  tft.drawString("TFT_eSPI Demo", SCREEN_WIDTH / 2, 20);
  
  // 绘制子标题
  tft.setTextSize(2);
  tft.drawString("ILI9488 + MSP3520", SCREEN_WIDTH / 2, 60);
  
  // 绘制按钮
  for (int i = 0; i < buttonCount; i++) {
    drawButton(buttons[i]);
  }
  
  // 绘制触摸状态区域
  tft.fillRoundRect(50, 200, SCREEN_WIDTH - 100, 80, 10, TFT_DARKGREY);
  tft.setTextColor(TEXT_COLOR, TFT_DARKGREY);
  tft.setTextSize(2);
  tft.drawString("Touch Status", SCREEN_WIDTH / 2, 210);
  tft.setTextSize(1);
  tft.drawString("Touch screen to see coordinates", SCREEN_WIDTH / 2, 240);
}

void drawButton(Button btn) {
  // 绘制按钮背景
  tft.fillRoundRect(btn.x, btn.y, btn.width, btn.height, 10, BUTTON_COLOR);
  
  // 绘制按钮边框
  tft.drawRoundRect(btn.x, btn.y, btn.width, btn.height, 10, TFT_WHITE);
  
  // 绘制按钮文本
  tft.setTextColor(BUTTON_TEXT, BUTTON_COLOR);
  tft.setTextSize(2);
  tft.setTextDatum(MC_DATUM);  // 居中
  tft.drawString(btn.label, btn.x + btn.width / 2, btn.y + btn.height / 2);
}

void processTouch() {
  static int lastX = -1, lastY = -1;
  
  if (touch.touched()) {
    TS_Point p = touch.getPoint();
    
    // 映射触摸坐标到屏幕坐标
    int x = map(p.y, TS_MINY, TS_MAXY, 0, SCREEN_WIDTH - 1);
    int y = map(p.x, TS_MINX, TS_MAXX, SCREEN_HEIGHT - 1, 0);
    
    // 限制坐标在屏幕范围内
    x = constrain(x, 0, SCREEN_WIDTH - 1);
    y = constrain(y, 0, SCREEN_HEIGHT - 1);
    
    // 显示触摸坐标
    tft.fillRect(60, 250, 360, 20, TFT_DARKGREY);
    tft.setTextColor(TFT_YELLOW, TFT_DARKGREY);
    tft.setTextSize(2);
    tft.drawString("X: " + String(x) + " Y: " + String(y), SCREEN_WIDTH / 2, 250);
    
    // 检查按钮点击
    for (int i = 0; i < buttonCount; i++) {
      if (isPointInButton(x, y, buttons[i])) {
        buttonPressed(i);
        break;
      }
    }
    
    // 绘制触摸点
    if (lastX != -1 && lastY != -1) {
      tft.fillCircle(lastX, lastY, 4, BACKGROUND_COLOR);
    }
    tft.fillCircle(x, y, 4, TFT_YELLOW);
    
    lastX = x;
    lastY = y;
  } 
  else if (lastX != -1) {
    // 清除触摸点
    tft.fillCircle(lastX, lastY, 4, BACKGROUND_COLOR);
    lastX = lastY = -1;
  }
}

bool isPointInButton(int x, int y, Button btn) {
  return (x >= btn.x && x < (btn.x + btn.width) &&
          y >= btn.y && y < (btn.y + btn.height));
}

void buttonPressed(int index) {
  // 视觉反馈:按钮按下效果
  Button btn = buttons[index];
  
  // 绘制按下的按钮
  tft.fillRoundRect(btn.x, btn.y, btn.width, btn.height, 10, TFT_DARKGREEN);
  tft.drawRoundRect(btn.x, btn.y, btn.width, btn.height, 10, TFT_WHITE);
  tft.setTextColor(TFT_WHITE, TFT_DARKGREEN);
  tft.setTextSize(2);
  tft.drawString(btn.label, btn.x + btn.width / 2, btn.y + btn.height / 2);
  
  // 在串口显示按下的按钮
  Serial.println("Button pressed: " + btn.label);
  
  delay(100); // 短暂显示按下的状态
  
  // 恢复按钮原始状态
  drawButton(btn);
}

void updateCounter() {
  static uint32_t lastUpdate = 0;
  static int counter = 0;
  
  if (millis() - lastUpdate > 1000) {
    // 更新计数器显示
    tft.fillRect(SCREEN_WIDTH - 100, 10, 90, 25, BACKGROUND_COLOR);
    tft.setTextColor(TEXT_COLOR, BACKGROUND_COLOR);
    tft.setTextSize(2);
    tft.setTextDatum(TR_DATUM); // 右上角对齐
    tft.drawString("Time: " + String(counter) + "s", SCREEN_WIDTH - 10, 10);
    
    counter++;
    lastUpdate = millis();
  }
}

void loop() {
  processTouch();  // 处理触摸事件
  updateCounter(); // 更新计数器
  delay(10);       // 短暂延迟
}

1. 查看版本

adb version

2. 查看adb设备

adb.exe devices

返回

E:\_1\qrs\tools\scrcpy-win64-v3.2>adb.exe devices
List of devices attached
f3ec0fc48fafc16f        device

3. 连接设备

如果只有一个设备

adb shell

如果有多个设备,需要指定连接的设备

3.1 安卓设备

adb connect HOST[:PORT]

3.2 Linux设备

adb -s f3ec0fc48fafc16f shell

4.电脑推送文件到设备

adb push <电脑文件路径> <设备路径>

test

adb push D:\Desktop\error.log /root
D:\Desktop\error.log: 1 file pushed, 0 skipped. 13.6 MB/s (3304 bytes in 0.000s)

5.复制设备文件到电脑

adb pull <设备文件路径> <电脑路径>

test(注意不能放在D盘(盘符)根目录,只能放在之下的目录)

adb p1ull /root/error.log D:\yc
/root/error.log: 1 file pulled, 0 skipped. 1.1 MB/s (3304 bytes in 0.003s)

IIC协议解析

一、IIC 模式设置

配置项含义与说明
IIC标准 I²C 模式。使用两根线(SDA 数据线、SCL 时钟线),支持多主机、多从机,通过地址寻址。这是最常用的模式。
SMBus-Alert-modeSMBus 的 警报信号模式。SMBus 是 I²C 的一个子集,增加了低功耗管理、超时检测等特性。Alert 模式允许从机通过一个专用信号线(SMBA#)主动通知主机发生事件,主机随后发起 Alert Response Address (ARA) 流程来识别哪个从机触发了警报。
SMBus-two-wire-interface也是 SMBus,但 不使用专用警报线,所有通信(包括警报响应)都在标准的 SDA/SCL 两根线上完成。更接近 I²C 的物理层,但遵循 SMBus 的协议层(如超时、包错误校验 PEC 等)。

简单区分

  • IIC:标准模式,无超时限制,支持任意速率(常见 100k/400k)。
  • SMBus Alert:需要额外 SMBA 线,用于从机主动报警。
  • SMBus 2-wire:仅用两根线,兼容 I²C 电气特性,但协议更严格(如最小时钟低电平时间、超时等)。

二、参数设置

1. Speed Mode 速度模式

  • Standard mode (100 kHz):经典速度,兼容性最好,适合大多数传感器、EEPROM。
  • Fast Mode (400 kHz):高速模式,传输更快,要求总线电容更低,部分旧设备不支持。

2. Clock Speed

  • 直接设定 SCL 时钟频率(单位通常是 Hz 或 kHz)。 如果已选 Speed Mode,此选项可能用于微调(例如设为 350 kHz 而非 400 kHz)。若没选模式,则需要手动输入频率值。

3. Clock No Stretch Mode 开启/关闭

  • 时钟拉伸:当从机来不及处理数据时,可以主动拉低 SCL,迫使主机等待。
  • Clock No Stretch Mode(无时钟拉伸模式)
    • 开启:禁止从机使用时钟拉伸。主机不会等待,如果从机拉低 SCL 可能会造成通信错误。
    • 关闭:允许从机拉伸时钟(标准 I²C 支持此特性)。
    • 使用场景:与某些不支持拉伸的快速设备通信,或为了简化主机时序。

4. Primary Address Length selection 地址长度

  • 7bit:标准 I²C 地址长度,最常用。实际发送时左移一位加 R/W 位。
  • 10bit:扩展地址长度,用于需要超过 112 个设备的总线(7 位地址除去保留地址后只有 112 个可用)。10 位地址需要两个字节传输,兼容性略差。

5. Dual Address Acknowledged 开启/关闭

  • 是否让从机 响应两个不同的从地址
    • 开启:从机有两个有效地址(例如主地址 + 第二地址),常用于需要区分不同功能的复合设备。
    • 关闭:只响应 Primary slave address。
    • 注意:此选项通常只在 从机模式 下有意义。

6. Primary slave address

  • 当本机作为 从机 时,自己的主要地址(7 位或 10 位,与地址长度设置匹配)。
  • 主机通过这个地址来访问本设备。

7. General Call Address detection

  • 广播地址检测。通用广播地址是 0x00(8 位形式,即 7 位地址 0x00 + R/W=0)。
    • 开启:从机响应广播地址,接收到主机发送的全局命令(如复位所有设备、改变从机地址等)。
    • 关闭:从机忽略广播地址,只响应自己的专用地址。
    • 一般传感器或简单从机不启用此功能,避免误响应。

IIC时序

SCK时钟稳定一高一低,高电平时开始采样。

采样时SDA数据线四个状态:

  • 从高到低:为开始状态,告诉设备要开始发送数据了。

  • 从低到高:为结束状态,告诉设备我已经发送完成了。

  • 高电平:数据为 1

  • 低电平:数据为 0

iic时序

应答过程:设备主动拉低电平。

IIC数据协议规定

一次完整的 IIC 读写操作(以写操作为例)包含以下序列:

  1. 起始条件(S):主机发出起始信号,开始一次通信。

  2. 设备地址与读写位(第 1 个字节): 前 7 位(A6~A0)为从机地址,用于寻址总线上最多 127 个设备。 最低位(R/W)表示本次操作方向:

    • 0 表示主机向从机写入数据(写操作)
    • 1 表示主机从从机读取数据(读操作) 该字节传输完成后,从机会在第 9 个时钟周期回复一个应答位(A)。
  3. 寄存器地址(第 2 个字节): 该字节(B7~B0)指向从机内部的寄存器地址。后续的读写操作将从此地址开始。 该字节传输完成后,从机再次回复一个应答位(A)。

    注意:若本次操作为连续读写,每次访问该寄存器后,地址指针会自动加 1,指向下一个寄存器。

  4. 数据字节(第 3 个及后续字节)

    • 写操作:主机发送数据字节(D7~D0),写入第 2 步指定的寄存器。从机每收到一个字节回复一个应答位(A)。之后可继续发送多个数据字节,寄存器地址自动递增。
    • 读操作:主机转为接收模式,从机发送数据字节(D7~D0),内容为当前寄存器地址中的值。主机每收到一个字节回复一个应答位(最后一个字节后可回复非应答 NAK 表示结束)。之后可继续读取多个字节,寄存器地址自动递增。
  5. 停止条件(S):主机发出停止信号,释放总线,结束本次通信。

iic数据规定

硬件IIC和软件IIC

  • 硬件IIC不需要操作SDA,SCL引脚,硬件会自己处理,只需要发送数据或命令就好。
//配置好硬件配置

void OLED_WriteCmd(uint8_t cmd) {
    uint8_t buf[2] = {0x00, cmd};  // 0x00 //cmd
    HAL_I2C_Master_Transmit(&hi2c1,OLED_ADDR, buf, 2, 100);
}
void OLED_WriteData(uint8_t data) {
    uint8_t buf[2] = {0x40, data};  // 0x40 //data
    HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, buf, 2, 100);
}
  • 需要自己模拟整个时序过程。
static void I2C_Start(void) {
    // SDA 和 SCL 初始高电平
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET); //时钟开始
    HAL_Delay(5);
    // SDA 拉低产生起始条件
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET);
    HAL_Delay(5);
    // SCL 拉低,准备传输数据
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);//时钟结束
    HAL_Delay(5);
}

static void I2C_Stop(void) {
    //1.先拉低SCL,再拉低SDA 
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET);
    HAL_Delay(5);
    // SCL 时钟开始
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
    HAL_Delay(5);
    // SDA 拉高产生停止条件
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
    HAL_Delay(5);
    // SCL 时钟结束
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
    
}

// 发送一个字节,返回从机应答位(0=应答,1=非应答)
static uint8_t I2C_SendByte(uint8_t data) {
    // 发送 8 位数据,高位在前
    for (uint8_t i = 0; i < 8; i++) {
        if (data & 0x80) {
            HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
        } else {
            HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET);
        }
        data <<= 1;
        HAL_Delay(2);
        // 产生时钟高电平,从机采样数据
        HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
        HAL_Delay(5);
        HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
        HAL_Delay(2);
    }
    // 释放 SDA 总线,准备接收应答
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
    HAL_Delay(2);
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
    HAL_Delay(5);
    // 读取应答位(低电平有效)
    uint8_t ack = HAL_GPIO_ReadPin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin);
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
    HAL_Delay(2);
    return ack;   // 0 表示应答,1 表示非应答
}


// 向 OLED 发送命令或数据(连续多个字节)
static void OLED_WriteBytes(uint8_t ctrl_byte, uint8_t *data, uint8_t len) {
    I2C_Start();
    I2C_SendByte(OLED_ADDR);          // 发送设备地址 + 写位
    I2C_SendByte(ctrl_byte);          // 控制字节:命令或数据
    for (uint8_t i = 0; i < len; i++) {
        I2C_SendByte(data[i]);
    }
    I2C_Stop();
}

// 便捷函数:写单个命令
static void OLED_WriteCmd(uint8_t cmd) {
    OLED_WriteBytes(0, &cmd, 1); // 0代表写命令,&cmd要发送的命令,1发送长度
}

// 便捷函数:写单个数据
static void OLED_WriteData(uint8_t data) {
    OLED_WriteBytes(1, &data, 1);
}

读数据&写数据

向设备写数据,前面已经有相关示例了。

读设备数据:

  • 硬件操作
// 从设备读取多个字节(标准流程:写寄存器地址 -> 重复起始 -> 读数据)
// ctrl_byte: 控制字节(例如 0x00 命令,0x40 数据,或者寄存器地址)
// data: 接收缓冲区
// len: 要读取的字节数
// 返回值:HAL 状态(HAL_OK 表示成功)
HAL_StatusTypeDef OLED_ReadBytes(uint8_t ctrl_byte, uint8_t *data, uint8_t len) {
    HAL_StatusTypeDef ret;
    // 1. 发送设备地址 + 写位,并发送控制字节
    ret = HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, &ctrl_byte, 1, 100);
    if (ret != HAL_OK) return ret;

    // 2. 重复起始,发送设备地址 + 读位,然后接收数据
    ret = HAL_I2C_Master_Receive(&hi2c1, OLED_ADDR, data, len, 100);
    return ret;
}

  • 软件模拟
// 读取一个字节,ack: 0=主机发送应答(继续读取),1=主机发送非应答(结束读取)
static uint8_t I2C_ReadByte(uint8_t ack) {
    uint8_t data = 0;
    // 释放 SDA 总线,让从机控制数据线
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
    HAL_Delay(2);
    for (uint8_t i = 0; i < 8; i++) {
        data <<= 1;
        // SCL 高电平,从机输出数据
        HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
        HAL_Delay(5);
        if (HAL_GPIO_ReadPin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin) == GPIO_PIN_SET) {
            data |= 0x01;
        }
        // SCL 低电平,准备下一位
        HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
        HAL_Delay(2);
    }
    // 主机发送应答位
    if (ack == 0) {
        HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_RESET); // 应答(低电平)
    } else {
        HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);   // 非应答(高电平)
    }
    HAL_Delay(2);
    // 产生应答时钟脉冲
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_SET);
    HAL_Delay(5);
    HAL_GPIO_WritePin(Soft_IIC_SCL_GPIO_Port, Soft_IIC_SCL_Pin, GPIO_PIN_RESET);
    HAL_Delay(2);
    // 释放 SDA 总线
    HAL_GPIO_WritePin(Soft_IIC_SDA_GPIO_Port, Soft_IIC_SDA_Pin, GPIO_PIN_SET);
    return data;
}

// 从 OLED 读取多个字节(先写入控制字节,再连续读取)
static void OLED_ReadBytes(uint8_t ctrl_byte, uint8_t *data, uint8_t len) {
    I2C_Start();
    I2C_SendByte(OLED_ADDR);          // 发送设备写地址
    I2C_SendByte(ctrl_byte);          // 发送控制字节(例如命令或寄存器地址)
    I2C_Start();                      // 重复起始条件
    I2C_SendByte(OLED_ADDR | 0x01);   // 发送设备读地址
    for (uint8_t i = 0; i < len; i++) {
        // 最后一个字节发送非应答,其余发送应答
        uint8_t ack = (i == len - 1) ? 1 : 0;
        data[i] = I2C_ReadByte(ack);
    }
    I2C_Stop();
}

hback-porch:HBP或tbp,行信号左边沿无效信号时间

HSYNC:行同步

由于mmap极为重要,所以单独拿出来作为一个注意点。

mmapmemory map 的缩写,它将内核中的一块内存(这里是 Framebuffer)映射到用户进程的地址空间,使得可以直接像访问普通数组一样读写这块内存,而不需要通过 read/write 系统调用。

说到mmap就不得不说零拷贝(zero-copy),举一些例子,但我们重点将mmap功能。

实现方式CPU拷贝次数系统调用适用场景硬件要求
mmap+write12小文件、需修改数据
sendfile0~11静态文件传输(HTTP服务器)可选DMA
splice01~2通用内核态数据传输
MSG_ZEROCOPY01大消息网络发送网卡支持
io_uring00 (共享内存)现代高性能异步I/O内核5.1+
RDMA00 (bypass)超低延迟、远程内存访问专用网卡

mmap应用场景

1. 高性能文件 I/O(替代 read/write)

这是 mmap 最经典的用途。

  • 传统方式 (read/write)磁盘 -> 内核页缓存 (Page Cache) -> 用户缓冲区。数据需要在内核态和用户态之间拷贝两次。
  • mmap 方式磁盘 -> 内核页缓存 <-> 用户虚拟地址。用户进程直接访问内核页缓存映射的地址,零拷贝(Zero-Copy),减少了上下文切换和数据拷贝开销。
  • 适用场景:
    • 大文件的随机读写(如数据库索引文件)。
    • 需要频繁读取同一文件的不同部分。
    • 注意:对于小文件或顺序读写,mmap 的优势不明显,甚至可能因为缺页中断(Page Fault)而比 read 慢。

2. 进程间通信(IPC)- 共享内存

mmap 是实现共享内存最标准、最便携的方式。

  • 原理:两个或多个进程映射同一个文件(或匿名内存),它们会看到同一块物理内存。一个进程修改数据,另一个进程立即可见。

  • 优势:比管道(Pipe)、消息队列(Message Queue)快得多,因为不需要数据拷贝。

  • 代码示例:

    c// 进程 A 和 进程 B 都执行以下代码
    int fd = open("shared_file", O_RDWR | O_CREAT, 0666);
    void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // 现在 ptr 指向的内存是共享的
    

3. 设备驱动交互(如 Framebuffer)

这正是你当前代码 001_proj.c 的使用场景。

  • 原理:Linux 将硬件设备抽象为文件(如 /dev/fb0, /dev/gpu, /dev/mem)。通过 mmap 将这些特殊文件映射到用户空间,用户程序可以直接读写硬件寄存器或显存。
  • 常见设备:
    • Framebuffer (/dev/fb0):直接操作屏幕像素。
    • GPIO / 嵌入式外设:在嵌入式 Linux 中,直接映射物理内存地址来控制硬件寄存器。
    • GPU 显存:图形程序中上传纹理或顶点数据。

4. 动态链接库的加载

当你运行一个程序时,操作系统加载 .so (共享库) 或 .dll 文件时,底层通常也使用 mmap

  • 原理:将库文件映射到内存,多个进程可以共享同一份库文件的物理页面(只读部分),节省内存。

5. 大内存分配(匿名映射)

当程序请求大块内存(例如 malloc 超过一定阈值,通常是 128KB 或 1MB)时,glibc 的 malloc 内部会使用 mmap 而不是 brk/sbrk

  • 参数fd 设为 -1flags 设为 MAP_ANONYMOUS | MAP_PRIVATE
  • 优势:释放方便(直接 munmap),不会造成堆内存碎片。

总结:什么时候该用 mmap

场景推荐指数原因
大文件随机读写⭐⭐⭐⭐⭐避免多次 seek 和拷贝,利用 OS 页缓存机制。
进程间共享大数据⭐⭐⭐⭐⭐最快的 IPC 方式,零拷贝。
操作硬件设备/显存⭐⭐⭐⭐⭐用户态直接访问硬件内存的唯一标准方式。
小文件顺序读写⭐⭐read/write 更简单,且 mmap 有缺页中断开销。
网络传输⭐⭐⭐结合 sendfilewrite 可实现零拷贝发送。

qemu ARM 驱动学习

本文档记录从零开始搭建驱动学习项目的完整步骤,包含所有脚本的源码与说明。


目录


1. 安装前置工具

# 设备树编译器
apt-get install -y device-tree-compiler

# QEMU ARM 模拟器
apt-get install -y qemu-system-arm

# ARM 交叉编译器 (也可用 Linaro 等预编译工具链)
apt-get install -y gcc-arm-linux-gnueabihf

# 辅助工具
apt-get install -y parted dosfstools mtools cpio

验证:

dtc --version                     # Device Tree Compiler 1.6.x
qemu-system-arm --version         # QEMU 6.x
arm-linux-gnueabihf-gcc --version # ARM gcc 11.x

2. 内核源码与编译

2.1 获取源码

# 方法 A:主线内核
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.226.tar.xz
tar -xf linux-5.10.226.tar.xz
mv linux-5.10.226 kernel

# 方法 B:厂商内核(RK、NXP 等,含 SoC 驱动补丁)
# 本项目使用 Rockchip 5.10 内核
# 目录: kernel/

2.2 配置内核

cd kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig

vexpress_defconfig 开启的关键选项:

配置项说明
CONFIG_ARCH_VEXPRESS=yvexpress 平台
CONFIG_SMP=y多核支持
CONFIG_ARM_GIC=y中断控制器
CONFIG_SERIAL_AMBA_PL011=y串口驱动
CONFIG_MMC_PL180=ySD 卡
CONFIG_SMC_LAN9118=y网卡

涉及的文件:

文件说明
arch/arm/configs/vexpress_defconfig默认配置
.config生成的最终配置
include/generated/autoconf.h配置的 C 头文件
include/config/配置标记目录

2.3 编译内核

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage dtbs modules_prepare

编译产物:

文件说明
arch/arm/boot/zImage内核镜像
arch/arm/boot/dts/*.dtb设备树
vmlinux未压缩内核(带符号表)
Module.symvers模块符号表

2.4 vermagic 匹配原理

vermagic 由 .config 决定,必须和运行内核一致

.config 选项vermagic 字段
CONFIG_SMP=ySMP
CONFIG_PREEMPT=ypreempt
CONFIG_THUMB2_KERNEL=ythumb2
# 查看内核 vermagic
strings arch/arm/boot/zImage | grep vermagic

2.5 kernel-build.sh

该脚本由 boot.sh 自动调用,也可单独执行。

位置:kernel-build.sh

#!/bin/bash
# kernel-build.sh - 编译 ARM 内核 (vexpress_defconfig)
# boot.sh 自动调用,也可单独执行

set -e

DIR="$(cd "$(dirname "$0")" && pwd)"
KERNEL_SRC="$DIR/kernel"

cd "$KERNEL_SRC"

if [ "$1" = "clean" ]; then
    echo "清理内核..."
    if [ -f arch/arm/boot/zImage ]; then
        ts=$(date +%Y-%m-%d-%H:%M:%S)
        cp arch/arm/boot/zImage "arch/arm/boot/zImage-vexpress-backup-${ts}"
        echo "已备份: zImage-vexpress-backup-${ts}"
    fi
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean 2>&1
    make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean 2>&1
    echo "=== 清理完成 ==="
    exit 0
fi

echo "配置内核 (vexpress_defconfig)..."
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- vexpress_defconfig 2>&1

echo "编译内核... make -j$(nproc)"
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j$(nproc) zImage dtbs modules_prepare 2>&1

echo ""
echo "=== 内核编译完成 ==="
ls -lh arch/arm/boot/zImage

echo ""
echo "复制 vexpress 设备树到 dts/..."
cp arch/arm/boot/dts/vexpress-v2p-ca9.dts  "$DIR/dts/" 2>/dev/null
cp arch/arm/boot/dts/vexpress-v2m.dtsi     "$DIR/dts/" 2>/dev/null
cp arch/arm/boot/dts/vexpress-v2p-ca9.dtb  "$DIR/dts/" 2>/dev/null
echo "完成"

用法:

./kernel-build.sh         # 编译内核
./kernel-build.sh clean   # 清理(自动备份 zImage)

3. 根文件系统 (initramfs)

3.1 设计思路

不使用完整 Linux 根文件系统(几百 MB),而是用最小 initramfs:

initramfs.cpio.gz (~1.4MB)
├── init              (PID 1 进程, 编译自 init.c)
└── bin/
    ├── busybox       (静态编译, 包含 sh/ls/insmod 等 100+ 命令)
    ├── sh → busybox
    ├── insmod → busybox
    └── ...

3.2 busybox 编译

修改busybox 的.config (table补全功能)

CONFIG_ASH_TAB_COMPLETION = y

编译:

cd rootfs
tar -xf busybox.tar.bz2
cd busybox-1_36_1
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
# → Settings: Build static binary (no shared libs) [=y]
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
# 产物: busybox (ARM 静态二进制)

3.3 init.c

PID 1 进程:挂载文件系统并启动 shell。

位置:rootfs/init.c

/* init: 挂载文件系统, 启动交互式 shell */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <sys/reboot.h>

int main(void)
{
    mkdir("/proc", 0755);
    mount("proc", "/proc", "proc", 0, NULL);
    mkdir("/sys", 0755);
    mount("sysfs", "/sys", "sysfs", 0, NULL);
    mkdir("/dev", 0755);
    mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);

    setenv("PS1", "\\w # ", 1);
    printf("QEMU ARM 设备树学习环境 - poweroff 关机\n");

    execl("/bin/sh", "sh", NULL);

    sync();
    reboot(0x4321fedc);
    return 0;
}

3.4 build-initramfs.sh

位置:rootfs/build-initramfs.sh

#!/bin/bash
# build-initramfs.sh - 构建完整的 initramfs 根文件系统
#   1. 编译 init.c
#   2. 放入 busybox 并预创建所有命令的软链接
#   3. 打包成 cpio.gz
#
# 输出: initramfs.cpio.gz

set -e
cd "$(dirname "$0")"

arm-linux-gnueabihf-gcc -static -Os -o init init.c 2>&1 | grep -v warn_unused || true

# 清理并创建目录结构
rm -rf initramfs_root
mkdir -p initramfs_root/{bin,sbin,etc,dev,proc,sys,tmp}

cp init initramfs_root/
cp busybox-1_36_1/busybox initramfs_root/bin/
chmod +x initramfs_root/init initramfs_root/bin/busybox

# 复制 .ko 模块(如果有)
if [ -d ../modules ] && [ "$(ls -A ../modules/**/*.ko 2>/dev/null)" ]; then
    echo "复制内核模块..."
    mkdir -p initramfs_root/modules
    cp ../modules/**/*.ko initramfs_root/modules/
fi

# 预创建 busybox 命令软链接
BB_LN="ln -s /bin/busybox initramfs_root"

for cmd in \
    sh ls cp mv rm cat echo printf test sleep \
    mkdir rmdir ln chmod chown chgrp \
    clear reset stty tty \
    ps top free uptime kill killall pkill \
    insmod lsmod rmmod modprobe modinfo \
    dmesg mount umount \
    grep find head tail wc sort cut tr uniq \
    seq basename dirname readlink realpath \
    date cal hexdump xxd strings \
    which env set export unset \
    tar gzip gunzip zcat bzip2 bunzip2 \
    vi diff patch cmp less more \
    df du sync dd mknod \
    xargs expr timeout yes nice \
    pidof pgrep watch; do
    $BB_LN/bin/$cmd 2>/dev/null || true
done

# poweroff 用 sysrq 触发关机
cat > initramfs_root/sbin/poweroff << 'EOF'
#!/bin/sh
sync
echo 1 > /proc/sys/kernel/sysrq 2>/dev/null
echo o > /proc/sysrq-trigger
EOF
chmod +x initramfs_root/sbin/poweroff

for cmd in \
    reboot halt \
    mdev ifconfig route ping ping6 arp \
    udhcpc ip neigh \
    swapon swapoff fdisk blkid blockdev \
    hwclock sysctl trigger; do
    $BB_LN/sbin/$cmd 2>/dev/null || true
done

cd initramfs_root
find . | cpio -o -H newc | gzip > ../initramfs.cpio.gz
cd ..

echo "=== initramfs 已生成 ==="
ls -lh initramfs.cpio.gz

rm -rf initramfs_root init

3.5 手动构建

cd rootfs
bash build-initramfs.sh

4. 设备树编译

4.1 文件结构

dts/
├── vexpress-v2p-ca9.dts    ← 板级设备树
└── vexpress-v2m.dtsi       ← 母板描述(被 include)

vexpress-v2p-ca9.dts 通过 #include "vexpress-v2m.dtsi" 引用公共部分。

4.2 编译命令

# 因为使用了 #include,需要用 cpp 预处理后再用 dtc 编译
cpp -nostdinc -I dts/ -undef -x assembler-with-cpp \
    dts/vexpress-v2p-ca9.dts | \
dtc -I dts -O dtb -o dts/vexpress-v2p-ca9.dtb -

# 反编译查看 DTB 内容
dtc -I dtb -O dts dts/vexpress-v2p-ca9.dtb

4.3 phandle 机制

DTS 中的标签(&gic&uart0)在编译时被替换为数字 phandle:

DTS (源文件):  interrupts = <&gic 0 37 4>;
DTB (二进制):  interrupts = <0x0001 0x00 0x25 0x04>;

5. 内核模块开发

5.1 模块源码

位置:modules/002_of/002_of.c

#include "linux/module.h"
#include "linux/init.h"
#include "linux/of.h"

static int of_init(void)
{ 
    printk("of_init\n");
    return 0;
}

static void of_exit(void)
{
    printk("of_exit\n");
}

module_init(of_init);
module_exit(of_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyl");
MODULE_DESCRIPTION("of function test");

5.2 模块 Makefile

位置:modules/002_of/Makefile

# ARM 交叉编译内核模块
KERNELDIR ?= ../../kernel

PWD := $(shell pwd)

obj-m := 002_of.o

all:
    $(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
        -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- \
        -C $(KERNELDIR) M=$(PWD) clean

5.3 编译流程

make -C kernel M=modules/002_of modules
  ├── 读取 kernel/Makefile
  ├── 读取 kernel/.config          ← 生成 vermagic
  ├── 进入 scripts/mod/modpost     ← 符号校验
  ├── 进入 modules/002_of/
  │     ├── 002_of.c → .o
  │     ├── modpost → .mod.c
  │     └── 链接 → .ko
  └── 生成 Module.symvers

5.4 常见问题

错误原因解决
invalid module formatvermagic 不匹配用同一份 .config 重编
Unknown symbol内核符号未导出检查 EXPORT_SYMBOL
undefined reference函数不存在确认内核 API 版本

6. QEMU 启动

6.1 boot.sh

主启动脚本。

位置:boot.sh

#!/bin/bash
# boot.sh - QEMU ARM 驱动/设备树学习环境 一键启动

DIR="$(cd "$(dirname "$0")" && pwd)"
KERNEL_DIR="$DIR/kernel/arch/arm/boot"

# 检查内核是否已编译
if [ ! -f "$KERNEL_DIR/zImage" ]; then
    echo "================================================"
    echo "  没有内核 zImage, 开始编译内核..."
    echo "================================================"
    bash "$DIR/kernel-build.sh"
    echo ""
fi

echo "编译设备树..."
(cpp -nostdinc -I"$DIR/dts" -undef -x assembler-with-cpp \
  "$DIR/dts/vexpress-v2p-ca9.dts" 2>/dev/null | \
dtc -I dts -O dtb -o "$DIR/dts/vexpress-v2p-ca9.dtb" - 2>&1) | grep -v Warning || true

echo "构建 initramfs..."
bash "$DIR/rootfs/build-initramfs.sh"

echo "=== QEMU vexpress-a9 启动 ==="
qemu-system-arm \
    -M vexpress-a9 \
    -kernel "$KERNEL_DIR/zImage" \
    -dtb "$DIR/dts/vexpress-v2p-ca9.dtb" \
    -initrd "$DIR/rootfs/initramfs.cpio.gz" \
    -append "console=ttyAMA0,115200" \
    -nographic -m 512M -no-reboot

6.2 启动流程

./boot.sh
  │
  ├── ① 检查 zImage → 没有则 kernel-build.sh
  │
  ├── ② 设备树: dts → dtb
  │
  ├── ③ initramfs: init.c + busybox → cpio.gz
  │
  └── ④ QEMU
        │
        └── 虚拟机内部
              ├── 加载 zImage
              ├── 加载 initramfs
              ├── 传入 DTB
              ├── 内核启动 → /init
              └── shell 就绪

6.3 QEMU 参数说明

参数说明
-M vexpress-a9模拟 vexpress-a9 开发板
-kernel内核镜像路径
-dtb设备树路径
-initrd根文件系统路径
-append "console=ttyAMA0"内核命令行
-nographic无图形界面
-m 512M512MB 内存
-no-reboot关机即退出

6.4 vexpress-a9 外设列表

地址设备内核驱动
0x10009000PL011 UART (控制台)SERIAL_AMBA_PL011
0x1000a000PL011 UART #1同上
0x10011000SP805 定时器ARM_TIMER_SP805
0x10017000PL031 RTCRTC_DRV_PL031
0x10020000PL181 MMC (SD卡)MMC_ARMMMCI
0x100e0000SMSC LAN9118 网卡SMC_LAN9118
0x1e000000GIC 中断控制器ARM_GIC

vexpress-a9 没有 I2C/SPI/GPIO 模拟。需要这些外设需用 -M virt


7. 错误排查

7.1 QEMU 无输出

# 原因:控制台参数不对
# 排查:检查 DTS 中 UART 地址和内核命令行
# 解决:确认 console= 参数与 DTB 中的 UART 匹配

7.2 Kernel panic: VFS: Unable to mount root fs

# 原因:内核没找到根文件系统
# 排查:
#   - -initrd 参数是否正确
#   - initramfs 是否损坏
# 解决:
file rootfs/initramfs.cpio.gz  # 应是 gzip compressed data

7.3 insmod: invalid module format

# 原因:vermagic 不匹配
# 排查:
strings kernel/arch/arm/boot/zImage | grep vermagic
strings modules/xxx/xxx.ko | grep vermagic
# 解决:确保 .config 一致,重新编译模块

7.4 make 报错 .git 不存在

# 原因:内核源码是 tar 解压的,没有 git 仓库
# 影响:无,只是警告信息
# 解决:忽略

附:全部脚本索引

#文件名用途
1boot.sh一键启动 QEMU
2kernel-build.sh编译/清理内核
3rootfs/build-initramfs.sh构建 initramfs
4rootfs/init.cPID 1 进程源码
5modules/002_of/Makefile模块 Makefile
6modules/002_of/002_of.c模块示例源码

SPI 协议详解

SPI 模式

SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工的同步串行通信总线,根据通信角色(主 / 从)和数据传输方向,可分为以下 8 种核心工作模式:

1. 全双工模式(Full-Duplex)

  • Full-Duplex Master(全双工主模式):

    作为 SPI 通信的主设备,可同时向从设备发送数据(MOSI 线)和接收从设备返回的数据(MISO 线),是 SPI 最常用的模式之一。例如在与 SPI 显示屏、Flash 芯片通信时,主设备发送指令 / 地址的同时,可接收设备的状态反馈,通信效率最高。

  • Full-Duplex Slave(全双工从模式):

    作为从设备,被动响应主设备的时钟(SCK),在主设备发送数据的同时,向主设备传输自身数据。典型应用如多传感器组网,传感器作为从设备,主控制器发起通信时,传感器同步回传采集数据。

2. 半双工模式(Half-Duplex)

  • Half-Duplex Master(半双工主模式):

    主设备同一时间段仅能单向传输数据(要么发、要么收),需通过硬件或软件控制数据方向。适用于无需同时收发的场景,如部分简单传感器(仅需主设备读取数据,或仅需主设备下发配置),可节省总线资源。

  • Half-Duplex Slave(半双工从模式):

    从设备仅能在主设备指定时段单向传输数据,常见于低功耗外设,减少不必要的信号交互。

3. 只读模式(Receive Only)

  • Recieve Only Master(只读主模式):

    主设备仅接收从设备数据,MOSI 线无数据输出,仅通过 SCK 提供时钟,从设备通过 MISO 线持续发送数据。适用于数据采集类场景(如高速 ADC 采样),主设备仅需读取采样结果,无需下发指令。

  • Recieve Only Slave(只读从模式):

    从设备仅向主设备发送数据,不接收主设备的任何指令 / 数据,典型如数据广播类外设(如温湿度传感器持续上报数据)。

4. 只发模式(Transmit Only)

  • Transmit Only Master(发送主模式):

    主设备仅向从设备发送数据,MISO 线无数据输入,适用于仅需下发控制指令的场景(如 LED 点阵屏控制,主设备持续发送显示数据,从设备无反馈)。

  • Transmit Only Slave(发送从模式):

    从设备仅接收主设备数据,无任何数据输出,常见于单向控制类外设(如继电器模块,仅接收主设备的通断指令)。

SPI 片选

SPI 通过片选(NSS,Slave Select)信号确定当前通信的从设备,多从设备场景下,仅被选中的从设备响应主设备的时钟和数据信号。

1. Hardware NSS Signal(硬件片选)

硬件片选由 SPI 控制器的专用 NSS 引脚实现,信号电平由硬件电路或控制器自动控制,稳定性高,减少软件开销。

  • Hardware NSS Input Signal(硬件 NSS 输入信号):

    该模式下 NSS 作为输入引脚,主设备通过检测 NSS 电平判断自身是否被选为 “从设备”(极少场景);或从设备通过 NSS 输入电平判断是否被主设备选中(主流场景)。通常高电平为 “未选中”,低电平为 “选中”,部分外设支持高电平片选,需匹配硬件手册。

  • Hardware NSS Output Signal(硬件 NSS 输出信号):

    仅主设备支持,NSS 作为输出引脚,主设备通信时自动拉低对应从设备的 NSS 电平,通信结束后拉高,实现精准的从设备选中控制。例如主设备挂载多个 SPI 从设备时,通过硬件 NSS 输出自动切换选中的从设备,无需软件干预。

2. Software NSS Signal(软件片选)

软件片选无需专用 NSS 引脚,可任意指定通用 IO 口作为片选引脚,通过软件控制引脚电平(高 / 低)实现从设备选择,灵活性高,适用于无专用 NSS 引脚或多从设备(超过硬件 NSS 数量)的场景。

  • 实现方式:初始化指定 IO 口为输出模式,通信前拉低目标从设备的片选 IO,通信完成后拉高,避免多个从设备同时响应。
  • 注意事项:软件片选需严格同步时钟(SCK)和片选电平,避免因软件执行延迟导致从设备误触发。

SPI 数据传输

SPI 数据传输的核心参数决定了数据的封装格式和解析规则,需主从设备严格一致,否则会出现数据解析错误。

1. Frame Format(协议格式)

SPI 主流协议格式为Motorola(摩托罗拉)格式,也是几乎所有 SPI 外设默认支持的格式;少数场景会兼容 TI 格式(同步串行通信的另一种格式),但非 SPI 标准,需特殊配置。

  • Motorola 格式特征:数据以帧为单位,时钟(SCK)同步,片选(NSS)拉低后开始传输,每帧数据的位宽由 Data Size 定义,高位 / 低位先行由 First Bit 定义。

2. Data Size(数据位宽)

定义单次传输的数据位数,需主从设备匹配,否则会出现数据截断或错位。

  • 8bits(八位传输):

    最常用的位宽,适用于绝大多数外设(如 Flash、传感器、显示屏),单帧传输 1 字节数据,解析简单,兼容性强。

  • 16bits(16 位传输):

    适用于高精度数据传输场景(如 ADC/DAC、电机驱动),单帧传输 2 字节数据,可减少传输次数,提升效率。

    • 从模式注意事项:单片机作为从设备时,需提前确认对接设备的传输位宽(8/16bits),若主设备发送 16bits 数据,从设备配置为 8bits 会导致仅接收低 8 位,高 8 位丢失;反之则会接收无效数据。

3. First Bit(数据位序)

定义数据帧中第一位传输的是高位还是低位,需主从设备严格一致。

  • MSB First(高位先行):

    数据的最高位(Most Significant Bit)先传输,是 SPI 默认且最主流的配置,几乎所有显示屏、Flash、传感器均采用此模式。例如传输字节 0x81(二进制 10000001),先传输最高位 “1”,最后传输最低位 “1”。

  • LSB First(低位先行):

    数据的最低位(Least Significant Bit)先传输,仅少数特殊外设(如部分工业传感器)使用,需手动配置控制器匹配。

SPI 时钟配置

SPI 的时钟(SCK)是同步通信的核心,时钟参数决定了通信速度和数据采样时机,需根据外设的最大支持速度和传输稳定性配置。

1. Prescaler(分频系数)

SPI 控制器的时钟源通常为单片机的系统时钟(如 72MHz、168MHz),通过分频系数降低时钟频率,得到 SPI 的实际波特率。

  • 常见分频系数:2、4、8、16、32、64、128 等,分频系数越小,SCK 频率越高,通信速度越快;反之则速度越慢。
  • 配置原则:需低于外设支持的最大 SCK 频率(如多数 SPI Flash 支持最高 108MHz,显示屏多支持≤50MHz),同时考虑传输距离(远距离传输需降低频率,避免信号衰减)。

2. Baud Rate(波特率)

SPI 的波特率即 SCK 的频率,计算公式为:波特率 = 系统时钟频率 / 分频系数

  • 示例:系统时钟 72MHz,分频系数 8,则波特率 = 72/8=9MHz,即每秒传输 9M 位数据。
  • 注意:全双工模式下,波特率同时决定 MOSI 和 MISO 的传输速度;单向模式(只读 / 只发)仅决定对应数据线的速度。

3. Clock Polarity (CPOL)(时钟极性)

定义 SPI 时钟线(SCK)在空闲状态(无数据传输时)的电平。

  • CPOL=0(LOW):空闲时 SCK 为低电平,数据传输时 SCK 在高 / 低电平间切换。
  • CPOL=1(HIGH):空闲时 SCK 为高电平,数据传输时 SCK 在低 / 高电平间切换。

4. Clock Phase (CPHA)(时钟相位)

定义数据采样的时机,即在 SCK 的第几个边沿(上升沿 / 下降沿)采集数据。

  • CPHA=0(1 Edge):第一个边沿采样

    • 若 CPOL=0(空闲低):SCK 从低到高的上升沿(第一个边沿)采样数据,下降沿发送数据;
    • 若 CPOL=1(空闲高):SCK 从高到低的下降沿(第一个边沿)采样数据,上升沿发送数据。
  • CPHA=1(2 Edge):第二个边沿采样

    • 若 CPOL=0(空闲低):SCK 从高到低的下降沿(第二个边沿)采样数据,上升沿发送数据;
    • 若 CPOL=1(空闲高):SCK 从低到高的上升沿(第二个边沿)采样数据,下降沿发送数据。

时钟模式组合(CPOL+CPHA)

CPOL 和 CPHA 的组合决定了 SPI 的 4 种核心时钟模式(Mode 0~3),是 SPI 配置的关键:

  • Mode 0:CPOL=0,CPHA=0(空闲低,上升沿采样)→ 最主流模式;
  • Mode 1:CPOL=0,CPHA=1(空闲低,下降沿采样);
  • Mode 2:CPOL=1,CPHA=0(空闲高,下降沿采样);
  • Mode 3:CPOL=1,CPHA=1(空闲高,上升沿采样)。

SPI CRC

CRC(Cyclic Redundancy Check,循环冗余校验)是 SPI 的可选数据校验机制,用于检测传输过程中的数据错误,提升通信可靠性。

1. CRC Polynomial(CRC 多项式)

SPI 控制器通过预设的 CRC 多项式生成校验码,主设备发送数据时附加 CRC 码,从设备接收后重新计算 CRC 并与接收的 CRC 码对比,判断数据是否出错。

  • 常用多项式:SPI 默认采用 CRC-8(多项式为 x⁸+x⁷+x⁶+x⁴+x²+1,对应十六进制 0x107),部分控制器支持 CRC-16(如 0x8005)。
  • 配置方式:需主从设备配置相同的 CRC 多项式,否则校验结果不匹配;若外设不支持 CRC,需关闭 SPI 的 CRC 功能,避免数据附加冗余位导致解析错误。
  • 应用场景:适用于高可靠性要求的场景(如工业控制、医疗设备),普通消费类外设(如显示屏)通常关闭 CRC 以提升传输效率。

2. CRC 校验流程

  1. 主设备初始化 CRC 计算器,写入待传输数据,生成 CRC 码;
  2. 主设备发送数据帧(含有效数据 + CRC 码);
  3. 从设备接收数据后,用相同多项式计算 CRC,对比接收到的 CRC 码;
  4. 若一致,确认数据正确;若不一致,判定传输错误,可触发重传或报错。

SPI 额外注意事项

  1. 总线拓扑:SPI 支持一主多从的星形拓扑,需注意片选信号的隔离和匹配,避免信号串扰;
  2. 信号电平:SPI 通常为 3.3V 电平,部分外设支持 5V,需通过电平转换芯片匹配,避免引脚烧毁;
  3. 传输同步:主设备的 SCK 时钟需与数据传输严格同步,高频传输时需考虑 PCB 走线长度,减少信号延迟;
  4. 中断 / DMA:高速 SPI 传输建议使用 DMA(直接存储器访问),减少 CPU 占用;低速传输可使用中断或轮询方式。

片选和数据/命令控制

涉及到数据传输。后面再补充

1. GRAM 写操作模式

LCD 驱动 IC 具有 GRAM(图形显存)写模式。当主机发送写显存命令(通常为 0x2C)后,控制器进入该模式,后续接收的所有数据均被写入 GRAM,并直接反映在屏幕上。

重要行为

  • 在 GRAM 写模式下,如果 DC(数据/命令)引脚被意外拉低(即发送了命令),控制器会立即退出 GRAM 模式,转而执行命令解析。
  • 因此,连续写入像素数据期间必须保持 DC 为高电平(数据模式)。

2. 命令分类

根据是否需要附带数据,LCD 命令可分为三类:

类型说明
无数据操作命令仅发送命令字节,无后续数据。例如:软件复位、进入睡眠模式等。
有数据操作命令发送命令后,必须发送固定数量的数据字节。例如:设置对比度、伽马校正。
不定长数据操作发送命令后,可连续发送任意长度的数据,直到被新命令打断。典型例子:写显存命令(0x2C

对于写显存操作,通常需要先通过 0x2A0x2B 命令设置窗口(地址范围),包括起始坐标 (x, y) 和结束坐标 (x1, y1)。之后发送的像素数据将按以下规则自动填充:

  • 第一个数据写入 (x, y)
  • 随后每个数据依次写入 (x+1, y)(x+2, y),… 直到该行末尾(x1)。
  • 到达行末后,列地址自动加 1(y+1),行地址回到起始列 x,继续写入。
  • 重复上述过程,直到最后一个数据写入 (x1, y1)。此后,控制器忽略任何额外发送的数据,直至收到新的命令或重新设置窗口。

:地址递增方向(先 X 后 Y 或先 Y 后 X)可通过特定寄存器配置,用于实现横屏/竖屏刷新方向。


3. SPI 通信方式:4 线制与 3 线制

3.1 4 线制 SPI(4W)

  • 信号线:SCK、SDI/MOSI、CS、DC(数据/命令区分线)。
  • 主机通过 DC 电平告知当前传输的是命令(DC=0)还是数据(DC=1)。

8 位模式

  • 每个传输周期为 8 个时钟周期。CS 可保持低电平以连续发送多个字节。

16 位模式

  • 若 LCD 控制器仅支持 8 位接收,则 16 位传输会被自动拆分为两个连续的 8 位传输。
  • 关键限制:发送命令时(如 0x2A0x2B0x2C),必须使用 8 位传输。若强行使用 16 位发送一个 8 位命令(例如高 8 位为命令,低 8 位填充 0),控制器会将低 8 位误判为数据,导致错误。 因此,16 位模式通常仅用于批量传输像素数据(颜色值),以提高效率。

3.2 3 线制 SPI(3W)

  • 无专用 DC 线。命令/数据的区分信息嵌入在数据流中:每个 9 位传输周期内,第 1 位为 DC 标志(1=数据,0=命令),后续 8 位为有效数据。

8 位模式的软件模拟

主机需用 GPIO 精确产生 9 个时钟周期:

// 假设已定义 DC_flag(0或1),data_byte 为要发送的8位数据

// 1. 发送 DC 标志位
HAL_GPIO_WritePin(SPI_SDIO_GPIO_Port, SPI_SDIO_Pin, DC_flag);
HAL_GPIO_WritePin(SPI_SCK_GPIO_Port, SPI_SCK_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(SPI_SCK_GPIO_Port, SPI_SCK_Pin, GPIO_PIN_RESET);

// 2. 发送 8 位数据(MSB 优先)
for (int i = 7; i >= 0; i--) {
    uint8_t bit = (data_byte >> i) & 0x01;
    HAL_GPIO_WritePin(SPI_SDIO_GPIO_Port, SPI_SDIO_Pin, bit);
    HAL_GPIO_WritePin(SPI_SCK_GPIO_Port, SPI_SCK_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(SPI_SCK_GPIO_Port, SPI_SCK_Pin, GPIO_PIN_RESET);
}

16 位模式的处理技巧

在 3 线制下,若想利用硬件 SPI 的 16 位传输能力,需解决“每 9 位为一个周期”与硬件“每 16 位连续时钟”之间的矛盾。常用两种方法:

方法原理实现要点
方法一:屏幕自动截取LCD 控制器的移位寄存器仅保留前 9 位,丢弃后续时钟产生的额外位。发送完整的 16 位数据,其中高 9 位有效(1 位 DC + 8 位数据),低 7 位任意填充(如 0)。发送完毕后拉高 CS,控制器在 CS 上升沿处理已接收的前 9 位,忽略剩余位。
方法二:CS 提前终止在发送完前 9 位后立即拉高 CS,强制终止本次传输,后 7 位不发送。该方法要求硬件能精确控制 CS 的拉高时机,但多数 SPI 外设难以在传输中途改变 CS 状态,故实际较少使用。

推荐方法一:构造 16 位数据 (DC_flag << 8) | data_byte,然后左移 7 位(使有效位对齐到高 9 位),低 7 位补 0。调用 HAL_SPI_Transmit 发送该 16 位数据,并在传输完成后拉高 CS。屏幕控制器将正确解析前 9 位。


4. 关键注意事项

  • GRAM 写模式下,务必保持 DC 为高电平,否则会意外退出数据模式。
  • 对于 4 线制 SPI,发送 8 位命令时禁止使用 16 位传输
  • 对于 3 线制 SPI 的 16 位传输,必须确保 LCD 控制器能够忽略多余的时钟位(多数主流控制器支持),否则需退回到 8 位模拟方式。
  • 窗口地址设置后不会自动清空,除非重新发送 0x2A / 0x2B 命令覆盖。

说明

SPI初始化+RGB显示:SPI负责初始化参数,RGB线路负责显示图像。可以通过SPI通信控制RGB显示,比如翻转,改变分辨率之类操作。

流程

  1. SPI初始化
  2. RGB控制

配置RCC时钟提供源为外部晶振

RCC配置项

  • High Speed Clock (HSE)高速时钟源

    • Disable 关闭
    • BYPASS CLock Source
    • Crystal/Ceramic Resonator
  • Low Speed Clock (LSE)低速时钟源

    • Disable 关闭
    • BYPASS CLock Source
    • Crystal/Ceramic Resonator
  • PLLCLK 锁相环,选择为外部晶振

  • APB1 Prescaler APB1的分频系数不能过高

TIM2设置

  • slave mode
  • trigger source
  • clock source
  • channel1
  • channel2
  • channel3
  • channel4
  • combined Channels

自动重装周期配置等(设置为1ms)

  • Counter Period设置为1000 即1ms(要先设置这个才能设置Prescaler )

  • Prescaler 设置为72000 即72-1

开启中断

  • NVIC Settings
    • TIM2 global interrupt 开启

启动和使用

  • 在main.c中调用启动函数
#include "tim.h"
HAL_TIM_Base_Start_IT(&htim2);
  • 在main.c中重写中断回调
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
	if(htim->Instance==TIM2)//如果是定时器2
	{
		uint8_t pData = 1;
		HAL_UART_Transmit(&huart1, &pData, 1, 100);
	}
}

页是读写单位,块是擦除单位

一、基本概念区分

术语常见设备作用最小操作单位
扇区 (Sector)机械硬盘、SD卡物理寻址和传输的最小单位通常 512B 或 4KB
块 (Block)NAND Flash擦除操作的最小单位通常 128KB、256KB 等
页 (Page)NAND Flash读写操作的最小单位通常 2KB、4KB、8KB 等

关键关系(针对 NAND Flash):

  • 一个块包含多个页(比如 64 个页,每个页 2KB → 块大小 128KB)
  • 读/写按进行,擦除必须按进行(不能单独擦除一个页)
  • “扇区”在 Flash 中不常用,但在一些文件系统或 SSD 控制器里会模拟扇区来兼容传统接口

二、举例

flash size = 255MB;
block size = 128KB;
page size = 2KB
  • 总容量 255 MB = 255 × 1024 KB = 261120 KB

  • 若块大小 = 128 KB,则块数量应为 261120 ÷ 128 = 2040 个

  • 若页大小 = 2 KB,则每块的页数应为 128 ÷ 2 = 64 页

内存卡,emmc,flash,eeprom(at24c256),nor flash(w25q64)这些存储设备里面都分为,页(page),扇区(sector),块(block)。

以w25q64这个存储芯片为例,共有

  • 128(8MB/64KB),即每个块大小为64KB
  • 每个块又分成16个扇区(64KB/4KB),即每个扇区大小为4KB
  • 每个扇区又分成16(4KB/256B),即每个页大小为256B

文件系统

区域内容作用
引导扇区/超级块文件系统魔数、块大小、inode 总数等文件系统的“身份证”,系统挂载时首先读取这里来识别格式
元数据区inode 表、目录项(dentry)记录文件的权限、大小、时间戳以及数据块的物理地址
数据区文件的实际内容存放你的文档、照片、代码等真实数据

超级块 = 书的版权页(写明书名、总页数、章节数)。

inode 表 = 书的目录(告诉你“第几章在第几页”)。

数据块 = 书的正文内容。

步骤Windows(自动)Linux(手动/自动)核心动作
1. 发现设备弹出“发现新硬件”内核生成设备节点 /dev/sdb1硬件被识别
2. 探测格式引导扇区,解析 FAT32/NTFS 签名Superblock(超级块),解析 magic number读取“身份证”
3. 建立连接自动分配盘符 E:执行 mount /dev/sdb1 /mnt/sdcard建立映射关系
4. 访问数据直接访问 E:通过 /mnt/sdcard访问路径重定向

Front

Css html js

Vue

index.html

​ 浏览器加载index.html

<!-- 浏览器看到的是: -->
<body>
    <div id="app"></div>  <!-- 空的,没有内容 -->
    <script type="module" src="/src/main.ts"></script>
</body>
  1. 解析 HTML:遇到 <div id="app">,创建空的 DOM 元素
  2. 注册元素:在 DOM 树中记录这个 div,设置 id="app"
  3. 样式计算:计算 CSS(如果有样式的话),但现在是空的
  4. 布局和绘制:绘制一个空矩形(0高度或默认高度)
  5. 继续解析:继续解析后面的 <script> 标签

进入到Vue的世界

  1. 解析main.ts,这一步开发环境和打包后是不同的。打包后就没有ts文件了,全部变成js文件。
  2. 在main.ts中,vue的语法,创建一个组件。再将这个组件绑定到id为app的div。
  3. App.vue的内容加载到界面
    中。

vite创建项目,不使用npm

npm create vue@latest

依赖安装

npm i

main.ts

1.main.ts中引入createApp,vue中的创建vue组件的工具。
2.引入App.vue,这个组件
3.创建app组件并挂载到id=app的div

//引入createApp用于创建应用
import { createApp } from "vue";
//引入App.vue根组件
import App from "./App.vue";
createApp(App).mount("#app");

App.vue

1.分为模板,脚本,样式区

2.学vue主要是学脚本区,其他两个区是css3,h5的技术栈

<template>
	
</template>
<script lang="ts">
    export default {
        name: 'App'
    }
</script>
<style scoped>

</style>

OptionsAPI(配置)和CompositionAPI(组合)

配置API

​ 每个功能都拆分。各种功能的数据放在一起。各种功能的方法也放一起。以及监视,计算属性都放一起。

export default{
	data(){
		return{
			user:{ //用户 功能 数据
				
			},
			dept:{ //部门 功能 数据

			}
		}
	},
	methods:{
		getUser(){//用户 功能 方法

		},
		getDept(){//部门 功能 方法

		}
	},
	computed:{

	},
	watch:{

	}
}

组合API

​ 将功能组合到一起。一个功能里面包含数据、方法、计算属性、监视。通过函数返回。

export default {
        name: 'Person',
        setup() { 
            let name = '张三' 
            let age = 18//此时的name不是响应式
            let phone = '12345678901'
            let dept = {
                id: 1,
                name: 'IT部'
            }
            console.log(this) //setup中this是undefined,Vue3弱化了this
            function addAge(){
                age++ //修改界面不会变化,但是值会变化
            }
            function shouTest(){
                alert(phone)
                console.log(phone)
            }
            return {name, age, phone, dept,addAge, shouTest}
        }
    }

基本使用

​ setup中没有this的概念,不能用this

export default {
        name: 'Person',
        setup() { 
            let name = '张三' //此时的name不是响应式
            let age = 18
            let phone = '12345678901'
            let dept = {
                id: 1,
                name: 'IT部'
            }
            function addAge(){
                age++
            }
            function shouTest(){
                alert(phone)
                console.log(phone)
            }
            return {a:name,b:age,phone,dept,addAge,shouTest}// 如果setup没有返回,是拿不到数据的,这个返回方式可以a:name方式,也可以phone方式
        }
}

和vue2语法混用的一些注意点。

  1. vue3的setup可以使用vue2语法中的data()中的内容。

引入ref

需要响应式的数据用ref(data)包起来。

基本类型数据,以及对象类型(底层还是使用到了reative)

import {ref} from 'vue'
let test = "test"//非响应式
let name = ref("张三")
let age = ref(18)

引入reative

只能定义 对象类型 数组类型 数据

import { reactive } from 'vue'
let car=reactive({name:'benci',price=100})
function carPriceChange(){
    car.price++;
}
let game=['ys','ww','aa']
function changeGame(){
    game[1]='bb'
}

ref定义对象类型

let obj = ref({name:'wyl',age:25})
//使用方式有点特殊
//obj是一个ref对象,不是里面包含的对象
function changeName(){
    obj.value.name = "gw"
}

对象复制

Object.assign(obj1,obj2,obj3,...)
//将obj3里面的key,value复制到obj2,又将obj2中的key,value复制到obj1
let person = reactive({
        name: '张三',
        age: 18,
        sex: '男'
    })
let { name, age, sex } = toRefs(person)  //直接可以用name,age,sex
let name1 = toRef(person, 'name')   //直接可以用name1 并且name1和person中的name是完全一致并且具有响应式功能的

toRefs

用于结构对象,从对象中提取值

toRef

Vue3只能监视以下4种数据

  • ref定义的数据
  • reactive定义的数据
  • 函数返回的一个值(getter函数)
  • 一个包含上述内容的数组

场景

  1. 监视ref基本类型
import { ref,watch } from 'vue'
let sum = ref(0)
function changeSum() {
    sum.value = sum.value + 1
}
let stopWatch = watch(sum, (newValue, oldValue) => {
    console.log(oldValue, newValue)
    if (newValue > 10) {
        stopWatch() //停止监视
    }
})

​ 2.ref监视 对象类型数据

let person = ref({
    name: '张三',
    age: 18
})
function ageAdd() {
	person.value.age++
}
function changePerson() {
	person.value = {
		name: '李四',
		age: 20
	}
}
watch(person, (newValue, oldValue) => { //这里监视的是对象的地址值的变化,整个对象都发生了变化
    console.log(oldValue, newValue)
    console.log('person被修改了')
})

watch(person, (newValue, oldValue) => {
   console.log(oldValue, newValue)
   console.log('person被修改了')
},{deep: true})//深度监视,对象里面的m 的属性被修改了也监视
//还有一个选项immediate: true,立即执行一次,界面加载就执行

3.reative类型数据

let person = reactive({
    name: '张三',
    age: 18
})
function ageAdd() {
    person.age++
}
function changePerson() {
    person = Object.assign(person,{name:'王五',age:20})
}
watch(person,(newPerson,oldPerson)=>{ //reactive默认开启深度监视
   	console.log('new',newPerson)
    console.log('old',oldPerson)
})

watchEffect

​ 加载就直接运行,不需要指明监视的数据。

let height = ref(0)
let temp = ref(0)
function addHeight() {
    height.value+=10
}
function addTemp() {
    temp.value+=10
}
watchEffect(()=>{
    if(height.value>=80||temp.value>=60){
        console.log('发送请求')
    }
})

props作用

用于父组件给子组件传递参数。

1.只接收不做限制

​ 父可以传一堆给子,子可以不要

//父
<Person a=1 :list="personList" />
let personList:Array<PersonInter> = [
    {id:"001", name: '张三', age: 18},
    {id:"002",name: '李四', age: 20},
    {id:"003",name: '王五', age: 22}
]
//子
import {defineProps} from 'vue'
let x = defineProps(['a','list','b']) //这里子接收了父没有传的b,子拿到的是无。
let persons = x.list//这种方式就能拿到父传过来的数据进行一些业务操作 
1.做了类型限制。
//父
<Person :a="a" :list="personList" />
let a = 1;
let personList:Array<PersonInter> = [
    {id:"001", name: '张三', age: 18},
    {id:"002",name: '李四', age: 20},
    {id:"003",name: '王五', age: 22}
]
//子
import { withDefaults } from 'vue'
import {type PersonInter} from '@/types'
defineProps<{list:Array<PersonInter>,a:number,b?:string}>() //加了?父可以不传 

//如果父没传,子需要默认值
withDefaults(defineProps<{list:Array<PersonInter>,a:number,b?:string}>(),{
	b:'123'
})

生命周期函数

  • 创建
  • 挂载
  • 更新
  • 销毁

vue2:

  • 创建前:beforeCreate
  • 创建完成:created
  • 挂载前:beforeMounte
  • 挂载完成:mounted
  • 更新前:beforeUpdate
  • 更新完成:updated
  • 销毁前:beforeDestroy
  • 销毁完成:destroyed

vue3:

  • 创建阶段只有:setup()
import { ref,onBeforeMount,onMounted,
        onBeforeUpdate,onUpdated,
       onBeforeUnmount,onUnmounted,} from 'vue'
onBeforeMount(() => {
    console.log('onBeforeMount')
})
onMounted(() => {
    console.log('onMounted')
})
onBeforeUpdate(() => {
    console.log('onBeforeUpdate')
})
onUpdated(() => {
    console.log('onUpdated')
})
onBeforeUnmount(() => {
    console.log('onBeforeUnmount')
})
onUnmounted(() => {
    console.log('onUnmounted')
})

父子组件生命周期

子先挂载

子先销毁

hooks,模块编程

在目录下创建一个hooks文件夹,里面存放各种useXxx.ts

各种模块需要暴露模块自身需要暴露的数据和方法

import {ref} from 'vue'

export default function () {
    let count = ref(0);
    function add() {
        count.value++;
    }
    return {count,add}
}
//在需要引入的地方引入
<script setup lang="ts">
    import useSum from '@/hooks/useSum'
    const { count, add } = useSum()
</script>

概念

1.一组key-value的对应关系

2.路由个数多,需要路由器去管理路由。

流程

  • 导航区、展示区
  • 路由器
  • 配置规则
  • 组件

路由安装

npm i vue-router

路由使用

  1. 根文件夹下创建router文件夹。
  2. 创建index.ts文件。
  3. 在index.ts文件编写路由规则。
  4. 在main.ts中引入路由使用路由。
  5. 在App.vue中写路由标签。

index.ts

//创建一个路由器并暴露

// 引入路由组件
import { createRouter,createWebHistory } from 'vue-router'

// 引入组件
import Home from '@/components/Home.vue'
import About from '@/components/About.vue'
import PersonManage from '@/components/PersonManage.vue'

// 创建路由
const router = createRouter({
    history:createWebHistory(), // 路由模式
    routes:[ // 路由规则
        {
            path:'/',
            component:Home
        },
        {
            path:'/pm',
            component:PersonManage
        },
        {
            path:'/about',
            component:About
        }
    ]
})

// 暴露路由
export default router

main.ts

//引入createApp用于创建应用
import { createApp } from "vue";
//引入App.vue根组件
import App from "./App.vue";
//引入路由器
import router from "./router";

//创建应用
const app = createApp(App);
//app.use(router);
app.use(router);
//挂载应用
app.mount("#app");

App.vue

<template>
    <div class="div1">
        <h1>Vue路由测试</h1>
        <!--导航区-->
        <div class="nav-class">
            <RouterLink to="/" >首页</RouterLink>|
            <RouterLink to="/pm" >人员管理</RouterLink>|
            <RouterLink to="/about" >关于我们</RouterLink>|//或者 :to={path:"/about"}
        </div>
        
        <!-- 内容区-->
        <div class="content-class">
            <RouterView/>
        </div>
    </div>
</template>

<script lang="ts">
    export default {
        name: 'App', // 组件名
        components: {  }
    }
</script>
<script setup lang="ts"> 
    import { RouterView } from 'vue-router';
</script>

<style scoped>
    .nav-class{
        background-color: rgb(241, 225, 225);
        padding: 10px;
        text-align: center;
    }
    .content-class{
        background-color: rgb(241, 223, 223);
        padding: 10px;
        margin-top: 10px;
        height: 500px;
        text-align: center;
    }
    .div1{
        width: 500px;
        margin-left: 30%;
        height: 100%;
        background-color: aliceblue;
    }
    a{
        text-align: center;
    }
    h1 {
        color: rgb(201, 79, 79);
        background-color: antiquewhite;
        text-align: center;
    }
</style>

路由的好处:

​ 在当前界面只会有当前路由组件,其他组件会卸载。

history

  1. vue2: mode:'history'
  2. vue3: history:createWebHistory()
  3. React: BrowserRouter

url更美观没有#。缺点:后期项目上线,需要配合服务端处理路径问题

hash

  1. vue2: mode:'hash'
  2. vue3: history:createWebHashHistory()
  3. React: BrowserHashRouter

兼容性好,有#号

路由名称

routers:{
	{
        path:'/',
        name:'home',
        component:Home
    }
}
{
    path:'/pm',
    component:PersonManage,
    children:[
          {
               path:'/pmcontent',
               component:PmContent
     	  }
     ]
}

路由传参

query

<RouterLink :to="`/pm/content?name=${item.name}&id=${item.id}`">{{item.name}}</RouterLink>
//或者
<RouterLink :to="{name:'PmContent',query:{name:item.name,id:item.id}}">{{item.name}}</RouterLink>
<script setup lang="ts">
    import { useRoute } from 'vue-router';
    let route = useRoute();
    let name = route.query.name
</script>

params

<RouterLink :to="`/pm/content/1/test`">{{item.name}}</RouterLink>

需要在路由器中的路由规则中配置

{
    path:'/pm',
    component:PersonManage,
    children:[
        {
            path:'content/:id/:name',
            component:PmContent
        }
    ]
}
<script setup lang="ts">
    import { useRoute } from 'vue-router';
    const route = useRoute();
</script>
children:[
    {
    	path:'content/:name/:id',
        component:PmContent,
        props:true
    }
]
<script setup lang="ts">
    import { useRoute } from 'vue-router';
    const route = useRoute();
    defineProps(['name','id'])
</script>

push

一个栈

replace

不做记录。不会返回上一页

默认push,修改成replace

<RouterLink replace to="/" >首页</RouterLink>|

方式

​ 场景,在代码中主动跳转到另一个路由。

<script setup lang="ts"> 
    import { useRouter } from 'vue-router';
    const router = useRouter();//拿到路由器 注意不是路由useRoute
    onMounted(()=>{
        setTimeout(()=>{
            console.log('@')
            router.push('/pm') //主动跳转
        },2000)
    })
</script>

重定向

在路由器中配置规则

{
    path:'/',
    redirect:'/home'主界面重定向到/home路由对应的规则
}

Pinia

集中式状态(数据)管理 | redux vuex pinia

  • 新建一个store文件夹
  • 创建vue组件对应的.ts
  • 引入和创建

main.ts

//引入createApp用于创建应用
import { createApp } from "vue";
//引入App.vue根组件
import App from "./App.vue";
//引入pinia
import { createPinia } from "pinia";
//创建应用
const app = createApp(App);
//app.use(router);
app.use(createPinia());
//挂载应用
app.mount("#app");

count.ts

import { defineStore } from "pinia";

export const useCountStore = defineStore("count", {
  state: () => {//状态 存储数据的容器
    return {
      count: 0,
    };
  },
  getters: { //计算属性
    int: (state) => state.count * 2,
  },
  actions: { //方法
    increment() {
      this.count++;
    },
  },
});

使用

<script setup lang="ts"> 
	import {useCountStore} from '@/store/count'
    const countStore = useCountStore()
</script>

组件之间相互传递数据。

props

  • 父传子
//父组件
let car = ref('奔驰')
<Child :car="car"/>

//子组件
defineProps(['car']);
  • 子传父
//父亲声明一个带参的方法,子组件调用
function recieveFun(value:string){
	console.log('父接收到子数据:',value)
}
<Child :recieveFunc="recieveFun"/>

//子调用传递
let toy = ref('奥特曼')
let props = defineProps(['recieveFun']);
//调用
<button @clike="recieveFun(toy)">调用传递</button>

自定义事件

子传父

  • 概念
<button @clike="test(1,$event)">调用传递</button>   //$event 事件对象

function test(x:number,y:Event){
    console.log(x,y)
}
  • 示例
//父组件定义函数,并且给子组件绑定事件。
<Child @haha="func"/>
function func(value:number){
    
}

//子组件需要声明事件。触发事件,触发事件之后就可以给父组件传递参数
const emit = defineEmits(['haha'])
<button @click="emit('haha',666)" >点击</button>

mitt

订阅发布。任意组件通信

//引入mitter
import mitt from 'mitt'
const emitter = mitt();
export default emitter
//定义事件
emitter.on("emit1",(value)=>{
	console.log(value)
})

//触发事件
emitter.emit("emit1(123)")

//解绑事件
emitter.off("emit1")

//全部解绑
emitter.all.clear()

v-model

自定义组件进行的 v-model

本质是v-bind+@input

<myInput v-model="username"/>

但是自定义的组件myInput处理起来就很麻烦。这里不做示例了。

$attrs

​ 用于实现当前组件的父组件,向当前组件的子组件通信(祖->孙)

父亲通过props传递的参数如果子组件没有使用defineProps声明,传递过来的参数会放在$attrs中。

  • 向下传
//父
let a = ref(0)
let b = ref(2)
let c = ref(3)
<Child :a="a" :b="b" :c="c"/>
    
//子
//defineProps(['a','b'])//直接不接收
//直接把父数据给到孙辈
<GrandChild v-bind="$attrs"/>
    
//孙
defineProps(['a','b','c'])

//甚至重孙
  • 向上传
//父定义方法。

//后辈调用方法

$ref和$parent

$ref 父传子

$ref 子传父

provide_inject

祖孙之间直接通信,不需要中间。

  • 向后代提供数据
//祖
import {provide} from 'vue'
let a = ref(10)
let car = reactive({brand:'本次',price:120})
provide("a",a)
provide("car",car)

//孙
let x = inject('a',0) //这里的0为默认值,如果没有接收到a那么x=0
let car = inject('car',{brand:'未知',price:0}) //设置默认值,防止推断报错

  • 后代向上层传递数据
//祖
import {provide} from 'vue'
let a = ref(10)
let car = reactive({brand:'本次',price:120})
function a_add(value){
    a -= value
}
provide("a_context",{a,a_add}) //a的上下文,这个上下文饱含a和一个a_add函数
provide("car",car)


//孙
lex {a,a_add} = inject("a_context",{a:0,a_add:(value:number)=>{} })
a_add(6)

默认插槽

<Child >
	<ui>
    	<li></li>
    </ui>
</Child>

<template>
	<div>
		<slot>默认内容</slot>  //如果这里父没有插槽内容,子会默认
	</div>
</template>

有名插槽

<Child  >
    <template v-slot:s1> //插槽1 或者 #s1
    	<h2></h2>
    </template>
    <template v-slot:s2> //插槽2 或者 #s2
		<ui>
    		<li>111</li>
    	</ui>
	</template>
</Child>

<template>
	<div>
        <slot name="s1">默认内容</slot>
		<slot name="s2">默认内容</slot>  //如果这里父没有插槽内容,子会默认
	</div>
</template>

作用域插槽

子组件需要给父组件传递参数

<Child  >
    <template v-slot="params"> 
    	<h2></h2>
    </template>
</Child>

默认暴露

  • 定义:默认暴露是指一个模块只能有一个默认导出,使用 export default 关键字来导出一个变量、函数或类。导入时可以使用任意名称来接收这个默认导出。

  • 适用场景:当一个模块只需要暴露一个接口时,使用默认暴露可以简化导入过程,增强代码的可读性和可维护性。

分别暴露

  • 定义:分别暴露是指一个模块可以导出多个变量、函数或类,使用 export 关键字。导入时需要使用 {} 来指定要导入的具体内容。

  • 适用场景:当一个模块需要暴露多个接口时,分别暴露可以清晰地列出所有可用的导出,便于管理和使用。

Vue project

项目目录

新建的项目目录缺少很多必要的目录和文件

要在src文件夹补充以下文件夹和文件

  • router文件夹,配置路由信息。
  • store文件夹,pinia和vuex相关内容
  • utils文件夹,工具类比如axios的二次封装request
  • components文件夹,一些公共的组件,比如文件上传,icon选择等。
  • assets文件夹,存放矢量文件svg和其他静态文件。
  • api文件夹,存放具体请求的方法,对外暴露。
  • views文件夹,最重要的文件夹,所有的路由组件都放在这个地方。
  • directive文件夹,存放自定义指令用来判断权限对菜单的展示是否隐藏
  • permission.ts文件,在src目录下与main.ts同级。用于判断是否登录,做路由守卫。

要做根文件夹添加开发和测试生产环境相关信息

  "scripts": {
    "dev": "vite",
    "build": "run-p type-check \"build-only {@}\" --",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build"
  }

.env.development文件

# 页面标题
VUE_APP_TITLE = demo1

# 开发环境配置
ENV = 'development'

# 
VUE_APP_BASE_API = '/dev-api'

# 路由懒加载
VUE_CLI_BABEL_TRANSPILE_MODULES = true

.env.production文件

# 页面标题
VUE_APP_TITLE = demo1

# 开发环境配置
ENV = 'production'

# 
VUE_APP_BASE_API = '/api'

步骤1

npm create vue@latest

步骤2

配置项目相关内容

创建一个空白的项目

vue-project-1

vue-project-2

cd demo01
npm install
npm run dev

vscode安装Vue 3 Snippets

安装

npm install element-plus --save
yarm add element-plus
pnpm install element-plus

项目引入

在main.ts引入

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)

使用

<template>
  <div>
    <h1>Home</h1>
    <div class="button-row">
      <el-button>Default</el-button>
      <el-button type="primary">Primary</el-button>
      <el-button type="success">Success</el-button>
      <el-button type="info">Info</el-button>
      <el-button type="warning">Warning</el-button>
      <el-button type="danger">Danger</el-button>
    </div>
  </div>
</template>

安装

npm i -D vite-plugin-windicss windicss

配置404界面

{ 
    path: '/:pathMatch(.*)*',
    name: '404', 
    component: () => import('@/views/404/index.vue') 
}
npm install @element-plus/icons-vue

登录界面代码

<template>
    <div class="container">
        <el-col :sm="12" :lg="6" :xl="4">
            <el-form :model="form" :rules="rules" ref="formRef" label-width="120px"><!--ref="formRef"中formRef表示el-form为整个对象-->
                <el-form-item label="Username" prop="username">
                    <el-input v-model="form.username" placeholder="Enter username"></el-input>
                </el-form-item>
                <el-form-item label="Password" prop="password">
                    <el-input v-model="form.password" placeholder="Enter password" type="password"></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submitForm">Login</el-button>
                </el-form-item>
            </el-form>
        </el-col>
    </div>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { reactive } from 'vue'
import { ref } from 'vue'
const formRef = ref()

const router = useRouter()
const form = reactive({
    username: '',
    password: ''
})
const rules = reactive({
    username: [
        { required: true, message: 'Please enter username', trigger: 'blur' }
    ],
    password: [
        { required: true, message: 'Please enter password', trigger: 'blur' }
    ]
})
const submitForm = async () => {
    await (formRef.value as any).validate()
    if (form.username === 'admin' && form.password === '123456') {
        router.push({ name: 'Home' })
    } else {
        ElMessage.error('Login failed')
    }
}
</script>
<style scoped>
.container {
    display: flex;
    justify-content: center;
    align-items: center;
}
</style>

  • 数据双向绑定v-model="form.username"和form数据对象中的username数据双向绑定
  • :model="form" 表示这个el-form和对象form绑定
  • 表单验证,:rules="rules"和ref="formRef",rules定义验证规则,formRef 的最终目的拿到了 el-form 的实例,调用它提供的 API 方法实现:validate() : 触发表单验证(检查必填项、格式等)。resetFields() : 重置表单项,将其值重置为初始值。clearValidate() : 移除表单项的校验结果。
const form = reactive({
    username: '',
    password: ''
})

<el-form :model="form" >
    <el-form-item label="Username" prop="username">
        <el-input v-model="form.username" placeholder="请输入账号"></el-input>
    </el-form-item>
    <el-form-item label="Password" prop="password">
         <el-input v-model="form.password" placeholder="请输入密码" type="password"></el-input>
    </el-form-item>
    <el-form-item>
         <el-button type="primary" @click="submitForm">登录</el-button>
    </el-form-item>
</el-form>

安装

npm install axios

封装

新建utils文件夹,新建axios.ts文件暴露request

import axios from 'axios'

const request = axios.create({
    baseURL: "http://localhost/proj1",
    timeout: 5000
})
export default request

配置跨域

在项目根目录下的vite.config.ts文件中新增配置

  server: {
    proxy: {
      '/proj1': {
        target: 'http://localhost',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/proj1/, ''),
      },
    },
  },

同步等待

async function getUser(){
	try{
		await axios.get("/info?id="+id)
	}catch(err){
	
	}
}

使用

  • get
request.get("/test")
.then((result)->{ //这里已经有base-url了会拼接在一起
	console.log(result.data)//真正返回的结果
})
.catch((err)->{ //异常处理
    console.log(err)
})
  • get + param
//方式一
request.get("/test?id=5")
.then((result)->{ //这里已经有base-url了会拼接在一起
	console.log(result.data)//真正返回的结果
})
.catch((err)->{ //异常处理
    console.log(err)
})
//方式二
request.get("/test",{
	params:{id=5}
})
.then((result)->{ //这里已经有base-url了会拼接在一起
	console.log(result.data)//真正返回的结果
})
.catch((err)->{ //异常处理
    console.log(err)
})
  • post->form

  • post->json

请求拦截

// 官方用例
// 1.添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

响应拦截

// 2.添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

前置守卫

  1. 在src文件夹新建permission.ts文件
  2. 在main.ts中引入这个文件
import { ElMessage } from "element-plus";
import router from "@/router/routes";

router.beforeEach((to, from, next) => {
  if (to.name !== 'Login' && !localStorage.getItem('token')) {
    // next({ name: 'Login' })
    next({path: '/login'})
    ElMessage({
      message: '请先登录',
      type: 'warning',
    })
  }
  //防止重复登录
  if(to.name === 'Login' && localStorage.getItem('token')){
    ElMessage({
      message: '您已登录,无需重复登录',
      type: 'warning',
    })
    next({path: '/'})
  }
  next()
})
import './permission'
//回车事件
function onKeyDown(e){
    if(e.key === 'Enter'){
        submitForm()
    }
}
// 组件挂载时添加事件监听
onMounted(() => {
    document.addEventListener('keydown', onKeyDown)
})
// 组件卸载时移除事件监听
onBeforeUnmount(() => {
    document.removeEventListener('keydown', onKeyDown)
})

自定义指令

  1. 根目录下创建directives。

  2. directives目录创建permission.ts文件。

    export default{
    	install(app){//传入app
    		app.directive('permission', {
                mounted(el,binding){
                    console.log(el,binding)
                    //拿到值
                    binding.value
                    for(let item of binding.value){
                		console.log(item)
                		if(item !== 'admin'){//没有权限移除掉
                    		el.parentNode.removeChild(el)
                		}
            		}
                }
    		})
    	}
    }
    
  3. 在需要的DOM元素中添加指令

    <template>
      <p v-permission="['admin']">This sentence is important!</p>
    </template>
    

表格

<el-table :data="tableData" border >
  <el-table-column prop="name" label="姓名" width="100px"/>
  <el-table-column prop="email" label="邮箱"  />
  <el-table-column prop="phone" label="手机号"  />
  <el-table-column prop="role" label="角色" />
</el-table>

分页

<el-pagination
  v-model:current-page="currentPage"
  v-model:page-size="pageSize"
  :page-sizes="[10, 20, 30, 40]"
  :size="size"
  :disabled="disabled"
  :background="background"
  layout=" prev, pager, next,jumper, ->,total, sizes"
  :total="total"
  @size-change="handleSizeChange"
  @current-change="handleCurrentChange"
/>

<script setup lang="ts">
import { ref,reactive } from 'vue'

const currentPage = ref(1) //当前页码
const pageSize = ref(10) //每页显示条数
const size = ref('') //分页器尺寸大小
const disabled = ref(false) //是否禁用分页器
const background = ref(true) //是否显示背景颜色
const total = ref(400) //总条数

const tableData = reactive([
  {
    name: '张三',
    email: 'zhangsan@example.com',
    phone: '13800000000',
    role: '管理员'
  },
  {
    name: '李四',
    email: 'lisi@example.com',
    phone: '13800000001',
    role: '普通用户'
  }
])
</script>

Dialog

1

第一层App.vue中的

第二层layout.vue

看routes中layout下面的子路由,children这才是正儿八经的第二层路由。路由嵌套的正确执行方式。

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'layout',
      component: () => import('@/layout/index.vue'),
      redirect: '/home', // 访问根目录时默认重定向到 home
      children: [
        {
          path: 'home',
          name: 'home',
          component: () => import('@/views/home/index.vue'),
        }
      ]
    },
    { 
      path: '/:pathMatch(.*)*',
      name: '404', 
      component: () => import('@/views/404/index.vue') 
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('@/views/login/index.vue'),
    },
    {
      path: '/register',
      name: 'register',
      component: () => import('@/views/register/index.vue'),
    },
  ],
})

代码

app.vue

<script setup lang="ts">

</script>

<template>
  <router-view></router-view>
</template>

<style scoped></style>

layout/index.vue

<template>
  <div class="app-wrapper">
    <el-container class="layout-container">
      <!-- 左侧边栏 -->
      <el-aside width="220px" class="sidebar-container">
        <div class="logo">
          <h2>SaaS RBAC Admin</h2>
        </div>
        <el-menu
          default-active="/home"
          class="el-menu-vertical"
          background-color="#304156"
          text-color="#bfcbd9"
          active-text-color="#409EFF"
          router
        >
          <el-menu-item index="/home">
            <el-icon><House /></el-icon>
            <span>首页</span>
          </el-menu-item>
          <el-menu-item index="/user">
            <el-icon><User /></el-icon>
            <span>用户管理</span>
          </el-menu-item>
          <el-menu-item index="/role">
            <el-icon><Setting /></el-icon>
            <span>角色管理</span>
          </el-menu-item>
        </el-menu>
      </el-aside>

      <!-- 右侧主体内容 -->
      <el-container class="main-container">
        <!-- 顶栏 -->
        <el-header class="header">
          <div class="header-left">
            <!-- 占位,可放折叠侧边栏按钮或面包屑 -->
            <span>后台管理系统</span>
          </div>
          <div class="header-right">
            <!-- 占位,可放用户头像、下拉菜单等 -->
            <el-dropdown>
              <span class="el-dropdown-link">
                管理员 <el-icon class="el-icon--right"><arrow-down /></el-icon>
              </span>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item>个人中心</el-dropdown-item>
                  <el-dropdown-item divided>退出登录</el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
          </div>
        </el-header>

        <!-- 内容展示区 -->
        <el-main class="app-main">
          <!-- 针对layout的子路由嵌套 -->
          <router-view />
        </el-main>
      </el-container>
    </el-container>
  </div>
</template>

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
app.use(ElementPlus)
// 注册 Element Plus 图标组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}

在src同级目录下新增.env.development和.env.production以及.env.test

在package.json中添加

"scripts": {
    "dev": "vite --mode development",
    "build": "run-p type-check \"build-only {@}\" --",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build",
    "build:prod": "vite build --mode production",
    "build:test": "vite build --mode test"
}

这里的文件名可以改短,.env.dev和.env.prod,只需要在package.json中修改配置即可

"scripts": {
    "dev": "vite --mode dev",
    "build": "run-p type-check \"build-only {@}\" --",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --build",
    "build:prod": "vite build --mode prod",
    "build:test": "vite build --mode test"
}
# 开发环境配置 必须以VITE_开头
NODE_ENV = development
VITE_APP_TITLE = 管理系统
VITE_API_URL = /
VITE_SERVER_URL = http://localhost:8080/proj1

使用环境变量中内容

const appTitle = import.meta.env.VITE_APP_TITLE
console.log(appTitle)

用来模拟后端借口,有后端了,不需要使用mock

在src文件夹同级创建mock

  • 在src目录下创建stores文件夹。

  • 创建index.ts文件用于创建pinio

index.ts

import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

main.ts引用

// 注册 Pinia
import pinia from './stores/index.ts'
app.use(pinia)
  • 创建各种store

useUserStore 一般以use开头Store结尾

import { defineStore } from 'pinia' //引入

export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: {}
  }),
  actions: {
    setToken(token: string) {
      this.token = token
    },
    setUserInfo(userInfo: any) {
      this.userInfo = userInfo
    }
  }
})

前端路由权限控制方案一般有两种

  • 动态路由
  • 全量路由 +后端权限

动态路由

如果对代码体积功能保密性要求高,也可以采用“动态添加路由”方案:

  • 前端只保留登录页、404 等静态路由
  • 登录后,根据权限,向后端请求当前用户的路由配置(路由路径、组件路径、权限等)。
  • 前端拿到后,动态解析并调用 router.addRoute() 添加。
  • 菜单也根据这个动态数据直接生成。

全量路由

  • 加载路由:前端初始化时,就加载定义好的所有路由(静态路由+所有动态业务路由)。
  • 获取权限:用户登录后,立即请求后端接口,获取其权限列表,比如 ['user:add', 'order:view', 'report:export']
  • 动态生成菜单:前端根据权限列表,递归过滤路由配置,只留下有权限的路由来渲染侧边栏菜单。
  • 注册全部路由:关键在于,所有路由其实都已经注册到路由实例中了。这才能让你在路由守卫里判断权限时,直接匹配到目标路由并检查其所需权限。
  • 路由守卫拦截:在每次跳转前,检查目标路由是否需要特定权限。如果需要,就验证用户是否有该权限;如果没有,就跳转到 403 页面。

方式一:直接在路由前置拦截中判断菜单是否有权限

在permission.ts中添加内容,

import { ElMessage } from "element-plus";
import router from "@/router/routes";
router.beforeEach((to, from, next) => {
  // 从 localStorage 中获取菜单
  const menu = localStorage.getItem('menu')||'[]'
  if (!menu&&to.name!=='login') {
    ElMessage({
      message: '请先登录',
      type: 'warning',
    })
    return next({ path: '/login' })
  }

  const menuList = JSON.parse(menu)
  // 遍历菜单,判断是否有权限访问当前路由
  let hasPermission = false
  menuList.forEach((item:string) => {
    if (item===to.path) {
      hasPermission = true
    }
  })
  console.log(hasPermission)
  if (!hasPermission&&to.name!=='login') {
    ElMessage({
      message: '您没有权限访问该页面',
      type: 'warning',
    })
    return next(false)
  }
  // 正常放行
  next()
})
router.afterEach((to, from) => {
  console.log(to, from)
})

方式二:自定义指令

Git

编辑etc\gitconfig文件,也有些windows系统是存放在C:\Users\Administrator\.gitconfig路径或安装盘符:\Git\mingw64\etc\gitconfig,在文件末尾增加以下内容:

[gui]  
    encoding = utf-8  
    # 代码库统一使用utf-8  
[i18n]  
    commitencoding = utf-8  
    # log编码  
[svn]  
    pathnameencoding = utf-8  
    # 支持中文路径  
[core]
    quotepath = false 
    # status引用路径不再是八进制(反过来说就是允许显示中文了)

清空暂存区

  • 将已经git add的文件变成没有add
git reset

清空文件

  • 将没有git add的清空
git clean -fd  # 删除所有未跟踪的文件和目录

二者结合就能删除某次提交

SSH秘钥

  1. 在 Ubuntu 上生成 SSH 密钥对

-t 指定加密算法,-C 添加注释(通常填邮箱)

ssh-keygen -t ed25519 -C "你的邮箱@example.com" -f ~/.ssh/id_ed25519




执行命令后,一路按回车即可(**注意**:建议不设置 passphrase,否则每次使用 SSH 仍需输入密码)。这会在 `~/.ssh/` 目录下生成 `id_ed25519`(私钥,绝不能泄露)和 `id_ed25519.pub`(公钥)两个文件。

2. **将公钥添加到你的 Git 服务器**:以 GitHub 为例,登录后进入 **Settings** -> **SSH and GPG keys**,点击 **New SSH key**,将 `~/.ssh/id_ed25519.pub` 文件中的内容完整粘贴进去并保存。

3. **修改远程仓库 URL**:这是最关键的一步。你需要将仓库的远程 URL 从 HTTPS 格式切换为 SSH 格式。

bash

```bash
# 查看当前 URL
git remote -v
# 如果显示是 https://... 开头,则需修改
git remote set-url origin git@gitee.com:wei-yuliu/c-language-learning.git

之后执行 git pull 等操作就会自动使用 SSH 密钥进行认证,无需再输入密码。

直接保存账号密码token

# 默认缓存 15 分钟
git config --global credential.helper cache
# 自定义缓存时间,例如 1 小时 (3600 秒)
git config --global credential.helper 'cache --timeout=3600'
#永久存储:凭证会永久保存到磁盘上的一个明文文件 (~/.git-credentials) 中
git config --global credential.helper store

其他

处理历史提交中的大文件导致推送失败

适用场景:本地历史中曾提交过大文件(超过平台限制),后续虽然有新提交,但历史记录仍携带这些文件,导致推送被拒绝。需要从所有历史中彻底移除这些大文件,同时保留后续的正常提交。

所需工具git-filter-repo(安装:sudo apt install git-filter-repopip install git-filter-repo


第一步:重写历史,删除指定的大文件/目录

git filter-repo --path path/to/large-file --path path/to/large-dir/ --invert-paths --force
  • --path:指定要保留的路径(可重复使用)。
  • --invert-paths:反转含义,即排除这些路径,从所有历史提交中删除它们。
  • --force:当本地仓库不是全新克隆时,强制重写历史。

示例:若需要删除根目录下的 large.zipbin/ 整个目录,则写为 --path large.zip --path bin/ --invert-paths


第二步:清理无用对象,使删除真正生效

git reflog expire --expire=now --all
git gc --prune=now --aggressive

作用:重写历史后,旧的大文件对象仍残留在 .git/objects 中,并被 reflog 引用。这两条命令强制清空所有 reflog 记录,并立即修剪不可达的对象,物理删除这些大文件,从而真正缩小仓库体积。若不执行此步,推送时仍会尝试上传已删除的大文件,导致失败。


第三步:检查并重新添加远程仓库(若丢失)

git remote -v                     # 查看现有远程配置
git remote add origin <仓库地址>   # 若缺失则重新添加
git remote -v                     # 确认

注:filter-repo 可能会清除远程配置,必要时重新设置。


第四步:强制推送重写后的分支

git push origin <分支名> --force

使用 --force 是因为本地历史已被重写,需要覆盖远程分支。


注意事项

  • 操作前建议备份 .git 目录(cp -r .git /tmp/backup.git)。
  • 强制推送会改变远程历史,若多人协作,需提前通知团队成员。
  • 如果还有其他大文件,可在第一步中一并列出。

分支影响:默认重写全仓库历史(所有分支和标签)。若只想测试特定分支,可加 --refs <分支名>,但这样无法彻底回收大文件空间,后续合并时易复发。建议在操作前备份全仓库,并统一处理所有分支。

Godot

Java

Ai

ChatClient和ChatModel

ChatModel常用的消息发到LLM,LLM返回

ChatClient,基于ChatModel构建,支持prompt,格式输出,参数交互,聊天记忆,工具调用,RAG

ChatClient

不支持自动注入,必须手动注入

@Bean
public ChatClient chatClient(ChatModel chatModel) {
    return ChatClient.builder(chatModel).build();
}

或者

@RestController
public class ChatClientController {
    private final ChatClient chatClient;

    public ChatClientController(ChatModel chatModel){
        this.chatClient = ChatClient.builder(chatModel).build();
    }
    @GetMapping("/chat/client")
    public Flux<String> chat2(@RequestParam(value = "message") String message) {
        return chatClient.prompt().user(message).stream().content();
    }
}

ollama本地部署

下载ollama

https://ollama.com

直接下载按照,ollama网站选模型就可以了用的是

ollama run qwen3.5:4b

ollama的指令和docker差不多,可以借鉴一下docker

run之后直接就能用了,ollama提供了一个对话界面窗口。

提示词(prompt)

System,User

CI CD jenkins

前端配置

pipeline {
    agent any
    environment {
        GIT_REPO = "http://YH:YH123456@gitlab.yuhecloud.com/tool/jiangqie_ow_free.git"
        GIT_CREDENTIALS = credentials('YHGIT')
        SSH_PASSWORD = 'yh608217yh'
        REMOTE_HOST = '192.168.120.253'
        REMOTE_DIR = '/usr/local/newtest/'
    }

    stages {
        stage('Clean Workspace') {
            steps {
                cleanWs()
            }
        }

        stage('Checkout Code') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: 'dev']],
                    userRemoteConfigs: [[
                        url: GIT_REPO,
                        credentialsId: 'YHGIT'
                    ]]
                ])
            }
        }

        stage('Build Java Project') {
            steps {
                dir('server'){
                    sh 'mvn clean install package'
                }
            }
        }

        stage('Archive Artifacts') {
            steps {
                // 归档所有需要的构建产物
                archiveArtifacts artifacts: 'server/target/*.jar', fingerprint: true
            }
        }

        stage('Deploy Applications') {
            steps {
                // 部署第一个应用
                script {
                    def infraJar = 'server/target/*.jar'
                    sh """
                        sshpass -p '${SSH_PASSWORD}' scp -o StrictHostKeyChecking=no ${infraJar} root@${REMOTE_HOST}:${REMOTE_DIR}
                        sshpass -p '${SSH_PASSWORD}' ssh -o StrictHostKeyChecking=no root@${REMOTE_HOST} 'cd /usr/local/ && ./jiangqie_deploy.sh'
                    """
                }
                // 可以继续添加更多应用的部署
            }
        }
    }

    post {
        success {
            echo 'Build and deployment succeeded!'
        }
        failure {
            echo 'Build or deployment failed!'
        }
    }
}

安装

wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt-get update
sudo apt-get install jenkins

jenkis默认安装路径:/usr/lib/jenkins/:jenkins安装目录,war包会放在这里。

/etc/sysconfig/jenkins:jenkins配置文件,“端口”,“JENKINS_HOME”等都可以在这里配

/var/lib/jenkins/:默认的JENKINS_HOME。

/var/log/jenkins/jenkins.log:jenkins日志文件。

jenkins默认拉取git代码存放路径:/var/lib/jenkins/workspace

安装2

直接java -jar jenkins.war
然后所有文件都在%user%/.jenkins目录。linux和windows都是

nginx外部映射:

server{
	listen 80;
	server_name jenkins.openso.top;
	# 精确匹配 /docker 路径
	location / {
		# 反向代理到内网 Jenkins
		proxy_pass http://10.147.17.85:8080;  
		# WebSocket 支持 - 用于实时日志输出
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        # 超时设置
        proxy_connect_timeout 90;
        proxy_send_timeout 90;
        proxy_read_timeout 90;
	}
}

+增加构建步骤

在配置项中增加构建步骤Build Steps

echo "切换到黑马商城项目"
cd ./hm-shopping/hmall
echo "开始进行编译构建"
mvn clean package
echo "编译构建完成"

echo "复制gateway模块到远程docker服务器(10.147.17.85)"
sshpass -p 20000316 \
scp -v -o StrictHostKeyChecking=no \
/var/lib/jenkins/workspace/hmall/hm-shopping/hmall/gateway/target/gateway-1.0.0.jar wyl@10.147.17.85:/home/wyl/docker/java_app/hmall/gateway.jar
echo "复制gateway模块成功"

echo "复制user模块到远程docker服务器(10.147.17.85)"
sshpass -p 20000316 \
scp -v -o StrictHostKeyChecking=no \
/var/lib/jenkins/workspace/hmall/hm-shopping/hmall/user-service/target/user-service-1.0.0.jar wyl@10.147.17.85:/home/wyl/docker/java_app/hmall/user-service.jar
echo "复制user模块成功"

echo "复制item模块到远程docker服务器(10.147.17.85)"
sshpass -p 20000316 \
scp -v -o StrictHostKeyChecking=no \
/var/lib/jenkins/workspace/hmall/hm-shopping/hmall/item-service/target/item-service-1.0.0.jar wyl@10.147.17.85:/home/wyl/docker/java_app/hmall/item-service.jar
echo "复制item模块成功"

echo "复制cart模块到远程docker服务器(10.147.17.85)"
sshpass -p 20000316 \
scp -v -o StrictHostKeyChecking=no \
/var/lib/jenkins/workspace/hmall/hm-shopping/hmall/cart-service/target/cart-service-1.0.0.jar wyl@10.147.17.85:/home/wyl/docker/java_app/hmall/cart-service.jar
echo "复制cart模块成功"

echo "远程执行Docker构建命令"
sshpass -p 20000316 \
ssh -o StrictHostKeyChecking=no wyl@10.147.17.85 \
"cd /home/wyl/docker/java_app/hmall && docker build -t gateway:1.0 ."
echo "Docker构建完成"

Exe4j

1

2

3

4

4-2

5

6

6-2

6-3

接下来就是一直下一步

JavaTCP编程 从入门到精通

Buffer 缓冲读写数据,内存中

ByteBuffer使用最多,其中有比较重要的概念和方法。

  • capacity(容量):Buffer的最大数据容量,创建时设置,不可改变
  • limit(限制):第一个不应该读取或写入的索引
  • position(位置):下一个要读取或写入的索引
  • mark(标记):一个备忘位置,通过mark()标记,reset()返回

Buffer的实现类

  • ByteBuffer
    • mappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

模式切换

假设buffer的capacity(容量)为10.

初始状态下

position=0    下一个写入位置
limit=10      可以写入最大位置
capacity=10   总容量
mark = -1     没有标记,标记用于拷贝之类的功能
写模式

可以通过put((byte)'A');方法写入一个字符。这时候

position=1 写入了一个指向1这个下标
limit=10 还能写9个
capacity = 10
mark = -1
读模式

执行flip()方法操作,将其他模式切换成读模式

limit = position;    // 将limit设为当前position(3)
position = 0;        // 将position重置为0
mark = -1;           // 清除标记
结果:
position = 0    ✓ 下一个读取位置
limit    = 1    ✓ 只能读到position=3的位置(之前写入的数据量)
capacity = 10   ✓ 总容量不变

Channel双向数据通道

​ 可以将数据读入buffer,也可以从buffer读取数据。

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

Selector

配合一个线程管理多个Channel上发生的事件。

同步阻塞IO

上下文切换过程:

  1. 用户态 → 内核态:调用 read() 系统调用
  2. 内核态:等待数据(进程挂起,不占用 CPU)
  3. 内核态:数据从网卡复制到内核缓冲区(DMA 直接内存访问)
  4. 内核态:数据从内核缓冲区复制到用户缓冲区
  5. 内核态 → 用户态:返回调用结果

特点:2 次上下文切换 + 2 次数据复制

用户进程              内核
   |                   |
   | 调用read()        |
   |----------------->|
   |                   |
   | 等待数据准备      |
   | (进程阻塞)        |--- 等待数据包到达 ---
   |                   |      ↓
   |                   | 数据到达网卡
   |                   |      ↓
   |                   | 数据复制到内核缓冲区
   |                   |      ↓
   | 数据复制完成      |
   |<-----------------|
   |                   |
   | 复制数据到用户空间|
   | (内核到用户复制)  |
   |                   |
   | 返回结果          |
   |<-----------------|

同步非阻塞IO

上下文切换过程:

  1. 用户态 → 内核态:发起系统调用
  2. 内核态 → 用户态:立即返回(无数据)
  3. 重复步骤 1-2 进行轮询(多次上下文切换)
  4. 用户态 → 内核态:数据就绪时调用
  5. 内核态:数据从内核复制到用户空间
  6. 内核态 → 用户态:返回结果

特点:多次上下文切换 + 2 次数据复制,CPU 消耗高

用户进程              内核
   |                   |
   | 调用recv()        |
   |----------------->|
   |                   |
   | 立即返回EWOULDBLOCK|
   |<-----------------|
   |                   |
   | 不断轮询recv()    |
   |----------------->|
   | 返回EWOULDBLOCK   |
   |<-----------------|
   |      ...          |
   | 第N次调用recv()   |
   |----------------->|
   |                   |
   | 数据已准备好      |
   | 开始复制到用户空间 |
   |<-----------------|
   | 复制完成          |
   |<-----------------|

多路复用

NIO+Selector对路复用器

用户进程              内核
   |                   |
   | 调用select()      |
   |----------------->|
   |                   |
   | 监视多个fd        |
   | (进程阻塞)        |--- 等待任意fd就绪 ---
   |                   |      ↓
   |                   | 有fd就绪(如socket1)
   |<-----------------|
   |                   |
   | 遍历就绪fd        |
   | 调用recv(socket1) |
   |----------------->|
   |                   |
   | 复制数据到用户空间 |
   |<-----------------|

上下文切换过程(以 select 为例):

应用程序               内核
   |                    |
   | select() 系统调用  | 1. 用户态→内核态
   |------------------>|
   |                    |
   | 遍历所有fd         | 2. 在内核遍历所有fd
   |                    |
   | 有fd就绪时返回     | 3. 内核态→用户态
   |<------------------|
   |                    |
   | 遍历就绪fd         | 4. 在用户空间遍历
   |                    |
   | 对每个就绪fd:      |
   |   recv()系统调用   | 5. 用户态→内核态
   |------------------>|
   |   数据复制         | 6. 内核到用户复制
   |<------------------| 7. 内核态→用户态

epoll 的改进:

应用程序               内核
   |                    |
   | epoll_create()     |
   |------------------>|
   |                    |
   | epoll_ctl(ADD)     | 添加fd到红黑树
   |------------------>|
   |                    |
   | epoll_wait()       | 1. 用户态→内核态
   |------------------>|
   |                    |
   | 等待就绪事件        | 2. 进程阻塞
   |                    |    ↓
   |                    | 事件就绪队列
   |<------------------| 3. 返回就绪事件
   |                    |    (只返回就绪的fd)
   |                    |
   | 处理就绪事件        |
   | recv(就绪fd)       | 4. 用户态→内核态
   |------------------>|
   |                    | 5. 数据复制
   |<------------------| 6. 内核态→用户态

特点

  • select/poll:每次调用都要传递所有 fd,内核遍历所有 fd
  • epoll:内核维护事件表,只返回就绪的 fd

异步阻塞IO(少用)

AIO,linux中对的异步实际上是假的AIO,底层实现还是多路复用模拟的AIO

在windows中实现了真正的异步AIO(IOCP)。

上下文切换:

类似同步阻塞,但操作发起是非阻塞的,等待完成是阻塞的。

用户进程              内核
   |                   |
   | 发起异步操作      |
   |----------------->|
   |                   |
   | 立即返回          |--- 异步处理 ---
   |<-----------------|      ↓
   |                   | 等待操作完成
   |                   |      ↓
   | 调用阻塞等待      | 完成时设置状态
   |----------------->|
   |                   |
   | 等待完成          | 阻塞等待信号
   |<-----------------|

异步非阻塞IO

数据流程(Linux AIO):

用户进程              内核
   |                   |
   | io_submit()       | 1. 用户态→内核态
   |----------------->|
   |                   |
   | 立即返回          | 2. 内核态→用户态
   |<-----------------|
   |                   |
   | 继续执行其他任务   |--- 内核异步处理 ---
   |                   |      ↓
   |                   | 等待数据到达
   |                   |      ↓
   |                   | 数据复制到用户缓冲区
   |                   |      ↓
   | 收到信号/回调     | 3. 内核发送信号
   |<-----------------|
   |                   |
   | 处理完成事件       |

Windows IOCP 流程:

应用程序              内核
   |                   |
   | WSARecv()         | 1. 提交异步请求
   |----------------->|
   |                   |
   | 立即返回          | 2. 返回正在处理
   |<-----------------|
   |                   |
   | 继续执行          |--- 内核处理 ---
   |                   |      ↓
   | GetQueuedCompletionStatus()|
   |----------------->| 3. 等待完成端口
   |                   |      ↓
   |                   | 操作完成时
   |                   | 数据已复制到用户缓冲区
   |<-----------------| 4. 返回完成状态

数据复制差异:

  • 同步 I/O:用户进程调用时,数据才从内核复制到用户空间
  • 异步 I/O:内核自动完成数据复制,完成后通知用户进程

信号驱动

不完善,基本不用

对比总结

模型用户态→内核态切换次数数据复制时机等待方式
同步阻塞1次(等待+复制)调用时复制进程阻塞
同步非阻塞多次轮询数据就绪时复制轮询
多路复用2次(select+recv)就绪后复制单线程阻塞等待多个
异步阻塞2次(提交+等待)完成时已复制阻塞等待完成
异步非阻塞1次(提交)内核自动复制回调/信号

数据复制过程

  1. DMA 复制:网卡数据 → 内核缓冲区(由 DMA 控制器完成,不占用 CPU)
  2. CPU 复制:内核缓冲区 → 用户缓冲区(需要 CPU 参与)

ServerSocketChannel.open()创建一个服务器的socket,并且建立管道Channel。这个方式跟我们一个开始使用new ServerSocket(8080);没有太大区别。

public class NioTcpDemo01 {
    public static void main(String[] args) throws Exception{
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        SocketChannel accept = ssc.accept();
        System.out.println(accept);
    }
}

⬇接着是接收客户端发来的消息,跟前面没有用Channel是一个性质的。

public class NioTcpDemo01 {
    public static void main(String[] args) throws Exception{
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        SocketChannel accept = ssc.accept();
        System.out.println(accept);
        accept.read(buffer);
        System.out.println(new String(buffer.array()));
    }
}

⬇设置为非阻塞模式,充分利用系统的函数进行优化

public class NioTcpDemo01 {
	//设置为非阻塞
    public static void main(String[] args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        ssc.configureBlocking(false);
        while(true){
            SocketChannel accept = ssc.accept();//重点为非阻塞
            if(accept != null)
            	System.out.println(accept);
        }
    }
}

⬆设置为非阻塞之后accept()不再阻塞,如果没有连接进来,会返回null。通过主线程while循环进行不断轮询可以知道是否有连接进来。

将SocketChannel也设置为非阻塞。即当客户端连接进来时不阻塞read事件.

public static void main(String[] args) throws Exception {
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        ssc.configureBlocking(false);
        List<SocketChannel> socketChannels = new ArrayList<>();
        while(true){
            SocketChannel accept = ssc.accept();//重点为非阻塞
            if(accept != null){
                System.out.println(accept);
                accept.configureBlocking(false); //设置SocketChannel为非阻塞
                socketChannels.add(accept);
            }

            for (SocketChannel socketChannel : socketChannels){

                int read = socketChannel.read(buffer);//这里不阻塞了,如果读到数据,则返回数据长度,否则返回0
                if (read > 0){
                    buffer.flip();
                    byte[] data = new byte[read];
                    buffer.get(data);
                    System.out.println(new String(data));
                    buffer.clear();
                    socketChannel.write(ByteBuffer.wrap(data));
                }
                if (read == -1) { //read==-1就断开连接了
                    // 客户端断开连接
                    System.out.println("客户端断开连接: " + socketChannel);
                    socketChannel.close();
                    socketChannels.remove(socketChannel);
                    continue;
                }
            }
        }
    }

selector和channel建立连接

public class NioTcpDemo02 {
    public static void main(String[] args) throws Exception {
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        ssc.configureBlocking(false);//设置为非阻塞

        //创建多路复用
        Selector selector = Selector.open();
        //ssc注册到多路复用器,监听连接事件,selector管理和监听这个Channel
        ssc.register(selector, SelectionKey.OP_ACCEPT);
        while(true){
            int select = selector.select(); //阻塞,等待事件触发
            if(select > 0){
                Set<SelectionKey> selectionKeys = selector.selectedKeys(); //获取所有事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if(key.isAcceptable()){//判断是否是连接事件
                        //连接事件的channel必定是ServerSocketChannel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        //接受连接
                        SocketChannel socketChannel = channel.accept();
                        System.out.println(socketChannel);
                        socketChannel.configureBlocking(false);
                        //socketChannel也交给Selector管理和监听
                        socketChannel.register(selector,SelectionKey.OP_READ);
                        System.out.println("连接成功");
                        iterator.remove(); // 时间处理完成移除事件
                    }
                    if(key.isReadable()){
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        byte[] buffer = new byte[1024];
                        int read = socketChannel.read(ByteBuffer.wrap(buffer));
                        if(read > 0){
                            System.out.println(new String(buffer));
                        }
                        socketChannel.write(ByteBuffer.wrap(buffer));
                        iterator.remove();
                    }
                }
            }
        }
    }
}

boss-worker

但实际上我们只用到了一个线程,一个selector,不能充分使用多核cpu的优势。现在我们创建多个selector。我们规定boss-selector只负责建立连接,worker-selector负责业务处理。每次连接创建一个线程,这个线程中创建worker-selector,这个线程中的selector负责处理其他事件。而且规定创建的worker数量。

连接事件->boss
读写事件->worker

假设worker最大数量=4个
当一个连接进来,我们创建一个worker,有4个连接进来,创建4个worker后,第5个连接进来时会复用原来的worker

多Reactor

这篇文章,带你从最简单的tcp服务器入手。再到NIO 服务器,学会用netty高性能框架搭建各种基于Tcp协议的服务器。

该过程全部针对的是java Tcp服务器,所以,TCP客户端使用工具即可(我这里使用的是友善串口助手)。

1.java最简单的Tcp服务:

代码:
public class TcpDemo01 {

	public static void main(String[] args) throws IOException {
		// 1.创建Tcp服务器并绑定端口
		ServerSocket serverSocket = new ServerSocket(8111);
		// 2.等待连接
		Socket socket = serverSocket.accept();
		// 3.打印连接内容
		System.out.println(socket);
	}

}

解释:

java创建tcp服务器是极其简单的。

  1. new ServerSocket(8111)就能创建一个Tcp服务器。
  2. 执行到serverSocket.accept()方法。代码会卡(阻塞)在这个方法,不执行后面的代码。当客户端连接进来时,该方法就不卡(阻塞)。同时会返回客户端连接信息。
  3. 打印的信息,addr客户端的ip地址,port客户端的端口,localport前面设置的服务器端口。

2.保持连接,接收客户端信息

代码:
public class TcpDemo02 {

	public static void main(String[] args) throws IOException {
		// 1.创建Tcp服务器并绑定端口
		ServerSocket serverSocket = new ServerSocket(8111);
		// 2.等待连接
		Socket socket = serverSocket.accept();
		// 3.打印连接内容
		System.out.println(socket);
		// 4.拿到输入流
		InputStream inputStream = socket.getInputStream();
		// 5.等待客户端发信息过来
		int readData = inputStream.read();
		System.out.println((char) readData);
	}

}
  1. 当TcpServer和TcpClient连接完成之后,他们之间就会有通道。我们拿到输入流是针对Server而言。即输入到Server的流。

  2. inputStream.read();方法也会阻塞,当客户端发送数据过来的时候才会取消阻塞。

  3. demo02-test

  4. 客户端发送的是ascii的123,所以服务器要将ascii转成字符(char) readData。通过测试我们发现,客户端发送的是123,但服务只打印了1。read()方法读取流中的数据是挨个读的。读到一个数据1后,代码继续执行打印,后面没有代码了,服务器就断开连接了流也断开了。所以只读到一个。

  5. 解决读取客户端数据不完整问题:

    1. read()有带参数的方法重载read(byte b[])。设置byte b[]数组的大小就能设置每次读取多少个数据。代码:

      public class TcpDemo02 {
      
      	public static void main(String[] args) throws IOException {
      		// 1.创建Tcp服务器并绑定端口
      		ServerSocket serverSocket = new ServerSocket(8111);
      		// 2.等待连接
      		Socket socket = serverSocket.accept();
      		// 3.打印连接内容
      		System.out.println(socket);
      		// 4.拿到输入流
      		InputStream inputStream = socket.getInputStream();
      		byte bs[] = new byte[3];
      		// 5.等待客户端发信息过来
      		int readData = inputStream.read(bs);
      		System.out.println(new String(bs));
      	}
      
      }
      

      ​ 但显然这种方式不合理,虽然接收到了客户端完整123信息,但只能接收到一次,服务器还是会断开。而且我们是不知道客户端要传多少信息过来的。当,接收大小 < 发送大小。数据还是会发生丢失。

    2. 循环读取,代码:

      public class TcpDemo02 {
      
      	public static void main(String[] args) throws IOException {
      		// 1.创建Tcp服务器并绑定端口
      		ServerSocket serverSocket = new ServerSocket(8111);
      		// 2.等待连接
      		Socket socket = serverSocket.accept();
      		// 3.打印连接内容
      		System.out.println(socket);
      		// 4.拿到输入流
      		InputStream inputStream = socket.getInputStream();
      		while (true) {
      			// 5.等待客户端发信息过来
      			int readData = inputStream.read();
      			System.out.println((char) readData);
      			// 客户端发送完成时read()方法会返回-1
      			if (readData == -1) {
      				System.out.println("接收完成断开连接");
                      break;
      			}
      		}
      	}
      
      }
      

      ​ 通过一个循环,一直等待read()方法返回数据,这样子就能一直接收到客户端传来的信息了。有数据就执行打印,并且判断read()返回的数据是否是-1,-1表示客户端发送完成。循环读取,每次只读一个字符,如果客户端发送的数据量大,那么性能就会下降。结合上面两种接收方式做以下改进。

    3. 结合优化

      public class TcpDemo02 {
      
      	public static void main(String[] args) throws IOException {
      		// 1.创建Tcp服务器并绑定端口
      		ServerSocket serverSocket = new ServerSocket(8111);
      		// 2.等待连接
      		Socket socket = serverSocket.accept();
      		// 3.打印连接内容
      		System.out.println(socket);
      		// 4.拿到输入流
      		InputStream inputStream = socket.getInputStream();
      		byte bs[] = new byte[1024];
      		while (true) {
      			// 5.等待客户端发信息过来
      			int readData = inputStream.read(bs);
      			System.out.println(new String(bs));
      			// 客户端断开连接时read()方法会返回-1
      			if (readData == -1) {
      				System.out.println("客户端断开连接");
                      break;
      			}
      		}
      	}
      
      }
      

3.服务器支持多个客户端

​ 至此,就得到了一个能接收到单个客户端信息的Tcp服务器。这时候再打开一个tcp客户端工具,就会发现只能第一个连接的客户端发送消息能在服务器打印。这是为什么呢?

//根据上面的代码,观察这3行代码
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
int readData = inputStream.read(bs);

​ 不难发现,我们只维护了第一个socket,第一个输入流。后面连接进来的也没有覆盖第一个也没有进行管理。我们的服务器只能接收一个请求,这显然不合理。进一步优化!

​ 首先我们要明确,如何才能使得两个客户端发送过来的消息都能进行处理。我们发现如果只在当前线程的话,只能接收处理一个连接。这时候引入多线程去做处理就能解决这些问题。(当然还有其他方法可以做到,后面讨论)

代码:
public class TcpDemo03 {

	public static void main(String[] args) throws IOException {
		// 1.创建Tcp服务器并绑定端口
		ServerSocket serverSocket = new ServerSocket(8111);
		while (true) {
			// 2.等待连接
			Socket socket = serverSocket.accept();
			// 3.打印连接内容
			System.out.println(socket + "连接成功");
			// 4.每次有连接,创建一个线程这个连接管理
			new Thread(() -> {
				try {
					InputStream inputStream = socket.getInputStream();
					byte bs[] = new byte[1024];
					while (true) {
						// 5.等待客户端发信息过来
						int readData = inputStream.read(bs);
						// 客户端断开连接时read()方法会返回-1
						if (readData == -1) {
							System.out.println(socket + "断开连接");
							break;
						}
						System.out.println(socket + "收到信息:" + new String(bs));
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}).start();

		}
	}
}

解释:

​ 每次有连接进来的时候,创建一个线程(lambda表达式),每个连接都单独处理,互不影响。这样子我们就能支持多个客户端的连接了。每次连接都会创建一个线程,客户端数量上来了就会卡顿,服务器扛不住这么多连接。既然如此再优化!

​ 设置最大连接数量,并且我们不主动创建线程,由线程池去管理线程。

public class TcpDemo04 {
	private static final int MAX_CONNECT = 200;// 设置最大连接数
	private static ExecutorService threadPool = Executors.newFixedThreadPool(MAX_CONNECT);
	public volatile static int connectedCount = 0;

	public static void main(String[] args) throws IOException {
		// 1.创建Tcp服务器并绑定端口
		ServerSocket serverSocket = new ServerSocket(8111);
		while (true) {
			// 2.等待连接
			Socket socket = serverSocket.accept();
			connectedCount++;
			if (connectedCount > MAX_CONNECT) {
				socket.close();
                connectedCount--;//连接关闭,连接数量-1
				System.out.println("连接数量超过设定数据,阻止连接");
			} else {
				// 3.打印连接内容
				System.out.println(socket + "连接成功");
				threadPool.execute(new HandleThread(socket));
			}
		}
	}
}

class HandleThread implements Runnable {

	private Socket socket;

	public HandleThread(Socket _socket) {
		this.socket = _socket;
	}

	@Override
	public void run() {
		try {
			InputStream inputStream = socket.getInputStream();
			while (true) {
				byte bs[] = new byte[1024];
				// 5.等待客户端发信息过来
				int readData = inputStream.read(bs);
				// 客户端断开连接时read()方法会返回-1
				if (readData == -1) {
					System.out.println(socket + "断开连接");
                    TcpDemo04.connectedCount--;//连接关闭,连接数量-1
					break;
				}
				System.out.println(socket + "信息:" + new String(bs));
			}
		} catch (Exception e) {

		}
	}
}

​ 连接过来时,线程池管理这些连接。经过测试我们发现,当超出最大连接数量时,后面的连接就会断开。这样子就能防止过多连接影响性能。显然这种方式对超出连接数量后的连接不够友好。它们无论如何都不能连接到服务器,我们提供的服务器用户多了就会卡。明显我们要解决这个问题。

解决问题的方式就有:

  1. 大力飞砖:又称纵向扩展,即升级配置,增强服务器的硬件配置(如增加CPU、内存、存储等)来提升其性能。

  2. 负载均衡:又称横向扩展,即增加服务器数量,一个服务器不够用那就搞多几个服务器。

  3. 优化:充分利用电脑的性能,减少连接开销,减少处理开销。创建线程过多,线程上下文切换,以及每个线程都会占用一定的栈空间。serverSocket.accept(),socket.getInputStream().read()都是阻塞的,阻塞过程中CPU都不工作.

前面两种都是要钱的。那就只能尽量通过优化的方式去解决问题。

下面我们引进nio(非阻塞io)

Http协议基于Tcp协议。

相对于TCP协议:

  • 规定客户端需要发过来什么格式的信息,服务器需要返回什么格式的信息,通过格式服务器或者客户端就能根据规定解析得到对方传递过来的内容。

  • 短连接,客户端连接服务器,服务器返回信息后主动断开连接。

最简单的Http服务器

​ 1.首先,通过浏览器访问我们的服务器。

public class HttpServerDemo01 {
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(8111);
		Socket accept = serverSocket.accept();
        OutputStream outputStream = accept.getOutputStream();
        //真实内容
        String content = "<html><body><h1>Hello World!</h1></body></html>";
        //按照协议,拼接返回内容
        String response = "HTTP/1.1 200 OK\r\n" + 
            			"Content-Type: text/html; charset=utf-8\r\n"  +
            			"Content-Length: "+content.getBytes().length + "\r\n"  +
            			"\r\n"+
            			content
		outputStream.write(buildHttpResponse().getBytes());
	}
}

返回内容解析

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 47

<html><body><h1>Hello World!</h1></body></html>
  • 协议版本:HTTP1.1。
  • 响应码:200。200为成功,我们常见的404响应码,可以通过改动这个地方。
  • 返回的数据类型:Content-Type: text/html; 这个数据类型表示html,浏览器会解析为html并且渲染在界面上。
  • 字符集:charset=utf-8。
  • 空行: 这个空行将上面协议内容和真正的数据隔离开。不可删除
  • 返回的数据:这个就是服务器真正返回给浏览器的数据。
  • 要注意的点:上面的返回内容,一行都是使用回车换行(\r\n)。

概念

**Zero-Copy 指的是:**在数据在内核空间中“就地传输”,无需拷贝至用户空间,避免中间缓冲区的重复搬运。

核心目标:

  • 最少拷贝:尽可能避免用户空间和内核空间之间的数据复制

  • 最少上下文切换:减少 CPU 参与传输过程

  • 高性能吞吐:提升 I/O 吞吐量与系统响应速度

Java日志框架

Java日志框架系统学习教程(完善版)

第一部分:日志基础与核心概念

  1. 日志的重要性与使用场景
    • 为什么需要日志框架
    • 日志的五个核心作用(调试、监控、审计、统计分析、故障排查)
    • 良好日志实践的标准
  2. 日志级别详解
    • TRACE、DEBUG、INFO、WARN、ERROR、FATAL
    • 级别选择策略与应用场景
    • 动态调整日志级别

第二部分:Java日志发展历史与架构演进

  1. 第一代:JDK自带日志
    • java.util.logging (JUL) 的诞生与设计
    • 优点与局限性
  2. 第二代:Log4j 1.x的统治时代
    • Apache Log4j 1.x的设计理念
    • 配置文件格式与基本使用
    • 性能问题与停止维护
  3. 日志门面的出现
    • 为什么需要门面模式
    • Commons Logging (JCL) vs SLF4J
    • 门面模式的优势与实现原理
  4. 第三代:现代日志框架
    • Logback:SLF4J的官方实现
    • Log4j 2.x:性能与功能的飞跃
    • 异步日志的演进

第三部分:主流日志框架深度使用

  1. java.util.logging (JUL)
    • 基础配置与Handler使用
    • 自定义Formatter与Filter
    • 在无依赖项目中应用
  2. Log4j 2.x
    • 架构:API与Core分离
    • 配置文件详解(XML、JSON、YAML、Properties)
    • Appenders:Console、File、RollingFile、Socket、Async等
    • Layouts:PatternLayout、JSONLayout、CSVLayout
    • Filters:Threshold、Burst、Time等
    • 异步日志:AsyncLogger与AsyncAppender
    • Lookups与自定义组件
    • 性能优化与监控
  3. Logback
    • 架构与组件:Logger、Appender、Layout
    • 配置文件详解:logback.xml、logback-spring.xml
    • Appender:ConsoleAppender、FileAppender、RollingFileAppender
    • 条件配置与变量使用
    • MDC(Mapped Diagnostic Context)使用
    • 性能特性与自动重载配置

第四部分:日志门面深入理解

  1. SLF4J深入
    • 绑定机制与桥接器
    • MDC与标记(Marker)高级使用
    • 参数化日志与性能优化
    • 桥接旧日志系统(jcl-over-slf4j, jul-to-slf4j, log4j-over-slf4j)
  2. 日志统一管理策略
    • 多模块项目日志统一
    • 第三方库日志接管
    • 避免日志冲突与依赖地狱

第五部分:Spring生态集成

  1. Spring Framework日志集成
    • Spring的日志抽象
    • 与JCL、SLF4J的集成方式
    • 在Spring MVC/WebFlux中的日志实践
  2. Spring Boot日志配置
    • 默认Logback配置与自动配置原理
    • 配置文件优先级:logback-spring.xml > application.properties
    • Profile-specific配置
    • 彩色输出与banner日志
  3. Spring Boot切换日志框架
    • 切换到Log4j2:排除logback,添加log4j2依赖
    • 切换到JUL:配置与注意事项
    • 多日志框架并存策略

第六部分:高级特性与最佳实践

  1. 性能优化
    • 异步日志的性能影响
    • 合理的日志级别设置
    • 避免日志中的性能陷阱
  2. 结构化日志
    • JSON格式日志输出
    • 与ELK/EFK栈集成
    • 在微服务中的结构化日志实践
  3. 分布式追踪
    • TraceId与SpanId的传递
    • 集成Sleuth/Brave
    • 日志与OpenTelemetry
  4. 日志监控与告警
    • 关键错误告警配置
    • 日志量监控
    • 基于日志的Metrics收集

第七部分:项目实战与案例分析

  1. 单应用日志配置实战
    • 开发/测试/生产环境不同配置
    • 日志文件切割与归档策略
    • 敏感信息过滤
  2. 微服务架构日志方案
    • 集中式日志收集架构
    • 日志规范与标准化
    • 跨服务调用链追踪
  3. 常见问题排查
    • 日志丢失问题定位
    • 性能问题排查
    • 配置不生效排查流程

第八部分:扩展与未来趋势

  1. 新兴日志框架了解
    • tinylog
    • Log4j 2.x最新特性
  2. 云原生环境日志
    • 容器环境日志最佳实践
    • Kubernetes日志方案
    • Serverless函数日志
  3. 日志即数据理念
    • 日志分析平台搭建
    • 基于日志的机器学习应用

大纲

  • 基本使用
  • 日志级别
  • 日志处理器Handler
  • 日志格式化器Formater
  • 日志的层级关系

基本使用

import java.util.logging.Logger;
public class Demo01 {
    private final static Logger log = Logger.getLogger(Demo01.class.getName());
    public static void main(String[] args) {
        log.info("hello world");
    }
}
2月 02, 2026 11:44:58 上午 w.Demo01 main
信息: hello world

解释

  • 代码

​ jul正常定义日志定义成private只在一个类中使用,final修饰。通过调用Logger的静态方法**getLogger()**将类的名称作为参数传进去.

  • 输出

​ 时间格式默认为:mm月 xx, yyyy年 am/pm 类全限定名 方法 信息:xxxxx

高版本的jdk比如17,默认配置文件在安装目录下的/conf目录的logging.properties下。

日志级别

​ 需要在配置文件logging.properties中设置

  • log.severe("严重级别");
  • log.warning("警告级别");
  • log.info("信息级别"); //默认
  • log.config("配置级别");
  • log.fine(" 细微级别");
  • log.finer("更细微级别");
  • log.finest("最细微级别");
public class Demo02 {
    private static final Logger log = Logger.getLogger(Demo02.class.getName());
    public static void main(String[] args) {
        log.severe("严重级别");
        log.warning("警告级别");
        log.info("信息级别"); //默认

        log.config("配置级别");
        log.fine(" 细微级别");
        log.finer("更细微级别");
        log.finest("最细微级别");
    }
}

Handler

public class Demo03 {
    private static final Logger log = Logger.getLogger(Demo03.class.getName());
    public static void main(String[] args) throws IOException {
        log.setUseParentHandlers(false); //必须关闭父级处理器
        
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(java.util.logging.Level.ALL);
        log.addHandler(consoleHandler);
        log.severe("严重级别");

        FileHandler fileHandler = new FileHandler("D://demo03.log", true);
        fileHandler.setLevel(java.util.logging.Level.ALL);
        log.addHandler(fileHandler);
        log.info("信息级别");
    }
}

JUL有两种处理器,控制台ConsoleHandler和FileHandler。创建处理器并且加入到日志对象中就完成功能。注意:必须要关闭父级管理器,java启动默认有一个处理器默认配置就是处理器中相关的内容。

Formatter

public class Demo04 {
    private static final Logger log = Logger.getLogger(Demo04.class.getName());
    public static void main(String[] args) throws IOException {

        log.setUseParentHandlers(false);//必须关闭父级处理器

        SimpleFormatter formatter = new SimpleFormatter();

        FileHandler fileHandler = new FileHandler("D://demo04.log", true);
        fileHandler.setLevel(Level.ALL);
        fileHandler.setFormatter(formatter); //设置日志格式
        log.addHandler(fileHandler);
        log.info("信息级别");
    }
}

控制台默认的日志格式就是SimpleFormatter,文件输出的是xml格式,所以只要把FileHandler的日志格式改成SimpleFormatter。

JVM

默认栈大小堆大小收操作系统,不同虚拟机,硬件情况,等因素影响

栈溢出

栈分成两种

  • 虚拟机栈
  • 本地方法栈(jni调用)

栈溢出原因主要是太多东西入栈,每次调用方法都是一次入栈,每次调用生成的栈帧入栈后占用一些空间。当出现无限递归时会溢出,当栈深度过深时也会出现溢出。

OOM

OutOffMemeryError有很多种出现情况,OOM一般分为:堆区,方法区,常量池。

堆区:

  • 一直创建且不让回收:一直往一个集合里面塞新建的对象。

  • 创建大规模数组

方法区:

  • 动态生成大量类

常量池:

  • jdk1.7后字符串常量放到了堆中:使用intern()方法导致。

Maven

Maven 多模块项目结构解析

项目父模块 POM 配置分析

以下是一个典型的多模块 Maven 项目的父 POM 配置示例:

xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <!-- 项目基本信息 -->
    <groupId>org.example</groupId>
    <artifactId>wyl-iot</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    
    <!-- 继承 Spring Boot 父项目 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>
    
    <!-- 模块定义 -->
    <modules>
        <module>iot-common</module>
        <module>iot-gateway</module>
        <module>iot-mqtt</module>
    </modules>
    
    <!-- 统一属性配置 -->
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <hutool.version>5.8.11</hutool.version>
        <mqtt.version>1.2.5</mqtt.version>
    </properties>
    
    <!-- 依赖版本管理 -->
    <dependencyManagement>
        <dependencies>
            <!-- MQTT 客户端依赖管理 -->
            <dependency>
                <groupId>org.eclipse.paho</groupId>
                <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
                <version>${mqtt.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <!-- 子模块共享依赖 -->
    <dependencies>
        <!-- Lombok 注解处理器 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        
        <!-- Hutool 工具库 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
    </dependencies>
</project>

父 POM 关键特性解析

1. 继承机制

  • 通过继承 spring-boot-starter-parent,项目自动获得 Spring Boot 的默认配置和依赖管理

  • 子模块可以无需指定版本号直接使用 Spring Boot 相关依赖,例如:

    xml

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    

2. 依赖管理

  • <dependencyManagement> 节用于统一管理依赖版本,确保多模块间版本一致性

  • 声明的依赖不会直接引入,需要在子模块中显式引用

  • 子模块引用时无需指定版本号:

    xml

    <dependency>
        <groupId>org.eclipse.paho</groupId>
        <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
    </dependency>
    

3. 共享依赖

  • <dependencies> 节中定义的依赖会被所有子模块继承
  • 这些依赖会通过依赖传递机制自动提供给子模块使用

子模块 POM 配置示例

以下是一个子模块的典型 POM 配置:

xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <!-- 指定父模块 -->
    <parent>
        <groupId>org.example</groupId>
        <artifactId>wyl-iot</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    
    <!-- 子模块标识 -->
    <artifactId>iot-mqtt</artifactId>
    
    <!-- 模块特定依赖 -->
    <dependencies>
        <!-- 引用同一项目中的其他模块 -->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>iot-common</artifactId>
            <version>${project.version}</version>
        </dependency>
        
        <!-- Spring Boot 相关依赖(无需版本号) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <!-- 父模块依赖管理中定义的依赖 -->
        <dependency>
            <groupId>org.eclipse.paho</groupId>
            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
        </dependency>
        
        <!-- 其他第三方依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
    
    <!-- 构建配置 -->
    <build>
        <plugins>
            <!-- Spring Boot 打包插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

子模块关键特性解析

1. 属性继承

  • 子模块可以引用父模块中定义的所有属性
  • 例如:${maven.compiler.source} 引用父模块中定义的 Java 版本

2. 依赖管理

  • 子模块自动继承父模块的依赖管理配置
  • 对于父模块中已通过 <dependencies> 定义的依赖(如 Lombok、Hutool),子模块无需显式声明即可使用
  • 对于父模块中仅在 <dependencyManagement> 定义的依赖,子模块需要显式声明依赖但无需指定版本号
  • Spring Boot 相关依赖也无需指定版本号,由继承的 Spring Boot 父 POM 管理

3. 模块间依赖

  • 子模块可以依赖同一项目中的其他模块
  • 建议使用 ${project.version} 确保版本一致性

maven中的坑

  • 依赖管理问题

父POM

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<!-- 通过import scope导入BOM -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子模块:

<parent>
    <groupId>org.example</groupId>
    <artifactId>wyl-iot</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId><!-- 不需要指定版本 -->
        
        <!--nacos 服务注册发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <!--网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependency>
</dependencies>

​ 如果版本管理中引入的spring-cloud-dependencies中没有使用import。子模块会找不到版本号。例如,这里如果去掉了。下面的:nacos 服务注册发现、负载均衡、网关统统找不到。但是,如果加了一次版本号,构建后去掉版本号又能找到了。所以这里有个坑。

原因分析:

已经使用了spring-boot-starter-parent作为parent,不能再有另一个parent。所以对于Spring Cloud的依赖管理,只能通过import scope来导入。

总结:Spring Boot starters不需要指定版本是因为版本管理通过parent继承,而Spring Cloud组件需要通过import scope导入BOM来进行版本管理。

scope说明
compile默认值。表示依赖在编译、测试、运行阶段都可用。这类依赖会被打包进最终的 artifact(如 JAR/WAR)。
provided表示依赖在编译和测试时需要,但在运行时由容器或 JDK 提供,不会被打包。典型场景:Servlet API、JSP API 等。
runtime表示依赖在编译阶段不需要,但在运行和测试时需要。例如 JDBC 驱动,编译时仅通过接口编程,运行时才需要具体实现。
test表示依赖仅在测试编译和执行时有效,不会传递到其他模块,也不会被打包。例如 JUnit、Mockito。
system类似 provided,但依赖不是从 Maven 仓库获取,而是通过 <systemPath> 显式指定本地文件路径。通常用于引入系统路径中的 JAR,不推荐使用。
import仅用于 <dependencyManagement> 中,且 type 必须为 pom。它表示将目标 POM 的 <dependencyManagement> 内容合并到当前项目的依赖管理中,常用于统一管理多个模块的版本(如 Spring Boot BOM、Spring Cloud BOM)。

Maven注意事项

  • 一个项目不能直接依赖parent模块,例如
<?xml version="1.0" encoding="UTF-8"?>
<project>
    <parent>
        <artifactId>java-study</artifactId>
        <groupId>psn.wyl</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>http-ota</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId> <!--这里直接依赖行不通-->
            <version>2.7.18</version>
            <scope>import</scope>
        </dependency>
    </dependencies>
</project>
  • 解决方法
<?xml version="1.0" encoding="UTF-8"?>
<project>
    <parent>
        <artifactId>java-study</artifactId>
        <groupId>psn.wyl</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>http-ota</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId> <!--不依赖parent-->
            <version>2.7.18</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

坑2

父pom

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>
        <!-- 缺少 mybatis-plus-core 的版本管理 -->
    </dependencies>
</dependencyManagement>
子pom
<dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-core</artifactId>
        </dependency>
</dependencies>

这时候,项目是拿不到mybatis-plus-core依赖包的

只有这种写法可以

<dependencies>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>
</dependencies>

如果已经有还想依赖spirngboot

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.7.18</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
        <version>2.9.11</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type><!--BOM 文件以 POM 形式存在的,它不包含实际的代码,只包含了各种依赖的版本声明-->
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Springcloud相关

一个完整的后端SpirngCloud项目大致如下:

maven项目:

​ 网关模块:gateway 做路由,验证等功能

​ 各种服务模块:xxx-service 各个服务之间一般通过OpenFeign远程调用

​ 以及公共模块:common

这个maven项目的pom.xml文件打包方式为pom,其他的模块打包方式为jar,服务模块可以打包成war部署在tomcat之类的服务器。

maven pom.xml文件做依赖管理

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.heima</groupId>
    <artifactId>hmall</artifactId>
    <packaging>pom</packaging>
    <version>1.0.0</version>
    <modules>
        <module>item-service</module>
        <module>gateway</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
        <relativePath/>
    </parent>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <org.projectlombok.version>1.18.20</org.projectlombok.version>
        <spring-cloud.version>2021.0.3</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <mysql.version>8.0.23</mysql.version>
    </properties>

    <!-- 对依赖包进行管理 -->
    <dependencyManagement>
        <dependencies>
            <!--spring cloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud alibaba-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- 数据库驱动包管理 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!-- mybatis plus 管理 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <!-- lombok 管理 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${org.projectlombok.version}</version>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>17</source> <!-- depending on your project -->
                        <target>17</target> <!-- depending on your project -->
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

微服务拆分之后,项目的之间使用通常有两种:

1.提供远程调用的服务自己去管理对外的远程接口,新建一个独立的子模块专门存放对外的远程调用。其他需要调用的客户端引入这个模块即可实现远程调用

2.一个独立的api模块。所有需要远程调用的模块都引入这个模块。

示例

  • hmall
    	- user-service 用户模块
    		- user-api 用户模块下api模块
    			- dto  api模块下的dto
    			- vo   api模块下的vo
    			- client  api模块下的远程调用接口
    		- user-biz 用户模块下的业务功能
    		
    	- card-service
    		- card-api 购物车模块下api模块
    			- dto  api模块下的dto
    			- vo   api模块下的vo
    			- client  api模块下的远程调用接口
    		- card-biz 购物车模块下的业务功能
    		
    	- item-service
    		- item-api 商品模块下api模块
    			- dto  api模块下的dto
    			- vo   api模块下的vo
    			- client  api模块下的远程调用接口
    		- item-biz 商品模块下的业务功能
    	- gateway  网关
    
  • hmall
    	- user-service 	用户模块
    		- domain
    			- dto
    			- vo
    			- po
    			- ...
    		- mapper
    		- service
    		- controller
    		- ...
    	- card-service 	购物车模块
    	- item-service 	商品模块
    	- gateway	   	网关
    	- openfeign-api 远程服务模块
    		- dto
    		- vo
    		- client
    

一般来说推荐第一种,各模块的耦合度没有那么高。

依赖引入

网关也是微服务,也需要注册到nacos

需要引入gateway依赖

需要引入loadbalancer依赖,用于负载均衡

<dependencies>
    <!--nacos 服务注册发现-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
</dependencies>

启动类

需要开启服务发现

@SpringBootApplication
@EnableDiscoveryClient // 启用服务发现客户端
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

配置文件

spring:
  application:
    name: api-gateway  #服务名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos 服务器地址
    gateway:
      routes:
        - id: item-service
          uri: lb://item-service # 使用lb负载均衡
          # uri: http://localhost:8081 # 不使用负载均衡
          predicates:
            - Path=/items/**
		  filters:
		  	- RewritePath= 
        - id: card-service
          uri: lb://card-service # 使用lb负载均衡
          predicates:
            - Path=/card/**

配置文件解析:

  • spirng.gateway.routes:

    每个 - 后面都是一个路由。

    id: 为这个路由的编号,具有唯一性。

    uri: 为要访问的服务器地址,可以写死,如果使用了负载均衡必须要引入nacos依赖和loadbalancer依赖。

    predicates: 断言,匹配路径 - Path=/items/**,意思是如果是这个通配符下的路径全部负载均衡到上面uri配置的服务地址。

    # 1. Path 路径匹配:匹配以 /api/ 开头的请求
    - Path=/api/**
    
    # 2. Method 方法匹配:匹配 GET 或 POST 请求
    - Method=GET, POST
    
    # 3. Header 请求头匹配:请求头必须包含 X-Request-Id,且值符合正则表达式
    - Header=X-Request-Id, \d+
    
    # 4. Cookie 匹配:请求必须包含名为 loginId 的 cookie,其值符合正则表达式
    - Cookie=loginId, .+
    
    # 5. Host 主机名匹配:匹配 Host 为 `**.example.com` 的请求
    - Host=**.example.com
    
    # 6. Query 参数匹配:请求必须包含名为 `token` 的查询参数,值可选(也可以像Header一样指定值正则)
    - Query=token
    # - Query=token, abc.  # 表示必须有token参数,且值匹配正则 `abc.`
    
    # 7. After/Before/Between 时间匹配(基于ZonedDateTime)
    - After=2023-01-20T17:42:47.789-07:00[America/Denver]
    - Before=2023-01-21T17:42:47.789-07:00[America/Denver]
    - Between=2023-01-20T17:42:47.789-07:00[America/Denver], 2023-01-21T17:42:47.789-07:00[America/Denver]
    
    # 8. RemoteAddr 远程地址匹配:匹配来自指定IP段(CIDR表示法)的请求
    - RemoteAddr=192.168.1.1/24
    

    filters: 过滤,可以直接放行一些路径比如swagger文档之类的

    # 1. Path 重写:将请求路径中的 `/user-service` 替换成空
    # 原始请求:/user-service/api/users -> 转发后的请求:/api/users
    - RewritePath=/user-service/?(?<segment>.*), /$\{segment}
    # 2. 添加请求头:在转发前添加一个请求头 `X-Gateway-Flag: true`
    - AddRequestHeader=X-Gateway-Flag, true
    
    # 3. 添加响应头:在收到下游响应后,给客户端响应添加一个头 `X-Response-From: gateway`
    - AddResponseHeader=X-Response-From, gateway
    
    # 4. 剥离路径前缀:去掉路径的前面一部分(例如这里去掉1级路径)
    # 原始请求:/api/user/1 -> 转发后的请求:/user/1
    - StripPrefix=1
    
    # 5. 请求参数处理:添加一个参数 `source=gateway`
    - AddRequestParameter=source, gateway
    
    # 6. 熔断器(Hystrix/Resilience4j):集成熔断,提供降级能力
    - name: Hystrix
      args:
        name: fallbackcmd
        fallbackUri: forward:/fallback # 熔断时转发到网关内的 /fallback 端点
    
    # 7. 重试机制:对特定情况(如5xx错误)进行重试
    - name: Retry
      args:
        retries: 3
        statuses: BAD_GATEWAY, INTERNAL_SERVER_ERROR # 遇到这些状态码才重试
        methods: GET # 只对GET方法重试
    
    # 8. 请求体缓存(通常用于需要读取请求体的过滤器,如修改请求体)
    - CacheRequestBody=REQUEST_BODY_CACHE
    
    # 9. 修改请求路径:简单直接地设置新路径
    - SetPath=/api/v2/$\{segment} # 需要与其他提取参数的Predicate/Filter配合使用
    # 10. 限流(RequestRateLimiter):通常配合Redis使用,基于令牌桶算法
    - name: RequestRateLimiter
      args:
        redis-rate-limiter.replenishRate: 10 # 每秒允许的请求数
        redis-rate-limiter.burstCapacity: 20 # 每秒最大处理的请求数(突发流量)
        key-resolver: "#{@userKeyResolver}" # 指定限流键的解析器Bean(例如按用户、IP限流)
    

SSM相关

SpringMVC

1.入口控制:通过DispatcherServlet作为入口控制器,负责接收请求和分发请求。自己写Servlet需要再web.xml中配置。通过请求路径确定分发到指定的地方。

2.参数直接绑定对象。前端参数直接可以通过DTO直接绑定对象数据。

3.ioc容器,不需要自己创建对象。

4.提供了拦截器,异常处理器。

5.支持识图解析。

执行流程

从浏览器再回到浏览器。

  1. 前端访问一个路径
  2. 通过一系列的过滤器链(Filter),过滤器执行顺序与过滤器在servlet容器注册顺序决定。
  3. 来到DispatcherServlet,通过httpServletRequest拿到访问路径(uri),通过路径获取要对应的执行器(controller)。
  4. 拦截到达执行器(controller)前的拦截,preHandle拦截器开始作用。
  5. controller处理业务,DispatcherServlet调用 ViewResolver视图解析器拿到前端上报的数据,也可以通过HttpServletRequest拿到前端请求携带的数据。
  6. 拦截处理完的结果,postHandler执行。
  7. 渲染视图,结合thymeleat之类的视图框架将各种再ModelAndView或者respose中的数据,解析并且放到html文件。
  8. 后置拦截器afterCompletion负责清理各种这次请求产生的资源和日志。
  9. 再次通过,过滤器链(Filter),过滤器执行顺序与过滤器在servlet容器注册顺序决定。
  10. 回到浏览器。

模板引擎

前后端不分离,使用后端直接整合前端css,js,html。一般使用模板引擎框架,有以下技术栈:

jsp(需要引入额外依赖),thymeleaf,FreeMarker,或者直接写原生。

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

如果没有引进thymeleaf依赖。springboot也支持访问静态资源。

/static
/public
/resources
/META-INF/resources

但是访问不到

/templates/index.html 
MqttClient mqttClient;

设置主题回调(所有主题都走这个接口实现)

mqttClient.setCallback(new MqttAllTopicCallback());

public class MqttAllTopicCallback implements MqttCallback{

}

设置主题回调(规定什么主题走什么接口实现)

mqttClient.subscribe("sys/#", 1,new MqttRuleListner()); //规定sys/#主题走MqttRuleListner
public class MqttRuleListner implements IMqttMessageListener{
	
}
//注意这种方式不能走共享订阅,$queue和$share

Timer

Timer timer = new Timer();
timer.schedule(new TimerTask(){
    @Override
    public void run(){
        System.out.println("执行完成");
    }
},1000);//一秒钟后执行一次

明确一点,是MP还是仅仅mybatis。

MP

直接添加一个bean进去就可以了,3.4.0以上版本使用MybatisPlusInterceptor,3.4.0以下使用PaginationInterceptor。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // 添加分页插件,指定数据库类型为 MySQL
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    return interceptor;
}
  • 使用
@GetMapping("/list")
public Result<IPage<SysTenant>> list(@RequestParam(defaultValue = "1") Long pageNum,
                                         @RequestParam(defaultValue = "10") Long pageSize) {
    Page<SysTenant> page = new Page<>(pageNum, pageSize);
    IPage<SysTenant> tenantPage = sysTenantService.page(page);
    return Result.success(tenantPage);
}

mybatis

必须要引进PageHelper分页插件

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper</artifactId>
    <version>5.3.2</version>
</dependency>
  • 配置类
@Configuration
public class MyBatisConfig {
    @Bean
    public PageInterceptor pageInterceptor() {
        PageInterceptor pageInterceptor = new PageInterceptor();
        Properties properties = new Properties();
        // 数据库
        properties.setProperty("helperDialect", "mysql");
        // 合理化分页(页码<1 查第一页,>总页数 查最后一页)
        properties.setProperty("reasonable", "true");
        pageInterceptor.setProperties(properties);
        return pageInterceptor;
    }
}
  • 使用
// 第一行:开启分页(紧跟在查询前面!)
PageHelper.startPage(1, 10);

// 第二行:正常查询
List<User> list = userMapper.selectList();

// 包装成分页结果
PageInfo<User> pageInfo = new PageInfo<>(list);
mybatis-plus:
  mapper-locations: classpath*:mapper/**/*.xml #所有的mapper文件,所有包含的模块 
  type-aliases-package: s.iot.**.entity #所有的包别名
  configuration:
  	log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: s.iot.core.entity,s.iot.system.entity #所有的包别名
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

classpath 只扫描当前模块的类路径;

classpath* 会扫描所有依赖 JAR 包及当前模块中匹配的路径(即全模块扫描)。

配置包扫描

@MapperScan("s.iot.**.mapper")   // 扫描所有模块的 mapper 包

1. SPU(Standard Product Unit,标准产品单元)

  • 定义:描述一类商品的标准化信息,不包含具体的销售属性(如颜色、尺寸)。
  • 特点:它代表“商品系列”或“产品款式”,同一SPU下的商品具有相同的基本属性(如品牌、型号、功能)。
  • 举例:iPhone 15 Pro Max 是一个 SPU。它不区分颜色和内存,只代表这个型号。

2. SKU(Stock Keeping Unit,库存量单位)

  • 定义:描述一个具体可销售的商品,包含了所有销售属性(如颜色、尺寸、版本)。
  • 特点:它代表“具体的库存单品”,每个SKU对应唯一的条码或编码,用于库存、价格、订单管理。
  • 举例:iPhone 15 Pro Max,黑色,512GB 就是一个 SKU。

关系与区别

维度SPUSKU
含义商品系列/款式具体单品
是否区分颜色/尺寸不区分区分
库存管理不用于库存用于精确库存
举例“华为P50手机”“华为P50,白色,8+256GB”
编码通常无唯一码或共用码每个SKU有唯一条码

简单记忆

  • SPU = 商品的“模板”或“型号
  • SKU = 货架上的“具体哪一件

认证流程

1.从登录接口执行认证:登录接口中创建UsernamePasswordAuthenticationToken

使用流程(最简单)

这是最简单的方式,基于cookie和session的方式,用security原生表单登录。

  • POM文件
<!-- springboot父工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<dependencies>
    <!-- 实体类参数校验 -->
	<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
    <!--security起步依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!--token生成-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
    </dependency>
</dependencies>
  • 实现UserDetailsService接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = new User("admin",passwordEncoder.encode("123456"), Collections.emptyList()); //这里第三个参数不能传null
        return user;
    }
}
  • 配置加密方式
@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

使用流程2

  • 实现UserDetailsService接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = new User("admin",passwordEncoder.encode("123456"), Collections.emptyList()); //这里第三个参数不能传null
        return user;
    }
}
  • 配置类
@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
	@Bean
	public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { //springboot自动装配配置了这个AuthenticationConfiguration,使得创建这个AuthenticationManager,@Bean时能注入进来
    	return authConfig.getAuthenticationManager();
	}
}
  • 自定义登录接口
@PostMapping("/login")
public String login(@RequestBody LoginRequest loginRequest) {
    // 执行认证
    Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                    loginRequest.getUsername(),
                    loginRequest.getPassword()
            )
    );

    // 从认证结果中提取 UserDetails
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    return jwtUtil.generateToken(userDetails);
}

Security+JWT

  • 登录认证,生成token返回给前端,token包含了实现UserDetails的LoginUser信息

    • 认证,UsernamePasswordAuthenticationToken会将当前前端传递过来的用户名密码设置为一个认证token放到认证上下文setContext(authenticationToken)。
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
    AuthenticationContextHolder.setContext(authenticationToken);
    
    • 校验,AuthenticationManager的authenticate()方法会调用UserDetailsServiceImpl实现的loadUserByUsername,判断这次登录的账号密码是否正确。
    • 缓存登录信息,校验成功之后把用户信息放入到缓存。
    • 生成token,jwt工具类将使用实现UserDetails的LoginUser生成token。
  • 后续访问

    • 携带token,token认证过滤器JwtAuthenticationFilter中拿到token
    • 拿到token中LoginUser信息,判断缓存是否有该信息。
    • UsernamePasswordAuthenticationToken生成认证信息设置到认证上下文SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    

JWT 工具类

创建一个工具类,用于生成和解析 JWT:

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(secret.getBytes());
    }

    // 从 token 中提取用户名
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    // 提取过期时间
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    // 生成 token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        // 可以添加额外信息,比如角色
        claims.put("roles", userDetails.getAuthorities());
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }

    // 验证 token
    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

application.yml 中配置密钥和过期时间:

jwt:
  secret: "your-256-bit-secret-key-here-must-be-long-enough"
  expiration: 3600000   # 1小时,单位毫秒

自定义 JWT 认证过滤器

创建一个过滤器,继承 OncePerRequestFilter,对每个请求进行 Token 校验:

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        // 提取 Bearer Token
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        // 如果有用户名且当前未认证
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {
                // 创建认证对象
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                // 设置到安全上下文
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

Spring Security 配置

配置 SecurityFilterChain,禁用 Session 和 CSRF,添加自定义过滤器,并设置权限规则:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // 禁用 CSRF(无状态 API)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**").permitAll()   // 登录、注册接口放行
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

登录接口

创建一个控制器,接收用户名密码,认证成功后返回 JWT:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserDetailsService userDetailsService;

    @PostMapping("/login")
    public String login(@RequestBody LoginRequest loginRequest) {
        // 1. 认证authenticationManager.authenticate方法会调用UserDetailsService的实现做密码校验
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(),
                loginRequest.getPassword()
            )
        );

        // 2. 认证成功后生成 token
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
        String token = jwtUtil.generateToken(userDetails);

        return token;
    }
}

// 简单请求体
class LoginRequest {
    private String username;
    private String password;
    // getters/setters
}

跨域

@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    config.addAllowedOriginPattern("*");
    config.setAllowCredentials(true);
    config.addAllowedMethod("*");
    config.addAllowedHeader("*");
    config.setMaxAge(3600L);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsFilter(source);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
            .csrf().disable()
            .cors().disable()
           .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests()
            .antMatchers("/auth/**").permitAll()
            .antMatchers("/menu/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
}

其实就是注册bean,通过配置文件的方式注册bean到spring容器中。

在 Spring Boot 2.7 及更高版本中, META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件取代了旧版 spring.factories 中的自动装配配置,成为了注册自动配置类的标准方式。

com.wyl.common.redis.configure.RedisConfig
com.wyl.common.redis.service.RedisService

多个模块的项目下,如果引入其他模块的bean,这个bean由于是其他模块的,包不一样,比如smart-system模块引入smart-core模块下的一个RedisConfig。这时候由于springboot包扫码扫码不到这个对象,不会初始化这个bean,就需要引入自动装配配置。

1. JDBC基本使用

1.1 引入必要依赖

<dependencies>
   <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-core</artifactId>
        <version>3.3.2</version> <!-- 更高的mp版本依赖于spring框架,这里只能用3.3.2 -->
    </dependency>
        <!-- sqlite-jdbc依赖 -->
    <dependency>
        <groupId>org.xerial</groupId>
        <artifactId>sqlite-jdbc</artifactId>
        <version>3.49.1.0</version>
    </dependency>
    
        <!--mysql连接 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.42</version>
    </dependency>
    
    <!-- druid数据库连接池-->
	<dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.28</version>
    </dependency>
</dependencies>

注意点:

  • mybatis-plus-core核心依赖的版本不能太高,高版本还依赖部分spring相关功能。
  • sqlite-jdbc依赖

1.2 使用

  • 按照jdbc的标准,首先要配置数据源。
//数据源连接地址。sqlite其实就是路径,例:D://te.db
// 或者内存数据库"jdbc:sqlite::memory:";//直接往内存中读写数据。
private static String dbUrl = "jdbc:sqlite:test.db"; 

//sqlite数据源 实现了DataSource接口,一般jdbc数据源实现这个接口,mysql也是
SQLiteDataSource dataSource = new SQLiteDataSource(); 
dataSource.setUrl(dbUrl);
  • 接下来就是jdbc部分的内容。不管数据源是什么玩意,只要你实现了我jdbc的接口规范。就能使用jdbc操作数据库,接下来编写测试。

    • 注意:

      Statement,每次执行都编译,❌ 不支持占位符,❌ 易受攻击,适用场景DDL、静态 SQL。

      PreparedStatement,预编译,可复用,✅ 支持 ? 占位符,✅ 防止注入,适用场景DML、DQL、动态参数。

public class SqliteDemo001 {
    private static String dbUrl = "jdbc:sqlite:test.db";
    private static SQLiteDataSource dataSource = new SQLiteDataSource();

    public static void main(String[] args) {
        dataSource.setUrl(dbUrl);

    }
    private static void createTable() throws Exception{
        String sql = "CREATE TABLE IF NOT EXISTS user (" +
                "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                "name TEXT NOT NULL, " +
                "age INTEGER, " +
                "email TEXT, " +
                "deleted INTEGER DEFAULT 0" +
                ")";
        //建立jdbc连接,这一步是与数据库建立连接
        Connection connection = dataSource.getConnection();
        //设置自动提交为false,表示不自动提交,自己处理事务,只对本次连接
        connection.setAutoCommit(false);
        //创建statement对象,用于执行sql语句
        Statement statement = connection.createStatement();
        //执行sql语句
        boolean execute = statement.execute(sql);
        //提交事务,不提交不会执行这一次的事务
        connection.commit();
        //关闭连接 自动提交
        statement.close();
        connection.close();
    }
    private static void insertData() throws Exception{
        String sql = "INSERT INTO user (name, age, email) VALUES ('张三', 25, 'zhangsan@example.com')";

        //获取连接
        Connection connection = dataSource.getConnection();
        connection.setAutoCommit(false);
        PreparedStatement statement = connection.prepareStatement(sql);
        int i = statement.executeUpdate();
        System.out.println("插入了" + i + "行数据");
        //提交事务,不提交不会执行这一次的事务
        connection.commit();
        //关闭连接
        statement.close();
        connection.close();
    }
}

2. 结合mybatis-plus

新建实体类

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer age;
    private String email;
    @TableLogic
    private Integer deleted;
}
public class SqliteDemo002 {
    private static String dbUrl = "jdbc:sqlite:test.db";
    private static SQLiteDataSource dataSource = new SQLiteDataSource();
    public static void main(String[] args) throws Exception{
        dataSource.setUrl(dbUrl);

        //mybatis-plus相关配置
        // 事务工厂
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        // 环境配置  "development"参数随便即可
        //一般用于区分不同环境(开发、测试、生产)
        Environment environment = new Environment("development", transactionFactory, dataSource);

        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setEnvironment(environment);

        //开启驼峰映射user_name->userName
        configuration.setMapUnderscoreToCamelCase(true);

        // 添加Mapper,比较重要,orm映射关系,配置
        // spring框架启动时,会自动扫描mapper接口,会做这个配置,这里我们手写
        configuration.addMapper(UserMapper.class);

        //全局策略配置类,逻辑删除、主键生成策略等。 这3行代码可要可不要。
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setBanner(false); //启动mp时,不打印mp启动信息
        configuration.setGlobalConfig(globalConfig);//关联全局策略

        // 创建 SqlSessionFactory,这个是操作数据库的工厂类
        MybatisSqlSessionFactoryBuilder builder = new MybatisSqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(configuration);
        //从工厂中创建连接器
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 获取Mapper
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        List<User> users = userMapper.selectAll();
        System.out.println("用户列表: " + users);

//        User user = new User("张三", 20,"lisi@email.com");
//        int insertResult = userMapper.insert(user);
//        System.out.println("插入结果: " + insertResult);

        sqlSession.commit(); // 提交事务
    }
}

3. 连接池

3.1 基本使用

public class SqliteDemo003 {
    private static String dbUrl = "jdbc:sqlite:test.db";
    public static void main(String[] args) throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dbUrl);
        druidDataSource.setDriverClassName("org.sqlite.JDBC");
        druidDataSource.setInitialSize(1);// 初始连接数
        druidDataSource.setMaxActive(10);// 最大连接数
        druidDataSource.setMinIdle(1);// 最小连接数
        druidDataSource.setMaxWait(3000);// 最大等待时间3000ms

        druidDataSource.setValidationQuery("select 1");//用来确定是否真正连接成功。以及保持连接
        
        DruidPooledConnection connection = druidDataSource.getConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("select * from `user`");
        ResultSet resultSet = preparedStatement.executeQuery();
        while (resultSet.next()) {
            System.out.println(resultSet.getInt(1) + " " + resultSet.getString(2) + " " + resultSet.getInt(3) + " " + resultSet.getString(4));
        }
        resultSet.close();
        preparedStatement.close();
        connection.close();
    }
}

3.2 数据库连接池+mybatis-plus

public class SqliteDemo004 {
    private static String dbUrl = "jdbc:sqlite:test.db";
    public static void main(String[] args) {
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dbUrl);
        druidDataSource.setDriverClassName("org.sqlite.JDBC");
        druidDataSource.setInitialSize(1); // 初始连接数
        druidDataSource.setMaxActive(10); // 最大连接数
        druidDataSource.setMinIdle(1); // 最小连接数
        druidDataSource.setMaxWait(3000); // 最大等待时间3000ms
        druidDataSource.setValidationQuery("select 1"); // 验证

        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        // 环境配置  "development"参数随便即可
        //一般用于区分不同环境(开发、测试、生产)
        Environment environment = new Environment("development", transactionFactory, druidDataSource);

        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setEnvironment(environment);

        //开启驼峰映射user_name->userName
        configuration.setMapUnderscoreToCamelCase(true);

        // 添加Mapper,比较重要,orm映射关系,配置
        // spring框架启动时,会自动扫描mapper接口,会做这个配置
        configuration.addMapper(UserMapper.class);

        //全局策略配置类,逻辑删除、主键生成策略等。
        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setBanner(false);
        configuration.setGlobalConfig(globalConfig);//关联全局策略

        // 创建 SqlSessionFactory,这个是操作数据库的工厂类
        MybatisSqlSessionFactoryBuilder builder = new MybatisSqlSessionFactoryBuilder();
        SqlSessionFactory sqlSessionFactory = builder.build(configuration);
        //从工厂中创建连接器
        SqlSession sqlSession = sqlSessionFactory.openSession();

        // 获取Mapper
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        while(true){
            List<User> users = userMapper.selectAll();
            System.out.println("用户列表: " + users);
            sqlSession.commit();
            Thread.sleep(1000); // 休眠1秒
        }
    }
}

多数据源

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.TransactionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.apache.ibatis.mapping.Environment;

import java.util.List;

public class SqliteDemo004 {
    // 两个数据库的URL
    private static final String DB_URL1 = "jdbc:sqlite:test.db";
    private static final String DB_URL2 = "jdbc:sqlite:test2.db";

    public static void main(String[] args) throws Exception {
        // 创建第一个数据源(test.db)
        DruidDataSource dataSource1 = createDataSource(DB_URL1);
        // 创建第二个数据源(test2.db)
        DruidDataSource dataSource2 = createDataSource(DB_URL2);

        // 为第一个数据库创建 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory1 = createSqlSessionFactory(dataSource1, UserMapper.class);
        // 为第二个数据库创建 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory2 = createSqlSessionFactory(dataSource2, UserMapper.class);

        // 循环查询两个数据库
        while (true) {
            // 查询第一个数据库
            try (SqlSession session = sqlSessionFactory1.openSession()) {
                UserMapper mapper = session.getMapper(UserMapper.class);
                List<User> users1 = mapper.selectAll();
                System.out.println("test.db 用户列表: " + users1);
            }

            // 查询第二个数据库
            try (SqlSession session = sqlSessionFactory2.openSession()) {
                UserMapper mapper = session.getMapper(UserMapper.class);
                List<User> users2 = mapper.selectAll();
                System.out.println("test2.db 用户列表: " + users2);
            }

            Thread.sleep(1000); // 每秒查询一次
        }
    }

    /**
     * 创建并配置 Druid 数据源
     */
    private static DruidDataSource createDataSource(String url) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(url);
        dataSource.setDriverClassName("org.sqlite.JDBC");
        dataSource.setInitialSize(1);
        dataSource.setMaxActive(10);
        dataSource.setMinIdle(1);
        dataSource.setMaxWait(3000);
        dataSource.setValidationQuery("select 1");
        return dataSource;
    }

    /**
     * 构建 SqlSessionFactory
     */
    private static SqlSessionFactory createSqlSessionFactory(DruidDataSource dataSource, Class<?> mapperClass) {
        TransactionFactory transactionFactory = new JdbcTransactionFactory();
        Environment environment = new Environment("development", transactionFactory, dataSource);

        MybatisConfiguration configuration = new MybatisConfiguration();
        configuration.setEnvironment(environment);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.addMapper(mapperClass);

        GlobalConfig globalConfig = new GlobalConfig();
        globalConfig.setBanner(false);
        configuration.setGlobalConfig(globalConfig);

        MybatisSqlSessionFactoryBuilder builder = new MybatisSqlSessionFactoryBuilder();
        return builder.build(configuration);
    }
}

主要是讲解SSE使用和websocket使用

WebSocket

独立协议ws,wss,可以发送文本和二进制。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

STOMP协议

类似订阅发布模式,通过主题可以广播,点对点。

  • 核心配置类
@Configuration
@EnableWebSocketMessageBroker // 启用WebSocket消息代理
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {

    // 注册STOMP端点
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 客户端通过 "/ws" 路径连接WebSocket服务器
        registry.addEndpoint("/ws") // 端点
                .setAllowedOriginPatterns("*") // 允许跨域,生产环境请指定具体域名
                .withSockJS(); // 启用SockJS作为备用方案,以支持不支持WebSocket的旧浏览器
    }

    // 配置消息代理
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 设置消息代理的前缀,客户端订阅的地址需以此开头,如 /topic/public
        registry.enableSimpleBroker("/topic");
        // 设置应用程序前缀,客户端发送消息的地址需以此开头,如 /app/send
        registry.setApplicationDestinationPrefixes("/app");
    }
}
  • 业务控制器
@Controller
public class WebSocketController {

    // 前端发送消息至 /app/sendMessage,将自动路由至此方法
    @MessageMapping("/sendMessage")
    // 方法返回值将被发送给所有订阅了 /topic/public 的客户端
    @SendTo("/topic/public")
    public String processMessage(String message) throws Exception {
        // 你可以在这里进行消息处理,例如存储到数据库
        return "服务端回显: " + message;
    }
}
WebSocketHandler (原始处理器)

比较灵活,需要自定义双方数据交换模式

  • 配置类
@ServerEndpoint("/websocket")
public class WebSocketServer {
 
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("Connection opened: " + session.getId());
        sessions.add(session);
    }
 
    @OnMessage
    public void onMessage(Session session, String message) throws IOException {
        System.out.println("Received message: " + message);
        session.getBasicRemote().sendText("Server received: " + message);
    }
 
    @OnClose
    public void onClose(Session session) {
        System.out.println("Connection closed: " + session.getId());
        sessions.remove(session);
    }
 
    private static final Set<Session> sessions = Collections.synchronizedSet(new HashSet<Session>());
}
  • 启用配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new WebSocketServer(), "/websocket").setAllowedOrigins("*");
    }
}

SSE

比较轻量级的-只有服务端往客户端发送数据的一种协议,基于http,发送文本。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • SseEmitter单体
@RestController
public class SseController {
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();

    @GetMapping("/sse/connect/{userId}")
    public SseEmitter connect(@PathVariable String userId) {
        SseEmitter emitter = new SseEmitter(60_000L); // 超时时间60秒
        emitterMap.put(userId, emitter);

        // 连接关闭或超时后的清理
        emitter.onCompletion(() -> emitterMap.remove(userId));
        emitter.onTimeout(() -> emitterMap.remove(userId));
        emitter.onError(e -> emitterMap.remove(userId));

        // 发送初始连接成功消息(可选)
        try {
            emitter.send(SseEmitter.event().name("init").data("connected"));
        } catch (IOException e) {
            emitter.completeWithError(e);
        }
        return emitter;
    }

    // 主动推送数据
    public void pushToUser(String userId, Object data) {
        SseEmitter emitter = emitterMap.get(userId);
        if (emitter != null) {
            try {
                emitter.send(SseEmitter.event().name("message").data(data));
            } catch (IOException e) {
                // 发送失败表示连接已断开,移除
                emitterMap.remove(userId);
            }
        }
    }
}
  • 分布式

保证分布式sse稳定运行。

连接:当用户携带唯一id连接sse时,负载均衡到一个后端业务服务器进行连接,连接完成之后,将用户信息和服务器ID存入redis

消息推送到前端:定时推送,定时服务实例拿到需要推送的sse连接。通过openfein调用指定的后端业务服务器,实现消息精确发送。

@Component
public class SseManager {
    private final Map<String, SseEmitter> localEmitters = new ConcurrentHashMap<>();
    private final RedisTemplate<String, String> redisTemplate;
    private final String instanceId;

    public SseManager(RedisTemplate<String, String> redisTemplate, 
                      @Value("${instance.id}") String instanceId) {
        this.redisTemplate = redisTemplate;
        this.instanceId = instanceId;
    }

    public SseEmitter connect(String userId) {
        SseEmitter emitter = new SseEmitter(120_000L);
        localEmitters.put(userId, emitter);
        redisTemplate.opsForHash().put("sse:user:node", userId, instanceId);
        redisTemplate.expire("sse:user:node", Duration.ofMinutes(2));

        emitter.onCompletion(() -> {
            localEmitters.remove(userId);
            redisTemplate.opsForHash().delete("sse:user:node", userId);
        });
        emitter.onTimeout(() -> {
            localEmitters.remove(userId);
            redisTemplate.opsForHash().delete("sse:user:node", userId);
        });
        return emitter;
    }

    public boolean pushToUser(String userId, Object data) {
        String targetInstance = (String) redisTemplate.opsForHash().get("sse:user:node", userId);
        if (targetInstance == null) return false;
        if (targetInstance.equals(instanceId)) {
            SseEmitter emitter = localEmitters.get(userId);
            if (emitter != null) {
                try {
                    emitter.send(data);
                    return true;
                } catch (IOException e) {
                    localEmitters.remove(userId);
                    redisTemplate.opsForHash().delete("sse:user:node", userId);
                }
            }
        } else {
            // 远程调用目标实例的推送接口(如 RestTemplate)
            callRemoteInstance(targetInstance, userId, data);
        }
        return false;
    }

    @Scheduled(fixedRate = 30000)
    public void heartbeat() {
        localEmitters.forEach((userId, emitter) -> {
            try {
                emitter.send(":heartbeat\n\n");
            } catch (IOException e) {
                localEmitters.remove(userId);
                redisTemplate.opsForHash().delete("sse:user:node", userId);
            }
        });
    }
}

静态代理

这个Proxyer代理类需要用户手动去创建,一般来说一个需要代理的类一个Proxyer

AOP之类的都用到了动态代理

  • 目标类接口
public interface UserService{
	String getUsername(int id);
}
  • 目标类具体实现
public class UserServiceImpl implements UserService{
	public String getUsername(int id){
		System.out.println("张三:"+id);
		return "张三:"+id;
	}
}
  • 代理类
public class Proxyer{
	private final UserService userService;
	public Proxyer(UserService userService){
		this.userService = userService;
	}
	public String getUsername(int id){
		System.out.println("目标方法执行--------前");
		String username =userService.getUsername(id);
		System.out.println("目标方法执行--------后");
		return username;
	}
}
  • 测试类
public class Test {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();//直接将具体实现传入如果要代理这个接口的其他实现,也可以new其他的实现传入
        Proxyer proxyer = new Proxyer(userService);
        proxyer.getUserName(1);
    }
}

动态代理

  • 目标类接口
与上面静态代理的一样
  • 目标类具体实现
与上面静态代理的一样
  • 代理类
public class MyInvocationHandler implements InvocationHandler {
	private final Object target;

	public MyInvocationHandler(Object target) {
		this.target = target;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		System.out.println("目标方法执行--------前");
        Object invoke = method.invoke(target, args);
        System.out.println("目标方法执行--------后");
        return invoke;
	}
}
  • 测试类
public class Test {
	public static void main(String[] args) throws Exception {
		UserService target = new UserServiceImpl();
		MyInvocationHandler handler = new MyInvocationHandler(target);
		UserService proxy = (UserService) Proxy.newProxyInstance(
				target.getClass().getClassLoader(),
				target.getClass().getInterfaces(),
				handler);
		// 调用目标方法
		proxy.getUserName(1);
	}
}

CGLib动态代理

  • 目标类对象
public class UserService {
    public String getUser(int id) {
        return "wyl:"+id;
    }
}
  • 拦截器
public class UserServiceInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("目标方法执行--------前");
        Object invoke = methodProxy.invokeSuper(o, objects);
        System.out.println("目标方法执行--------后");
        return invoke;
    }
}
  • 测试
public class CglibProxyer {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserService.class);
        enhancer.setCallback(new UserServiceInterceptor());
        UserService userService = (UserService) enhancer.create();
        System.out.println(userService.getUser(1));
    }
}
1. smart-common (通用模块)
   
   - 定位 :系统的基石,存放所有模块共用的工具类和基础定义。
   - 包含内容 :
     - 通用工具类 ( DateUtils , StringUtils , IdUtils )
     - 全局异常处理 ( GlobalExceptionHandler )
     - 统一响应对象 ( AjaxResult , R )
     - 基础实体类 ( BaseEntity )
     - 常量与枚举 ( Constants , Enums )
2. smart-framework (框架核心模块)
   
   - 定位 :配置层,整合第三方框架和系统级配置。
   - 包含内容 :
     - Spring Security / Shiro 权限配置
     - WebMvc 配置 (拦截器、跨域设置)
     - MyBatis Plus / JPA 配置
     - Redis 缓存配置
     - Swagger / Knife4j 接口文档配置
3. smart-system (系统管理模块)
   
   - 定位 :负责平台的基础管理功能(参考 RuoYi 或 JetLinks 的系统管理)。
   - 包含内容 :
     - 用户管理 (User)、角色管理 (Role)、菜单管理 (Menu)
     - 部门/组织架构 (Dept)
     - 系统日志 (Log)、字典管理 (Dict)
     - 通知公告
4. smart-module (业务模块)
   
5. smart-admin (启动模块 / Web 入口)
   
   - 定位 :项目的聚合点和启动入口。
   - 包含内容 :
     - Spring Boot 启动类 ( StartApplication )
     - application.yml 核心配置文件
     - Controller 层(如果希望业务逻辑更内聚,Controller 也可以放在各自的模块中,这里只做聚合打包)。

反射使用

java.lang.reflect

获取Class对象的三种方式

public class Demo001 {
    public static void main(String[] args) {
        Class s = Student.class; //方式1
        System.out.println(s.getName());
        System.out.println(s.getSimpleName());
        
        Class c2 = Class.forName("reflect.Student"); //方式2
        System.out.println(c2.getName());
        
        System.out.println(c1==c2); //返回true,说明是同一个对象
        
        Student s = new Student();//方式3
        Class c3 = s.getClass();
        System.out.println(c3==c2); //返回true,也是同一个对象
    }
}

为什么三种对象创建的方式创建出来的对象都是同一个呢?

Class 对象

​ 在 JVM 中,每个类在整个生命周期中只有一个 Class 对象。只有一份字节码。

类加载器 (ClassLoader) → 加载类 → 创建唯一的 Class 对象 → 缓存到 JVM
堆内存 (Heap):
┌─────────────────────────┐
│   Class 对象 (唯一)     │ ← c1, c2, c3 都指向这里
│  - 类名:reflect.Student│
│  - 字段信息             │
│  - 方法信息             │
│  - 构造函数信息         │
└─────────────────────────┘
          ↑
          │ c1, c2, c3 都指向同一个地址

实例对象

Student s1 = new Student();
Student s2 = new Student();
Student s3 = new Student();

这3种都不是同一个对象。

类构造器(构造方法)

Class c1 = Student.class;
Constructor[] constructors = c1.getConstructors();//获取全部构造方法 只能拿public构造方法
Constructor[] declaredConstructors = c1.getDeclaredConstructors();//获取全部构造方法
Constructor ct1 = c1.getConstructor(String.class, int.class); //获取指定参数的构造方法
Constructor ct2 = c1.getDeclaredConstructor(String.class, int.class); //获取指定参数的构造方法

通过类构造器初始化对象

 Constructor constructor = c1.getConstructor(String.class, int.class); //获取指定参数的构造方法
Constructor constructor2 = c1.getDeclaredConstructor(String.class, int.class); //获取指定参数的构造方法

Student s1 = (Student) constructor.newInstance("张三", 18);

Constructor cs4 = c1.getDeclaredConstructor();
cs4.setAccessible( true); //设置访问权限可以访问private构造方法
Student s2 = (Student) cs4.newInstance();

属性

Class c1 = Student.class;
Field[] fields = c1.getFields();//只能拿public字段
for (Field field : fields) {
    System.out.println(field.getName()+"--"+field.getType());
}
Field[] declaredFields = c1.getDeclaredFields();//获取全部字段
for (Field field : declaredFields) {
    System.out.println(field.getName()+"--"+field.getType());
}
//通过名字定位
//        Field name = c1.getField("name"); //拿不到private
//        System.out.println(name.getName()+"--"+name.getType());

Field age = c1.getDeclaredField("age");
System.out.println(age.getName()+"--"+age.getType());

        //为实例对象赋值
Student s = new Student("11",1);
age.setAccessible(true);
age.set(s,18);
System.out.println(s.getAge());

//取值
int ageValue = (int) age.get(s);
System.out.println(ageValue);

方法

public static void testGetMethod() throws Exception {
        Class c1 = Student.class;
        Method[] methods = c1.getMethods();//获取全部方法 只能拿public方法
        for (Method method : methods) {
            System.out.println(method.getName()+"--"+method.getParameterCount() +"---"+method.getReturnType());
        }
        Method[] declaredMethods = c1.getDeclaredMethods();//获取全部方法
        for (Method method : declaredMethods) {
            System.out.println(method.getName()+"--"+method.getReturnType());
        }
        Method getAge1 = c1.getMethod("getAge"); //获取指定参数的构造方法,只能拿public方法
        Method getAge2 = c1.getDeclaredMethod("getAge"); //获取指定参数的构造方法

        Method setAge1 = c1.getDeclaredMethod("setAge", int.class);
        //方法执行
        Student s = new Student("11",1);

        setAge1.invoke(s,18);
        System.out.println(s.getAge());
        Method show = c1.getDeclaredMethod("show");
        show.setAccessible(true);
        show.invoke(s);
}

反射作用

  • 得到一个类的全部成分,然后操作。

  • 破坏封装性,直接可以调用私有的方法。

  • 主要是在java框架使用

注解使用

​ java里的特殊标记,让其他程序根据注解信息来决定怎么执行该程序。

注解本质是一个接口,继承于Annotation接口。

​ 使用@注解的过程就是创建一个实现类对象。

元注解

修饰注解的注解

  • @Target

    @Target(ElementType.TYOE)
    1.TYPE 类,接口
    2.FIELD 成员变量
    3.METHOD 成员方法
    4.PARAMETER 方法参数
    5.CONSTRUCTOR 构造方法
    6.LOCAL_VARIABLE 局部变量
    
  • @Retention 声明注解的保留周期

    1.SOURCE 只在源码阶段,字节码文件不存在
    2.CLASS 默认值,保留到字节码,运行阶段不存在
    3.RUNTIME 常用,保留到运行阶段
    

注解解析

  • ​ 要解析哪个类上面的注解,就要先拿到他。
  • ​ 先拿到Class对象,再解析上面的注解
  • ​ 拿到方法Method对象,在解析上面的注解。
  • ​ 由于Class,Method,Field,Constructor都实现了AnnotatedElement接口,所以都有解析能力。

测试

创建4个注解

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClassAnnotationTest {
    /**
     * 权限标识(如:system:user:list)
     */
    String value();
    /**
     * 描述
     */
    String description() default "";
}
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldAnnotationTest {
    String value();
}

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MethodAnnotationTest {
    String value();
}

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParameterAnnotationTest {
    String value();
}

创建使用注解的类

@ClassAnnotationTest(value = "admin",description = "测试")
public class AnnotationTest {
    @FieldAnnotationTest("x前缀-")
    private String name;
    private int age;
    @MethodAnnotationTest("system:user:list")
    public void login(){
        System.out.println("登录成功");
    }
    public void test(@ParameterAnnotationTest("参数前缀-") String name){
        System.out.println(name);
    }
}

运行测试类

public class Demo01 {
    //注解解析
    public static void main(String[] args) throws Exception {
//        getClassAnnotation();

//        getMethodAnnotation();

//        getFieldAnnotation();

        getParameterAnnotation();
    }


    public static void getParameterAnnotation() throws Exception {
        Class demo01Class = AnnotationTest.class;
        Method method = demo01Class.getDeclaredMethod("test", String.class);
        ParameterAnnotationTest annotation = method.getParameters()[0].getAnnotation(ParameterAnnotationTest.class);
        System.out.println(annotation.value());
    }

    /**
     * 获取属性注解
     */
    public static void getFieldAnnotation() throws NoSuchFieldException {
        Class demo01Class = AnnotationTest.class;
        //拿到属性全部注解
        Field[] fields = demo01Class.getDeclaredFields();
        for (Field field : fields) {
            System.out.println(field.getName());
        }
        Field name = demo01Class.getDeclaredField("name");
        //拿到属性上面全部注解
        if (name.isAnnotationPresent(FieldAnnotationTest.class)) {
            FieldAnnotationTest annotation = name.getAnnotation(FieldAnnotationTest.class);
            System.out.println(annotation.value());
        }
    }
    /**
     * 获取方法注解
     */
    public static void getMethodAnnotation() throws NoSuchMethodException {
        Class demo01Class = AnnotationTest.class;

        Method method = demo01Class.getDeclaredMethod("login");
        //拿到方法上面全部注解
        Annotation[] annotations = method.getDeclaredAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

        if(method.isAnnotationPresent(MethodAnnotationTest.class)){
            MethodAnnotationTest annotation = method.getAnnotation(MethodAnnotationTest.class);
            System.out.println(annotation.value());
        }
    }
    public static void getClassAnnotation() {
        Class demo01Class = AnnotationTest.class;
        //拿到类上面全部注解
        Annotation[] annotations = demo01Class.getDeclaredAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

        //判断注解是否存在
        boolean annotationPresent = demo01Class.isAnnotationPresent(ClassAnnotationTest.class);
        System.out.println(annotationPresent);
        if(annotationPresent){
            //拿到指定注解
            ClassAnnotationTest checkPermission = (ClassAnnotationTest) demo01Class.getDeclaredAnnotation(ClassAnnotationTest.class);
            System.out.println(checkPermission.value());
            System.out.println(checkPermission.description());
        }
    }
}

模仿Junit框架

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
    String value() default "";
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class AnnotationTest {
    @MyTest
    public void test1(){
        System.out.println("test1");
    }
    @MyTest
    public void test2(){
        System.out.println("test2");
    }
//    @MyTest
    public void test3(){
        System.out.println("test3");
    }

    public static void main(String[] args) throws Exception {
        AnnotationTest annotationTest = new AnnotationTest();
        Class demo01Class = AnnotationTest.class;
        //拿到所有方法
        Method[] methods = demo01Class.getDeclaredMethods();
        for (Method method : methods) {
            //判断方法是否有@MyTest注解
            if(method.isAnnotationPresent(MyTest.class)){
                //执行方法
                method.invoke(annotationTest);
            }
        }
    }
}

鉴权

结合前面学的反射和注解实现一个简单的鉴权框架

①注解

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Perms {
    /**
     * 权限标识
     */
    String value();
}

②数据库

public class RolePermsDBService {
    /*假设从数据库拿到角色权限*/
    public String[] getRolePerms(){
        String[] rolePerms = {"dept:add","dept:find"};
        return rolePerms;
    }
}

③使用注解的对象

public class DeptController {
    @Perms("dept:add")
    public void addDept(){
        System.out.println("添加部门");
    }
    @Perms("dept:update")
    public void updateDept(){
        System.out.println("修改部门");
    }
    @Perms("dept:delete")
    public void deleteDept(){
        System.out.println("删除部门");
    }

    @Perms("dept:find")
    public void findDept(){
        System.out.println("查询部门");
    }
}

④反射处理对象

public class DeptControllerBean {
    DeptController deptController = new DeptController();
    Class demo01Class = DeptController.class;
    private List<String> rolePerms;

    private DeptControllerBean(){
        //初始化时获取数据库中的权限
        RolePermsDBService rolePermsService = new RolePermsDBService();
        rolePerms = Arrays.stream(rolePermsService.getRolePerms()).toList();
    }
    private static final DeptControllerBean instance = new DeptControllerBean();

    public static DeptControllerBean getInstance(){
        return instance;
    }
    private void invoke(String methodName) {
        try{
            Method addDept = demo01Class.getDeclaredMethod(methodName);
            //判断权限
            Perms annotation = addDept.getAnnotation(Perms.class);
            if(annotation!=null){//存在注解
                if (rolePerms.contains(annotation.value())) {
                    addDept.invoke(deptController);
                    return;
                }else{
                    System.out.println("没有权限");
                    return;
                }
            }else{ //不存在注解
                addDept.invoke(deptController);
            }
            addDept.invoke(deptController);
        }catch (Exception e){
            System.out.println("addDept方法不存在");
        }
    }
    public void addDept(){
        invoke("addDept");
    }

    public void updateDept(){
        invoke("updateDept");
    }
    public void deleteDept(){
        invoke("deleteDept");
    }
    public void findDept(){
        invoke("findDept");
    }
}

⑤启动类

public class RunApplication {
    public static void main(String[] args) {
        DeptControllerBean instance = DeptControllerBean.getInstance();
        instance.addDept();
        instance.updateDept();
        instance.deleteDept();
        instance.findDept();
    }
}

步骤

  • 确定springboot版本,这个很重要,微服务项目springcloud和spirngboot版本有一定关系。
  • 确认JDK版本,springboot和jDK版本有关系
  • springcloud版本确认,通过maven版本管理引入spirngcloud。spring-cloud-dependencies
  • 引入spring-cloud-alibaba-dependencies微服务组件的作用,alibaba有更好的配置中心nacos,熔断限流Sentinel,更好的消息中间件MQ。
  • 子项目中就能直接使用spirngcloud相关的组件,比如alibaba的nacos,springboot本来就支持的gateway,loadbanlace等组件,而且子项目引入相关依赖不需要指定版本。
  • 设置打包项目,版本

顶层父pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>psn</groupId>
    <artifactId>smart-iot</artifactId>
    <version>1.0</version>
    <modules>
        <module>smart-gateway</module>
    </modules>
    <packaging>pom</packaging>

    <!-- 确认springboot版本 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.6</version>
    </parent>

    <!-- 配置项目版本属性 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <mysql.version>8.0.33</mysql.version>
        <mybatis-plus.version>3.5.7</mybatis-plus.version>
        <lombok.version>1.18.20</lombok.version>
        <spring.cloud.version>2024.0.0</spring.cloud.version>
        <alibaba.cloud.version>2023.0.3.2</alibaba.cloud.version>
    </properties>
    <!-- 配置依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <!-- mysql -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>

            <!-- mybatis-plus -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

            <!-- spring-cloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba.cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!--所有子项目共有依赖-->
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
    </dependencies>

    <!--打包插件-->
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.1</version>
                    <configuration>
                        <source>${maven.compiler.source}</source>
                        <target>${maven.compiler.target}</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

子模块pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>smart-iot</artifactId>
        <groupId>psn</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>smart-gateway</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>
    <dependencies>
        <!--nacos 注册发现 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!-- 负载均衡 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>

    <!-- 打包springboot项目 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Linux common

  • 快捷方式

    • 文件夹
    ln -s /root/abc ./abc/
    
    • 文件
    ln -s /root/.bashrc ./.bashrc
    

    -s : 软链接,不输入这个就是硬链接

    • 删除,直接删文件或者文件夹的链接(不要删源文件)
  • useradd

基本语法sudo useradd [选项] <用户名>

常用选项

选项说明重要程度
-m自动创建用户的家目录(如 /home/用户名强烈推荐
-s指定用户登录后使用的Shell,通常为 /bin/bash强烈推荐
-d自定义家目录的路径可选
-G指定附加用户组,多个组用逗号分隔可选
-r创建一个系统用户(通常用于运行服务,无家目录)特殊用途
find [搜索路径] [表达式]
  • 搜索所有目录下的a-*.txt
find -name "a-*.txt"
  • 查找当前目录下,不区分大小写
find . -iname "a-*.txt"

scp

​ 两个主机之间通过网络复制文件(注意有时候不用-i

//将当前目录下的a.txt文件复制到192.168.1.101主机的/root目录
scp -i ./a.txt root@192.168.1.101:/root

//将主机192.168.1.101的/root/b.txt文件复制到当前主机的当前目录
scp -i root@192.168.1.101:/root/b.txt ./

报错不允许ssh-rsa

scp -o HostKeyAlgorithms=+ssh-rsa ./a.txt root@192.168.1.101:/root

sshpass(注意有时候不用-i

sudo apt update
sudo apt install sshpass

//将当前目录下的a.txt文件复制到192.168.1.101主机的/root目录    并且做密码校验
sshpass -p passwrod scp -i ./a.txt root@192.168.1.101:/root
//将主机192.168.1.101的/root/b.txt文件复制到当前主机的当前目录   并且做密码校验
sshpass -p password scp -i root@192.168.1.101:/root/a.txt ./

screen

​ linux创建后台终端命令神器,通过这个命令可以后台启动一个终端,常用于一些后台命令,比如内网穿透工具 frp

// 创建一个名为frp的后台终端并且进入这个终端中
screen -S frp
// 按住ctrl 然后按a 再按d 退出当前的终端。(ps在主终端也能用,会直接断开远程连接)

//查看已经有的screen 终端 (这个命令可以知道是不是在某个终端中)
screen -ls 

screen-ls1

Attached: 你当前正在该screen会话内。
Detached: 你在其他地方,或者没有附加到此 screen 会话。
R (Detached): 分离状态,并且可能无法恢复。
无状态信息: 该 screen 会话的状态不明,极大概率已经结束。
Dead: 极罕见, screen session 已经退出, 但可能残留了一些信息。
//进入一个终端
screen -r frp

脚本第一个行必须是 #!/bin/bash或者 #!/bin/sh

#! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell。

变量

定义
name="wyl"  #注释,注意等号两边最好不要添加空格
#和其他语言定义类似,
使用
name="wyl"
age=18

echo $name
echo ${name} #加花括号主要是边界问题比如
echo "my name is ${name}"

echo $age
只读变量
name="wyl"
readonly age=18

echo "my name is ${name}"
name="lisi"
echo "my name is ${name}"
echo $age

编辑 SSH 配置文件:

sudo vim /etc/ssh/sshd_config

找到并修改:

#PermitRootLogin prohibit-password

改为:

PermitRootLogin yes

重启 SSH 服务:

sudo systemctl restart ssh

/etc/rc.local

在这个文件下添加要启动的文件就好,注意要加&符号后台启动

#延迟3s后执行
sleep 3 #3s
#建议先给权限
chmod 755 /home/root/lvgl/lvgl_demo

/home/root/lvgl/lvgl_demo &

Linux 解压与压缩专题(常见文件格式)

文件格式解压命令压缩命令说明
.tartar -xvf file.tartar -cvf archive.tar file/仅打包,不压缩
.tar.gz / .tgztar -xzvf file.tar.gztar -xzvf file.tgztar -czvf archive.tar.gz file/gzip 压缩
.tar.bz2 / .tbz2tar -xjvf file.tar.bz2tar -xjvf file.tbz2tar -cjvf archive.tar.bz2 file/bzip2 压缩(压缩率更高)
.tar.xztar -xJvf file.tar.xztar -cJvf archive.tar.xz file/xz 压缩(压缩率高,较慢)
.tar.Ztar -xZvf file.tar.Ztar -cZvf archive.tar.Z file/古老的 compress 压缩
.gzgunzip file.gzgzip -d file.gzgzip file (原文件会被替换) 保留原文件:gzip -c file > file.gz单文件压缩
.bz2bunzip2 file.bz2bzip2 -d file.bz2bzip2 file (原文件会被替换) 保留原文件:bzip2 -c file > file.bz2单文件压缩
.xzunxz file.xzxz -d file.xzxz file (原文件会被替换) 保留原文件:xz -c file > file.xz单文件压缩
.zipunzip file.zipzip -r archive.zip file/常见跨平台格式,可保留目录结构
.rarunrar x file.rar (需安装 unrarrar x file.rar (需安装 rarrar a archive.rar file/ (需安装 rar商业压缩格式,Linux 需额外软件
.7z7z x file.7z (需安装 p7zip7z a archive.7z file/高压缩率,开源
.lzlzip -d file.lzlzip -c file > file.lzLZMA 算法,类似 xz
.lz4lz4 -d file.lz4lz4 file (原文件保留在 file.lz4极快速压缩
.zstunzstd file.zstzstd -d file.zstzstd file (原文件保留) 压缩目录:先 tarzstdZstandard,高压缩比+高速

使用 Netplan (适用于Ubuntu 18.04及以后版本)

这是Ubuntu官方推荐的配置方式。

  1. 编辑配置文件/etc/netplan/ 目录下的文件名可能不同(如 01-netcfg.yaml, 50-cloud-init.yaml),建议先查看目录下的文件。

    bash

    sudo vi /etc/netplan/01-netcfg.yaml
    

配置

network:
  ethernets:
    ens33:                #网卡名
      dhcp4: false        #no/false  yes/true     关闭DHCP
      addresses: [192.168.1.100/24]
      gateway4: 192.168.1.1
      nameservers:
        addresses: [8.8.8.8, 114.114.114.114]
  version: 2
  renderer: networkd   # 服务器环境用networkd,桌面版可用NetworkManager[reference:21]
  

创建

  1. 检查现状 确认当前无Swap,并确保有足够磁盘空间。
swapon --show
df -h /
  1. 创建Swap文件 创建大小为2GB的Swap文件。
fallocate -l 2G /swap 
  1. 第2步,有些文件系统不支持fallocate指令,使用dd以下命令 2048为2g
dd if=/dev/zero of=/swap bs=1M count=2048 status=progress
  1. 设置权限 锁定文件权限,防止被随意读取。
chmod 600 /swap
  1. 格式化 将文件标记为交换空间格式。
mkswap /swap
  1. 立即启用 激活Swap。
swapon /swap
  1. 验证启用 检查Swap是否成功启用并查看大小。
swapon --show
free -h
  1. 永久生效 配置系统启动时自动挂载此Swap。
echo '/swap none swap sw 0 0' | sudo tee -a /etc/fstab
  1. 设置交换积极性
echo 'vm.swappiness=60' | sudo tee -a /etc/sysctl.conf
  1. 确定交互积极性
sysctl -p

移除Swap

  1. 查看当前状态 确认正在使用的Swap文件路径。
sudo swapon --show
  1. 停用Swap 关闭所有Swap交换空间。
sudo swapoff /swap
  1. 验证已停用 确认Swap已完全关闭。
sudo swapon --show
free -h
  1. 删除自动挂载 从系统配置中移除Swap,使其开机不启动。
打开文件 vim /etc/fstab  
删除这行 /swap none swap sw 0 0
  1. 删除Swap文件 永久删除磁盘上的文件以释放空间。
rm /swap
  1. (可选)清理内核参数 移除之前调整的swappiness优化设置。
打开文件 vim /etc/sysctl.conf
修改vm.swappiness=0
  1. 重启生效
sysctl -p

方式一

ubuntu系统下,环境变量的文件在/etc/profile修改完成之后执行source /etc/profile命令。

vim /etc/profile
export PATH=$PATH:/root/embedded/gcc-arm-none-eabi/bin
source /etc/profile

如果直接暴露 export PATH=$PATH:/root/embedded/sdcc/bin,针对的是每次ssh连接,ssh连接断开之后会删除。

方式二

1.编辑用户目录下的 .bashrc 文件

vim ~/.bashrc

2.在文件末尾添加下面这行:

export PATH=$PATH:/root/embedded/sdcc/bin

3.配置立即生效

source ~/.bashrc

Mqtt相关

version: '3'
services:
  emqx:
    image: emqx/emqx:5.8.6
    container_name: emqx-mqtt
    ports:
      - "1886:1883"    # MQTT 端口
      - "8886:8883"    # MQTT over SSL
      - "8086:8083"    # WebSocket 端口
      - "18086:18083"  # Web 管理后台端口
      - "8087:8084"    # WebSocket SSL 端口
      - "18087:18084"  # Dashboard SSL 端口
    environment:
      - EMQX_ALLOW_ANONYMOUS=off
      - EMQX_HTTP_BASIC_AUTH_USERNAME=admin
      - EMQX_HTTP_BASIC_AUTH_PASSWORD=YH_6688
      - EMQX_DASHBOARD__DEFAULT_PASSWORD=YH_6688
      - EMQX_NAME=emqx
      - EMQX_HOST=localhost
      # ---------- MQTT over SSL (8883) ----------
      - EMQX_LISTENERS__SSL__DEFAULT__BIND=8883
      - EMQX_LISTENERS__SSL__DEFAULT__SSL_OPTIONS__CERTFILE=/etc/emqx/certs/fullchain.pem
      - EMQX_LISTENERS__SSL__DEFAULT__SSL_OPTIONS__KEYFILE=/etc/emqx/certs/privkey.pem
      # WebSocket SSL 配置
      - EMQX_LISTENERS__WSS__DEFAULT__BIND=8084
      - EMQX_LISTENERS__WSS__DEFAULT__SSL_OPTIONS__CERTFILE=/etc/emqx/certs/fullchain.pem
      - EMQX_LISTENERS__WSS__DEFAULT__SSL_OPTIONS__KEYFILE=/etc/emqx/certs/privkey.pem
      # Dashboard HTTPS 配置
      - EMQX_DASHBOARD__LISTENERS__HTTPS__DEFAULT__BIND=18084
      - EMQX_DASHBOARD__LISTENERS__HTTPS__DEFAULT__SSL__CERTFILE=/etc/emqx/certs/fullchain.pem
      - EMQX_DASHBOARD__LISTENERS__HTTPS__DEFAULT__SSL__KEYFILE=/etc/emqx/certs/privkey.pem
    volumes:
      - ./emqx.conf:/etc/emqx/emqx.conf
      - ./emqx_data:/opt/emqx/data
      - ./certs:/etc/emqx/certs   # 挂载证书目录
    restart: unless-stopped
MqttClient mqttClient;

设置主题回调(所有主题都走这个接口实现)

mqttClient.setCallback(new MqttAllTopicCallback());

public class MqttAllTopicCallback implements MqttCallback{

}

设置主题回调(规定什么主题走什么接口实现)

mqttClient.subscribe("sys/#", 1,new MqttRuleListner()); //规定sys/#主题走MqttRuleListner
public class MqttRuleListner implements IMqttMessageListener{
	
}
//注意这种方式不能走共享订阅,$queue和$share

Mqtt基于TCP协议每次信息收发都会有完成的mqtt协议内容,包括:主题,qos状态等等内容。

Mqtt3个对象:

发布者、broker(代理)、订阅者

broker作为中心,负责消息的调度。发布者订阅者连接到broker,broker负责监听二者在线状态,二者通过连接协议中的心跳时间作为连接后的离线判断。发布者发送消息到主题:将该条消息发到broker。broker负责信息推送给其他的订阅者。如果有多个订阅者订阅了这条主题,broker会向这些订阅者推送信息。

Qos 0

根据报文可知,当发送者向broker(代理)时,broker是否收到。或者broker向订阅者发送信息,订阅者是否收到都不关心。

qos0

Qos 1

qos1

QOS 2

qos2

单主题方式

  • json方式上报

​ 设备上报所有内容都走一个主题,这个主题中的内容通过json体里面的code值进行判断要执行操作类型

{"code":400,"status":1}//code 为 400,设备心跳
  • 16进制Hex方式上报

​ 这种一般用于透传,一些485设备,比如温湿度传感器,振动传感器等通信设备使用的协议直接是16进制的方式。这些模块连接wifi透传或者4G透传进行数据上报和采集。通常后端使用定时器进行定时采样,下发的是16进制指令。

多主题方式

Opencv相关

下载

https://opencv.org/releases/

文档

https://docs.opencv.org/4.12.0/

安装配置系统环境

Path 添加 D:\Program Files\opencv\build\x64\vc16\bin

vs使用

选择项目—>属性—>VC++目录—>包含目录—>编辑

001

002

项目—>属性—>链接器—>输入—>附加依赖项—>编辑
  • opencv_world4120.lib:Release 版本,用于最终发布的程序。
  • opencv_world4120d.lib:Debug 版本,用于开发调试阶段。(只需要填这个就好)

003

测试使用

#include <opencv2\opencv.hpp> 
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
	Mat img;
	img = imread("D:/111.png");
	if (img.empty())
	{
		cout << "null" << endl;
		return 0;
	}
	imshow("test", img);
	waitKey(0);
	return 0;
}

POI

感兴趣区域,一般用来狂出物体的边界。

Mask

黑白图

HSV:

H——Hue即色相,就是我们平时所说的红、绿,如果你分的更细的话可能还会有洋红、草绿等等;在HSV模型中,用度数来描述色相,其中红色对应0度,绿色对应120度,蓝色对应240度。

S——Saturation即饱和度,色彩的深浅度(0-100%) ,对于一种颜色比如红色,我们可以用浅红——大红——深红——红得发紫等等语言来描述它(请原谅一个纯理科生的匮乏的颜色系统),对应在画水彩的时候即一种颜料加上不同分量的水形成不同的饱和度。

V——Value即色调,纯度,色彩的亮度(0-100%) ,这个在调整屏幕亮度的时候比较常见。

差值计算

对于突然出现的物品做出处理,

  • MOG2

  • KNN

背景差分

目标跟踪

移动路径

二维码识别

人脸识别

1

特征工程

特征提取

windows安装py和pip

安装yolo

pip install -U ultralytics
C:\Users\24346>yolo --version
WARNING argument '--version' does not require leading dashes '--', updating to 'version'.
8.4.40

使用测试yolo

命令行输入

yolo predict model=yolov8n.pt source="图片路径" project="结果路径" name="结果名称"
  • model标识模型:这个模型文件必须要和打开的命令行存放路径一致。如果没有会主动去拉取文件模型文件。

  • source:图片路径

  • project:结果路径

  • name:输出的文件名

yolo能这些模型能识别80多种常见的物品

使用摄像头

0代表0号摄像头,会在命令行打印识别到的内容

yolo predict model=yolov8n.pt source="0"

实时显示画面

yolo predict model=yolov8n.pt source="0" show=True

参数

model指定模型文件路径model=yolo11n.pt
source指定输入源(图像、视频、文件夹等)source=test.jpg
data指定数据集配置文件(.yaml)data=coco8.yaml
epochs训练轮数epochs=100
imgsz输入图像尺寸imgsz=640
batch批量大小batch=16
conf置信度阈值conf=0.25
device指定运算设备device=cpudevice=0
format导出模型格式format=onnx

模型微调 数据微调

  1. 安装环境

    pip install ultralytics  # YOLOv8/v11
    # 或
    git clone https://github.com/ultralytics/yolov5
    pip install -r requirements.txt
    
  2. 准备数据集

    • 格式:每张图片对应一个 .txt 标注文件(YOLO格式:class x_center y_center width height,归一化值)。

    • 目录结构示例:

      dataset/
        images/
          train/  # 训练图8/10
          val/    # 验证图2/10
        labels/
          train/  # 训练图对应标注
          val/    # 验证图对应标注
      
    • 创建 data.yaml

      yaml

      path: ../dataset
      train: images/train
      val: images/val
      nc: 2  # 类别数
      names: ['cat', 'dog']
      

开始训练

yolo task=detect mode=train data=data.yaml model=yolov8n.pt epochs=100 imgsz=640
参数含义常见值
epochs训练轮数100~300
imgsz输入图片尺寸640
batch批量大小根据显存调整(16, 32, 64)
lr0初始学习率0.01(YOLOv5)
deviceGPU编号0(第一块GPU)
patience早停轮数50~100

OTA

接口与单片机连接。

  • 接口是抽象接口,可以是CAN、uart,也可以其他。
  • 接口对应的上位设备,可以是pc上位机、lora模块、4G模块、wifi模块、蓝牙模块等。

上位设备分成透传设备、需进一步协议处理设备(如,AT指令设备),还有支持二度开发的上位设备。

  • 透传设备,单片机直接处理上位设备发过来的内容,这种只需要规定协议格式就好,比如json格式,比如自定义格式(1标头-2字节是长度-3数据)。
  • 协议处理设备,单片机需要进一步解析的设备,比如AT指令,需要解析AT指令之后拿到真正的数据,再度解析真正数据,需要两步解析。AT指令是比较完善的指令,不会出现粘包,少包问题。
  • 二度开发的设备,这个设备通常需要二度开发,其实就是AT指令的本身,相当于封装了一种AT指令,但是不会定义成AT指令,只会模仿。这种设备通常是一个辅助MCU。

升级方式

方式1

​ 设备上报运行时状态才用MQTT,如果涉及到OTA升级,应该尽量使用HTTP的方式,也就是说,单片机的上位设备模块退出MQTT长连接模式,切换成HTTP短连接模式。

流程:接收到mqtt发送过来的升级指令,或者自己比对版本发现差异,主动断开MQTT连接,切换成HTTP方式。分段下载更新,完成之后,重新建立MQTT连接。

方式2

​ 单片机资源少,ram少,而且又没有外挂flash,上位设备不想进行mqtt和http连接切换。只使用MQTT构建一个自定义的可靠报文传输协议QoS 1(确保消息至少到达一次),并在设备端做幂等处理

协议规定

OTA升级协议规定。远程升级。

方式1

​ 这个比较简单,只要把文件传输到oss服务器中就行了,当设备请求后端服务器之后①,返回oss下载链接,通过下载链接分块下载内容。对后端服务器开销很小。第①步记录更新开始,单片机更新完成之后再调用后端接口记录更新结束。

单片机端定义结构体,struct upgrader,包含,第几个块blank,长度firmware_size,crc32,

方式2

​ 全程MQTT,这个比较有挑战。需要后端全程接管,每次处理单片机数据都要从oss中分块拿到数据,再封装返回给单片机。

自定义二进制协议(例如 1 字节类型 + 2 字节块号 + 数据)

**json方式:**1.数据块用数组的方式传输。2.使用base64的方式传输。

数据协议规定:

  • 设备端:
  • 服务端:

要求

管理升级包,根据产品型号。分批更新之类的。

设备端HTTP方式进行OTA

​ 类似OSS服务器

设备端MQTT方式进行OTA

​ 实现自定义可靠传输协议

核心名词

针对单片机

  • 内部flash:单片机的内部代码存储器。

  • 外部flash:外挂的flash。SPI NOR Flash的W25q64、I2C EEPROM的AT24C系列

  • RAM:单片机的运行内存。

单片机升级方式

bootloader + app启动方式,正常情况下上电启动时bootloader负责判断是否要升级app版本。IAP功能对app分区进行擦除和写入。还有一种情况,app区代码在执行时收到更新指令,直接软件复位重启。

  • 单分区不备份:

    适用范围:内部flash很小,没有外挂flash。

    流程:1.bootloader判断进入升级,2.从串口拿到升级的信息,对app内容擦除写入。

    适用场景:基本没什么使用商业场景,风险还大,用来学习boot+app这个概念,一旦没完成完整的升级就会出现死机,所以必须要求有个存储标志位用来判断是否是完整的app代码。读写flash后部分没有占用的分区页。

  • 单分区+备份:

    适用范围:内部flash够大,没有外挂flash,内部flash比代码大小大起码两倍。

    流程:1.bootloader判断进入升级,2.备份当前app代码到备份区,3.从串口拿到升级信息,对app内容擦除写入。

    适用场景:升级不频繁。

  • 外挂flash备份:

    适用范围:这个需要设计电路时添加外置flash存储器。

    流程:1.app运行是判断是不是要进行升级,2.通过串口拿到升级信息写入到外置flash中,并且版本信息也要写入到外置flash,3.写完之后,软件复位,bootloader通过外置flash中版本信息判断是否要升级,4.bootloader从外置flash中拿到升级信息对app内容擦除写入。

    适用场景:ota升级最容易出现问题就是网络,写到外部的flash,保证升级内容完整,再对现有的内容擦除写入更新不容易出错。

  • 外挂flash多备份:

    与"外挂flash备份"基本一致,就是多了几层备份,会备份多个版本。

  • 直接烧录替换:

    这个不是ota,但是有些c端用户设备,可以提供一个上位机软件,一个比较适合用户操作的界面,让用户通过这个软件对设备进行更新升级。而不是用什么stc-icp之类的软件。这个软件会通过串口拿到设备的版本信息,同时软件从服务器中拿到最新版本的升级bin文件。

    软件设计要求:1.一键安装ch340驱动,把ch340驱动放到软件目录下,软件中一个按钮(安装驱动)点击按钮后拉起ch340驱动软件进行安装。2.自动识别串口,要求用户先拔掉与设备连接的usb,识别到当前串口数量和串口名称信息,用户插入设备连接usb之后,判断新增的串口,打开新增的串口,发送握手协议,拿到设备的信息。剩下的就是烧录了。

    boot和app需求:可以做boot和app分区,也可以不做。

  • 内存卡升级:

    适用范围:要求电路设计时规划内存卡插槽。

    流程:1.插上内存卡。2.手动断电重启。3.bootloader判断是否有内存卡,并且内存卡内容是否正确,防止随意的内存卡都修改app分区,如果内存卡数据有问题1s钟后自动启动app分区内容,不做修改。4.内存卡数据正确,擦除和写入app分区。

    上位机软件要求:内存卡数据应该在前部分数据就说明本次版本信息。比如verson+crc+data这种方式写入内存卡。上位机软件写卡流程,1.从服务器拿到版本信息,拿到bin文件。2.写入内存卡,将版本信息和校验和数据等内容烧到内存卡。

Qt c++

引入qss文件并设置qss样式的方式

创建在资源文件中创建blueButton.qss文件。

QPushButton {
    background-color: white;
    color: #2196F3; /* 蓝色文字 */
    border: 2px solid #2196F3; /* 蓝色边框 */
    border-radius: 25px;
    max-height: 50px;
    min-height: 50px;/*固定按钮大小*/
    font-weight: bold;
}

QPushButton:hover {
    background-color: #e7f3ff; /* 淡蓝色背景 */
}

QPushButton:pressed {/*按下时的颜色*/
    background-color: #cce6ff;
}
QPushButton:checked {/*被选中时的颜色*/
    background-color: #cce6ff;
}

QFile qssFile(":/qss/blueButton.qss");
QString styleSheet;
if(qssFile->open(QFile::ReadOnly|QFile::Text))
{
      QTextStream stream(qssFile);
      styleSheet = stream.readAll();
      qssFile->close();
      Debug() << "样式表加载成功:";
 }else{
      qDebug() << "无法打开样式表文件:" << qssFile->errorString();
 }
btn->setStyleSheet(styleSheet);

多个按钮的情况


注意:

必须要将必要文件放放到Gwing相应目录,include放入QtMqtt头文件 lib放入连接文件 bin放入dll文件

链接:QT5.12编译MQTT 5.13图文详细版_qt 5.12.12对应qwt-CSDN博客

mqtt源码

码云:
https://gitee.com/mathematicsX/qtmqtt/tree/5.12.9/
github:
https://github.com/qt/qtmqtt.git

1.拉下来

git clone https://github.com/qt/qtmqtt.git

mqtt01

2.*切换分支

git checkout 5.12.9

3.qt creator 打开这个项目

mqtt02

4.构建

5.构建完成后

**注意区分是debug构建还是release构建。

**如果构建和引用错了,会连接不上mqtt服务器

mqtt03

6.生成文件使用

1.放到qt目录,后面就能直接使用哦

1.在QT安卓目录下的include新建QtMqtt目录,将,mqtt相关头文件放入这个新建的目录。
	举例:"D:/Qt/Qt5.12.9/5.12.9/mingw73_32/include/QtMqtt"
2.将生成上面图片中6个文件放到QT的bin目录。其实只需要.a和.dll文件即可。
	举例:"D:/Qt/Qt5.12.9/5.12.9/mingw73_32/bin"
3.在项目中引用
	## MQTT包含路径(绝对路径)
	INCLUDEPATH += "D:/Qt/Qt5.12.9/5.12.9/mingw73_32/include/QtMqtt"
	## MQTT链接库(绝对路径)
	LIBS += -L"D:/Qt/Qt5.12.9/5.12.9/mingw73_32/bin" -lQt5Mqtt
	
	#include "QtMqtt/qmqttclient.h"

2.使用绝对路径

1.在要使用qt项目的绝对路径下新建include和lib文件夹。
2.将构建完成的include所有.h文件复制到上面新建的include文件
	例如:D:\Qt\build-qtmqtt-Desktop_Qt_5_12_9_MinGW_32_bit-Debug\include
	复制到 要使用的项目的include文件夹
3.复制上面图片中构建完成的6个文件放入lib目录,其实只需要.a和.dll文件即可。
4.在项目中引用
	INCLUDEPATH += $$PWD/include
	LIBS += -L$$PWD/lib -lQt5Mqtt
	
	#include "QtMqtt/qmqttclient.h"

7.导入(在.pro文件中引入)

## MQTT包含路径(绝对路径)
#INCLUDEPATH += "D:/Qt/Qt5.12.9/5.12.9/mingw73_32/include/QtMqtt"
## MQTT链接库(绝对路径)
#LIBS += -L"D:/Qt/Qt5.12.9/5.12.9/mingw73_32/bin" -lQt5Mqtt

# 添加包含路径 (使用相对路径)
INCLUDEPATH += $$PWD/include

## 添加库路径 (使用相对路径)
LIBS += -L$$PWD/lib -lQt5Mqtt

# 添加库路径 (使用相对路径)
#win32:CONFIG(release, debug|release): LIBS += -L$$PWD/lib -lQt5Mqtt
#else:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/lib -lQt5Mqtt

第一步:

打开qt的cmd终端界面;

打包1

第二步:

找到项目release的地址信息,注意必须是release,选debug打包会很大

打包2

第三步:

执行打包命令

打包3

Rbac和Saas

rbac权限管理框架

最基本的五个表格

  • 用户

  • 角色

  • 权限(菜单)

  • 用户角色

  • 角色权限

设计

用户表(sys_user)

字段名数据类型注释关系&默认
idint主键id递增,不为空
username用户名
password登录密码
create_time创建时间

角色表(sys_role)

字段名数据类型注释关系&默认
id递增,不为空
name
create_time

权限表(菜单sys_menu)

字段名数据类型注释关系&默认
id
name
perm菜单
parent_id父菜单
route前端路由

用户角色表(sys_user_role)

字段名数据类型注释关系&默认
id
user_id
role_id

角色权限表(sys_role_menu)

字段名数据类型注释关系&默认
id
role_id
perms_id

概念

SaaS(Software as a Service,软件即服务)是一种基于云的软件交付模式。在该模式下,服务提供商将应用程序托管在云端,并通过互联网向用户提供服务。用户无需在本地安装和维护软件,只需通过浏览器或客户端访问即可使用相应功能,通常按订阅制或使用量计费。

SaaS 的核心特点包括:

  • 多租户架构:单一软件实例可为多个客户(租户)服务,实现数据与配置的逻辑隔离。
  • 集中部署与维护:应用由服务商统一更新、维护,用户始终使用最新版本。
  • 按需订阅:用户可根据实际需求选择服务套餐,灵活扩展或缩减功能。
  • 跨平台访问:支持通过互联网在各种设备上使用,促进协作与移动办公。

SaaS 模式广泛用于企业管理、客户关系管理、协同办公、人力资源、财务管理等领域,显著降低了企业的IT投入与运维成本。


系统角色

SaaS 平台通常涉及以下三类核心系统角色,分别对应不同的职责与权限范围:

1. 运维系统管理员

  • 职责说明:作为平台的技术运营方或开发团队代表,拥有系统最高权限。
  • 主要权限
    • 管理所有租户(企业)账户的生命周期,包括创建、修改、停用等;
    • 配置和维护全局系统参数、安全策略、日志监控等;
    • 根据套餐方案为各企业分配可用的功能模块与菜单权限;
    • 处理系统级别的异常与故障。

2. 企业管理员

  • 职责说明:代表一个租户(企业),通常由运维系统管理员在创建该租户时同步初始化生成。
  • 创建流程
    • 由运维系统管理员创建租户记录,并关联对应套餐;
    • 系统自动在用户表中生成该企业的管理员账户,并分配专属角色;
    • 该角色权限依据所选套餐预先配置,包含相应的菜单与操作权限。
  • 主要权限
    • 管理本企业内的用户账号,包括创建、编辑、禁用等;
    • 为企业内部用户分配角色,并控制其可访问的功能范围;
    • 查看本企业的使用数据与操作日志。

3. 企业用户

  • 职责说明:隶属于某一特定租户(企业)的终端使用者。
  • 账号来源
    • 通常由企业管理员创建并赋予角色;
    • 也支持自主申请加入企业,例如申请成为“运营人员”“开发人员”“普通员工”等角色。
  • 权限获取
    • 用户角色及相关权限由企业管理员或具备审核权限的用户审批后分配;
    • 获得角色后,即可访问其权限范围内的系统功能。

表格

租户表(tenant)
字段名数据类型注释约束
idbigint主键id不为空
namevarchar(50)租户名称
package_idbigint套餐id外键,关联租户套餐表
租户套餐表(tenant_package)
字段名数据类型注释约束
idbigint主键id不为空
menu_idstext菜单id集合存储格式:逗号分隔或JSON数组
菜单表(menu)
字段名数据类型注释约束
idbigint主键id不为空
namevarchar菜单名称
permvarchar权限标识如:sys:user:list
角色表(role)
字段名数据类型注释约束
idbigint主键id不为空
namevarchar角色名称
tenant_idbigint租户id外键,关联租户表
角色菜单关联表(role_menu)
字段名数据类型注释约束
idbigint主键id不为空
menu_idbigint菜单id
role_idbigint角色id

业务流程说明

一、创建租户

  1. 选择套餐 运营人员在前端为待创建租户选择一个已配置好的套餐(即 tenant_package 中的一条记录)。

  2. 创建租户管理员账号 系统自动创建一个用户账号,该账号默认为当前租户的管理员。

  3. 创建租户管理员角色 系统为当前租户新增一条角色记录,角色名称建议为“租户管理员”或“{租户名称}_admin”,并关联 tenant_id

  4. 初始化角色菜单权限 根据所选套餐中的 menu_ids(菜单ID集合),系统批量将菜单与角色的关联关系插入 role_menu 表,完成管理员角色的权限初始化。

    说明:租户管理员角色的权限来源于租户套餐,通过角色与菜单的关联实现,为后续权限扩展(如新增子角色)奠定基础。


二、修改套餐配置

  1. 更新套餐定义 运营人员修改 tenant_package 表中的 menu_ids 字段内容(例如新增或移除某些菜单ID)。
  2. 查询受影响的租户 系统查询所有 tenant 表中 package_id 指向该套餐的租户记录。
  3. 遍历租户并同步权限 对每个受影响的租户执行以下操作:
    • 获取该租户的“租户管理员”角色(或所有需要同步的角色)。
    • 获取当前角色的已有菜单权限(从 role_menu 查询)。
    • 将原 menu_ids 与新 menu_ids 进行比对:
      • 新增菜单:将新出现的菜单ID插入 role_menu
      • 删除菜单:将不再包含的菜单ID从 role_menu 中移除。
    • 提交变更,确保租户管理员权限与套餐定义一致。

三、用户登录与权限加载

  1. 用户登录认证 用户输入账号密码,系统校验身份合法性。

  2. 查询用户角色 根据用户ID关联查询其所属的角色(支持多角色)。

  3. 查询角色菜单权限 根据角色ID集合,关联查询 role_menumenu 表,获取所有授权菜单及权限标识(perm)。

    关键点:权限来源于角色与菜单的关联关系,而非直接关联租户套餐。租户管理员角色的菜单权限已在创建租户时通过套餐初始化至 role_menu,因此登录时只需通过角色即可获取完整权限。

  4. 构建权限数据 系统去重后生成权限标识列表(如 ["sys:user:list", "sys:role:add"])。

  5. 返回前端渲染 将权限列表返回给前端,前端根据权限控制菜单显隐及按钮级别的操作权限。

中间件

Kafka

Scala和java开发的消息中间件

线程之间数据交互

每个线程都有自己的栈内存,但是所有线程共用堆内存。只要把需要共享的数据放到堆内存就可以实现数据交互。在java并发工具包中java.util.concurrent,可以实现线程之间数据共享。

进程之间数据交互

一般通过socket

消息队列的两种模式

点对点模式

消费者主动拉数据,消息收到后清除消息。

producer ----生产---->message queue ----消费---->comsumer---->确认----mq---->mq删除已经消费的消息

发布订阅模式

  • 可以有多个topic主题(浏览,点赞,收藏,评论等)
  • 消费者消费数据之后,不删除数据
  • 每个消费者互相独立,都可以消费到数据

producers----消息---->topic----推送---->comsumers

直接安装

Downloads | Apache Kafka

  • 直接按照下载的版本是3.6.1,需要zookeeper。

  • 解压D:/kafka

  • 在这个目录下新建data目录,以后数据就放在这里。

  • 配置zookeeper

修改
D:/kafka/config/zookeeper.properties
改成
dataDir=D:/kafka/data/zookeeper
  • 启动zookeeper
cd D:/kafka/bin/windows
执行命令
zookeeper-server-start.bat ../../config/zookeeper.properties
  • 配置kafka
修改
D:/kafka/config/server.properties
改成
log.dirs=D:/kafka/data/kafka
  • 启动kafka
cd D:/kafka/bin/windows
执行命令
kafka-server-start.bat ../../config/server.properties

docker安装

services:
  broker:
    image: apache/kafka:latest
    network_mode: "host"
    container_name: broker
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://localhost:9092,CONTROLLER://localhost:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
      KAFKA_NUM_PARTITIONS: 3
docker-compose up -d

主题创建、推送、订阅

进入D:\kafka\bin\windows要使用kafka-topics.bat脚本

创建

kafka-topics.bat --bootstrap-server localhost:9092 --topic test --create

查看

kafka-topics.bat --bootstrap-server localhost:9092 --list

详细查看

kafka-topics.bat --bootstrap-server localhost:9092 --topic test --descrip 

修改


删除

kafka-topics.bat --bootstrap-server localhost:9092 --topic test --delete

windows下删除,会出现kafka服务会关闭

订阅

打开两个窗口使用kafka-console-consumer.bat和kafka-console-producer.bat

生产者
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test

在控制台输入数据后回车

消费者
kafka-console-producer.bat --bootstrap-server localhost:9092 --topic test

生产者

public class Demo001Producer {
    public static void main(String[] args) {
        //创建构建参数
        Map<String,Object> map = new HashMap<>();
        map.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"localhost:9092");
        map.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//序列化类型
        map.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());//序列化类型

        //创建生产者对象
        KafkaProducer<String,String> producer = new KafkaProducer<>(map);

        //创建数据
        ProducerRecord<String,String> record = new ProducerRecord<>("test", "wyl", "hello kafka");

        //发送数据到kafka-topic
        producer.send(record);

        //关闭生产者对象
        producer.close();
    }
}

消费者

public class Demo001Consumer {
    public static void main(String[] args) {
        //配置map
        Map<String, Object> configs = new HashMap<>();
        configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        configs.put(ConsumerConfig.GROUP_ID_CONFIG, "test"); // 消费者组ID
        configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // 反序列化类型,字符串类型
        configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()); // 反序列化类型,字符串类型
        //创建消费者
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(configs);
        consumer.subscribe(Collections.singletonList("test"));
        while (true) {
            consumer.poll(1000).forEach(record -> {
                System.out.println(record.key() + ":" + record.value());
            });
        }
    }
}

POM依赖

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.7.18</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
        <version>2.9.11</version>
    </dependency>
</dependencies>
  • application.yaml文件添加
spring:
  kafka:
    bootstrap-servers: localhost:9092
  • 生产者代码
@Component
public class KafkaProducer {
    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    public void sendMessage(String topic, String key, String value) {
        // 创建消息
         ProducerRecord<String, String> record = new ProducerRecord<>(topic, key, value);
        // 发送消息
         kafkaTemplate.send(record);
    }
}
  • 消费者代码
@Component
public class KafkaConsumer {
    @KafkaListener(topics = "test", groupId = "test-group")
    public void receiveMessage(String message) {
        System.out.println("接收到消息:" + message);
    }
}
  • 生产者生产数据
@RestController
public class TestController {
    @Resource
    private KafkaProducer kafkaProducer;
    @GetMapping("/test")
    public String test() {
        kafkaProducer.sendMessage("test", "test", "test");
        return "hello world";
    }
}

生产环境配置

  • 生产者yaml文件
spring:
  kafka:
    bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
    producer:
      # 确认机制:all 表示所有副本都写入才确认,最强可靠性
      acks: all
      # 发送失败重试次数,建议大于 0,同时配合 retry.backoff.ms 控制重试间隔
      retries: 10
      # 批量大小,适当调大可提升吞吐(默认 16384)
      batch-size: 16384
      # 发送延迟,为了凑足 batch-size 或时间到达即发送
      linger-ms: 5
      # 缓冲区内存大小
      buffer-memory: 33554432
      # 生产者客户端 ID,用于监控
      client-id: ${spring.application.name}-producer
      # 键值序列化器
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
      # 可选:启用事务(如果需要 exactly-once 语义)
      transaction-id-prefix: tx-${spring.application.name}-
    properties:
      # 幂等性(防止重试导致的重复),必须开启(>=0.11)
      enable.idempotence: true
      # 最大请求大小(默认 1MB,如果消息体大需要调大)
      max.request.size: 10485760
  • 生产者
@Component
public class KafkaProducer {
    @Resource
    private KafkaTemplate<String, Object> kafkaTemplate;

    public void sendMessage(String topic, String key, Object value) {
        ProducerRecord<String, Object> record = new ProducerRecord<>(topic, key, value);
        ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(record);
        future.addCallback(new ListenableFutureCallback<SendResult<String, Object>>() {
            @Override
            public void onSuccess(SendResult<String, Object> result) {
                // 记录成功日志或 Metrics
                log.info("消息发送成功 topic={} partition={} offset={}", 
                         result.getRecordMetadata().topic(),
                         result.getRecordMetadata().partition(),
                         result.getRecordMetadata().offset());
            }

            @Override
            public void onFailure(Throwable ex) {
                // 记录失败并告警,必要时进行补偿(如持久化到本地后重试)
                log.error("消息发送失败 topic={} key={}", topic, key, ex);
                // 可触发自定义告警或转存到死信表
            }
        });
    }
}
  • 消费者yaml文件
spring:
  kafka:
    consumer:
      # 消费者组 ID,建议按业务区分
      group-id: ${spring.application.name}-group
      # 自动提交关闭,改为手动提交(保证至少一次)
      enable-auto-commit: false
      # 从何处开始消费:latest / earliest / none
      auto-offset-reset: latest
      # 键值反序列化器
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
      # 允许反序列化不信任的包(使用 JSON 时必须)
      properties:
        spring.json.trusted.packages: "*"
      # 每次拉取的最大记录数
      max-poll-records: 50
      # 客户端 ID
      client-id: ${spring.application.name}-consumer
    listener:
      # 手动提交模式:手动调用 Acknowledgment.acknowledge()
      ack-mode: manual
      # 并发消费者数量(对应分区数)
      concurrency: 3
      # 当消费者出现异常时的行为:默认抛出异常停止,可配置重试
    properties:
      # 一次 poll 的最大等待时间(毫秒)
      fetch.max.wait.ms: 500
      # 每次 fetch 的最小数据量(字节)
      fetch.min.bytes: 1024
  • 消费者
@Component
@Slf4j
public class KafkaConsumer {

    @KafkaListener(topics = "test", groupId = "test-group")
    public void receiveMessage(String message, Acknowledgment ack) {
        try {
            // 业务处理
            log.info("接收到消息:{}", message);
            // 处理成功,手动提交偏移量
            ack.acknowledge();
        } catch (Exception e) {
            log.error("处理消息失败,消息内容:{}", message, e);
            // 不提交偏移量,消息会重试
            // 为防止无限重试,可配置重试次数 + 死信队列
            throw e; // 抛出异常触发重试
        }
    }
}

partition

  • 作用:每一个分区在一个broker上存储。把大量数据按照分区切割成一块一块数据存储在多个broker,实现负载均衡。生产者按分区发送数据,消费者按分区消费数据,提高并行度。

分区负载策略

随机策略:生产者每次都发送到随机分区
按key分配:
副本机制:

大体流程

某种主题规则什么动作推送什么内容

1. 创建连接器

首先要创建连接器去连接kafka

1 2

2. 配置规则

这一步是规定什么主题要发送到kafka的什么主题

比如接收kafka/# 这一类以kafka开头的主题,#是通配符,表示所有然后推送到kafka的test主题

首先要创建动作。规定这个kafka/# 主题要做什么动作。比如说推送到kafka的test主题,或者推送到Redis中。

3

3. 测试

4

Mysql

允许远程登录

UPDATE mysql.user SET host='%' WHERE user='root';

FLUSH PRIVILEGES;

索引概念(index)

​ 一种高效获取数据数据结构(有序)

索引 = 数据结构(存储键值)+ 指向数据行的指针(或引用)

Btree

B+free

**hash:**只有Memory引擎支持,InnoDB有自适应的hash功能,通过B+tree构建成hash索引(自动)

每一行数据进行hash。将所需要进行索引的列(字段)的值进行内部hash。内部hash生成特定的位置id(hash表中),然后将这个索引的值和行hash存入hash表。如果出现hash冲突,则会像java一样,会生成链表拼接在已有的值的后面。

1.Hash索引只能用于对等比较(=,in)不能用在范围查询(between,>,<...)

2.无法利用索引完成排序操作

3.查询效率高

新增用户,删除用户,修改密码等功能

操作数据库,表,字段

操作数据库

创建数据库

create database mysql_std

先判断是否存在再创建

create database if not exists mysql_std

指定字符集

create database if not exists default charset utf8mb4

删除数据库

drop database mysql_std

删除数据库判断

drop database if not exists mysql_std;

查看所有数据库

show databases; 注意后面有s

查看当前数据库

select database();注意英文括号

操作表结构

列出数据库中表

show tables;

创建表

create table sys_user(

​ id int comment '编号',

​ name varchar(20) comment '姓名'

​ age int comment '年龄',

​ sex varchar(1) comment '性别'

) comment '用户表';

删除表

查询表中字段

修改字段

删除字段

增加字段

DML负责新增、修改、删除表中数据

所有select相关都是用DQL语句

插入优化

  • 批量插入

    insert into tb_user(id,name) values(1,'tom'),(2,'jack'),(3,'jerry');

  • 手动提交事务

    start transaction;

    insert into ...

    insert into ...

    insert into ...

    commit;

  • 主键顺序插入,减少 B+tree 页分裂。

    主键乱序插入:8 1 9 3 7 5

    主键顺序插入:1 2 3 4 5 6

  • 删除或禁用索引

    插入前临时删除非必要索引(或禁用),插入完成后重建。适合大批量数据加载。

  • LOAD DATA INFILE

大批量数据插入

load指令:用于数据迁移,一下子将磁盘文件插入到数据库。

image-20251225165226520

文件里面的数据主键顺序插入,也要比乱序高。

顺序插入高效原因

查询优化

索引优化

  1. 在 WHERE、JOIN、ORDER BY、GROUP BY 涉及的列上建立合适索引。
  2. 使用覆盖索引(索引包含查询的所有列),避免回表。
  3. 避免在索引列上使用函数或计算(如 WHERE DATE(col)=... → 应改为范围条件)。
  4. 区分度低的列(如性别)不单独建索引,可联合索引放在后面。
  5. 联合索引遵循最左前缀法则
  6. 定期分析并重建索引(OPTIMIZE TABLE)。

SQL 语句优化

  1. 避免 SELECT *,只取需要的列。
  2. EXISTS 代替 IN(子查询数据量大时),或者使用 JOIN 改写。
  3. UNION ALL 代替 UNION(不需要去重时)。
  4. 大分页优化:
    • LIMIT 100000,10 → 改为 WHERE id > 上次最大id LIMIT 10(游标分页)。
    • 或先通过覆盖索引查出主键,再回表取数据。
  5. 避免 OR 导致索引失效,可改用 UNION 或优化为 IN
  6. 使用 LIKE 时避免前置通配符 '%abc',会索引失效。
  7. 合理使用 EXPLAIN 分析执行计划,关注 type(至少达到 rangeref)、rowsExtra 中是否出现 Using filesort / Using temporary

表结构优化

  1. 字段类型尽量小且固定长度(如用 INT 不用 BIGINT,用 CHAR 代替变长等),减少 I/O。
  2. 适当反范式化(冗余常用字段)减少 Join。
  3. 拆分大表(水平分表/垂直分表)。
  4. 对只读或很少变动的历史数据使用归档表。

其他

  1. 查询缓存(MySQL 5.7 及之前,8.0 已废弃),若使用则避免不适合缓存的查询(带 NOW() 等)。
  2. 限制返回行数:LIMIT 子句尽早加。
  3. 避免在循环中执行 SQL,改成批量或 JOIN。

更新&删除优化

  1. 批量更新/删除 每次操作限定行数(如 LIMIT 1000),配合循环,避免长事务锁住大表。
  2. 利用索引 WHERE 条件必须命中索引,否则行锁升级为表锁(InnoDB 走索引才锁行)。
  3. 避免锁争用 更新高频热行时,考虑排队机制或减少并发直接写。
  4. 先查询后更新 如果更新逻辑复杂,可先查出主键集合,再按主键批量更新。
  5. 分区裁剪 对分区表进行 UPDATE/DELETE 时,WHERE 条件带上分区键,只锁定相关分区。

查看和设置事务的提交方式

select @@autocommit;

set @@autocommit = 0;

提交事务

commit;

事务回滚

rollback;

开启事务

start transaction 或者begin;

事务四大特性

原子性

一致性

隔离性

持久性

并发事务问题

脏读

不可重复读

幻读

隔离级别

为了解决上面的并发问题提出的方案

分四种

read uncommited

read commited

repeatable read(默认)

serializable

查看隔离级别

select @@transaction_isolation;

设置隔离级别

set session transaction isolation level read uncommited

字符串函数

concat(s1,s2,...) 字符串拼接

upper(str)

lower(str)

lpad(str,n,pad)

rpad(str,n,pad)

trim(str)

substring(str,start,len);

数值函数

ceil 向上取整

floor向下取整

mod 取模,余数

rand 随机生成0-1

round 四舍五入,小数四舍五入,规定多少位小数

日期函数

curdata() 当前日期

curtime()当前时间

now() 当前日期时间

year(data) 获取给出的时间的年分

month(data)获取给出的时间的月份

day(data)获取给出的时间的当日

data_add(data,INTERVAL count d) 给出时间叠加之后的时间

datadiff(data1,data2) 时间间隔天数

流程函数

IF(value,tt,ff) 如果value为ture,返回tt否则返回ff

IFNULL(value1,value2) 如果value1不为空返回value1,否则返回value2

case when value1 then value2

多表查询的几种分类可以根据两个相交的集合分析

集合A

ab相交处

集合B

内连接

左外连接

右外连接

左排除连接

右排除连接

全外连接

外排除连接

三种存储引擎

INNODB(默认),MYISAM,MEMERY

INNODB支持所有操作。

MYISAM 用于插入和查询,极少修改删除。现在已经被其他数据库比如mongoDB之类的代替了

MERMERY 被Redis之类的内存数据库代替了。

类型:

非空约束 NOT NULL

唯一约束UNIQUE

主键约束 primary key

默认约束 default

检查约束(8.0.16之后) check

外键约束foreign key

Nginx

server{
	listen       80;
	listen 443 ssl;
    server_name  www.xxx.com;

	location /YH-LX001B {
    	try_files $uri $uri/ /YH-LX001B/index.html;
	}
}

$uri$uri/ 的作用

“保障静态资源(CSS, JS, 图片等)能够被正常访问

当一个浏览器访问你的单页面应用时,会发生以下步骤:

  1. 浏览器请求 /YH-LX001B/ -> Nginx 返回 index.html
  2. index.html 文件被浏览器解析,它里面包含了链接去请求各种静态资源,比如:
    • <script src=“js/app.js”>
    • <link href=“css/style.css”>
  3. 浏览器会向服务器发起新的请求:
    • GET /YH-LX001B/js/app.js
    • GET /YH-LX001B/css/style.css

正确的流程是怎样的?

有了完整的 try_files $uri $uri/ /YH-LX001B/index.html;,流程是这样的:

  1. 请求 /YH-LX001B/js/app.js:
    • $uri -> Nginx 在网站根目录下寻找 /YH-LX001B/js/app.js 这个文件
    • 找到了! 于是 Nginx 停止尝试,直接将这个正确的 JavaScript 文件发送给浏览器。✅
  2. 请求 /YH-LX001B (一个目录):
    • $uri -> 寻找名为 YH-LX001B文件,没找到。
    • $uri/ -> 寻找名为 YH-LX001B/目录,找到了。
    • Nginx 会默认寻找这个目录下的索引文件(如 index.html),并返回它。✅
  3. 请求 /YH-LX001B/settings (一个前端路由):
    • $uri -> 寻找名为 settings文件,没找到。
    • $uri/ -> 寻找名为 settings/目录,也没找到。
    • 最终 fallback 到 /YH-LX001B/index.html,Nginx 返回这个文件。
    • 浏览器拿到 index.html 后,由前端的路由器(Vue Router/React Router)解析 URL 中的 /settings 并显示对应的页面。✅

域名为:api.openso.top

​ 对外开放的api接口,所有项目的openapi都走这里。

后端项目必须要配置项目名:

server:
  port: 48080
  servlet:
      context-path: /proj1

​ 例如:

​ 1.proj1:api.openso.top/proj1/getuser/1

​ 2.proj2:api.openso.top/proj2/getuser/1

配置方式1

server{
    listen 80;
    listen 443;
    server_name api.openso.top;
        
    location /proj1/ {
        proxy_pass http://10.147.17.85:48080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    location /proj2/ {
        proxy_pass http://10.147.17.86:48080; #映射到不同的后端地址
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

配置方式2

server{
    listen 80;
    listen 443;
    server_name api.openso.top;
        
    location /proj1 {
        proxy_pass http://10.147.17.85:48080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    location /proj2 {
        proxy_pass http://10.147.17.86:48080; #映射到不同的后端地址
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

核心规则

Location 匹配规则

  • location /proj1/ :精确前缀匹配,要求路径以 /proj1/ 开头
  • location /proj1 :宽泛前缀匹配,/proj1/proj1//proj1abc 都会匹配

Proxy_pass 路径处理规则

proxy_pass 格式处理方式说明
http://... (无斜杠)保留完整路径location 匹配部分 + 剩余路径都转发
http://.../ (有斜杠)替换 location 匹配部分只转发 location 匹配后的剩余路径

一、后端有配置项目名

  • 后端
server:
  port: 8080
  servlet:
      context-path: /proj1
  • Nginx
location /proj1 {
	proxy_pass   http://localhost:8080;
}
#或者
location /proj1/ {
	proxy_pass   http://localhost:8080;
}

二、后端没有配置项目名

  • 后端
server:
  port: 8080
  • Nginx
location /proj1/ {
	proxy_pass   http://localhost:8080/;
}

访问http://localhost/proj1/test。

  • proxy_pass http://.../ 有斜杠,会替换掉 location 匹配的部分

  • /proj1/test中的/proj1/会被替换成/,剩余test部分

apt install nginx

在server下添加

server {
	listen 80;
	listen 443 ssl;#HTTPS的默认访问端
	server_name www.openso.top;
	
	#禁止ip访问
	if ($host != $server_name) {
        return 403;
    }
}

安卓开发

安卓原生打包uniapp插件

博客链接以及教程

  • assets
  • [uniapp原生Android插件开发入门教程 (最新版).md](uniapp原生Android插件开发入门教程 (最新版))

uniapp原生Android插件开发入门教程 (最新版)

原创 已于 2023-11-30 10:38:30 修改 · 6.6k 阅读 · 57 · 64 · CC 4.0 BY-SA版权 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 文章链接:https://blog.csdn.net/qq_56892518/article/details/134686079

前言:

前段时间写了一个点餐系统的项目,APP运行在商米收银机上,需要调用其内置打印机去打印小发票,但是uniapp不能直接调用Android设备,所以只能通过Android插件实现,但当时对于Android原生插件开发的了解还是一片空白,后面在插件商城找了一个插件并完美实现了打印功能,自那之后,就一直想学习一下Android插件开发。刚好最近公司事情不多,便认真学习了一下,但是学习过程中发现官方文档对初学者不是很友好。为了让后面的头同学少踩坑,特点花了时间整理出此篇贴文。希望此文能够帮助到您!

1.开发环境

以下是我的开发环境,各位可根据自己的情况来,没必要跟我的一模一样

HBuilder X版本号3.96AndroidStudio版本号 4.2.2

2.下载Android 离线SDK

下载完成后打开的zip文件是这样的

将该文件解压到电脑本地磁盘上

注意 : 保存该文件夹的路径不得包含中文,空格等非法字符,要不然Android Studio 打开会报错

这是我解压的文件路径 D:\test1\Android-SDK@3.96.81954_20231106

3. 使用Android Studio 打开 UniPlugin-Hello-AS 文件

导入完成后,这个地方改成Project,方便预览文件

这个时候可以尝试运行一下这个模板项目,看有没有报错,第一次运行时间有点就,因为需要加载资源,耐心等待右下角进度条加载完成即可。

运行后发现无论是Android Studio还是模拟器都提示appkey错误,那么下面我们就去申请一下appkey等配置信息。

3.申请配置信息

使用HBuilder打开uniapp示例工程源码下面的unipluginDem项目文件夹

打开manifest.json选择基础配置重新获取AppID

点击确定后AppID这一栏便后自动生成一个Id编号

这个时候打开uniapp开发者后台就可以看到这个项目的信息 开发者后台

如果还没有证书的话点击创建证书,如果已经有证书了那就点击下载证书

等待证书下载完成后将证书文件 复制到app文件下面,这里的目录需要注意一下,不要放错了

点击证书详情查看证书SHA1、SHA256秘钥和别名,点击查看证书密码,查看此证书密码,这些信息可先复制到其他地方,后面要用到。

将app 信息填写到build.gradle中

创建离线打包Key管理,离线打包key管理这个功能迁移到了各平台信息,单击 “点击前往”链接前往各平台信息选项卡

点击右上角新增按钮创建一个新的key

点击创建按钮后回弹窗下图最右侧的弹窗,把相应的信息填写进去

填写完成表单信息后点击提交按钮

这里会提示这个包名已经在其他开发者使用了,自己可以将包名改一下,改了就不会出现这个提示框 。

添加完成后列表中会出现一行新的数据,点击创建按钮

创建完成后点击查看按钮,将APP key 复制到AndroidManifest.xml下面的android:value中,

修改完成后记得同步一下信息

修改app文件下的 dcloud_control.xml 里面的 appid

生成本地打包APP资源

打包完成后点击导出路径

将打包文件复制到apps文件下面

补充 : 这里如果不好粘贴的话,将apps文件夹在外部打开,用电脑自带文件夹去操作

这个时候再运行一下

这个时候便运行成功了

下面我们以前分析一下案例代码,在uniapp的ext-module页面中中分别调用了testAsyncFunc、testSyncFunc、gotoNativePage三个方法,而在Android的TestModule java文件中同样有这三个方法说明uniapp调用的就是这三个方法,我看可以改一下里面的返回值内容,看一下代码运行是否正常。

修改返回信息后,页面内容变化了,说明上面分析的没错,并且代码运行正常,可以进行下一步

下面我们自己创建一个module试试

这里我选择的是Android 4.1 最低版本,大家跟可以跟我一样,也可以根据自己的情况来。

创建完成后在这里可以看到创建的文件夹

创建一个java文件,这里是创建在com.example.test 这个包下面

复制 uniplugin_module文件夹下面 build.gradle 配置文件里面的内容,这里只要复制我框起来的就可以了。

将复制来的内容粘贴到新我们刚刚创建的test文件夹下面的build.gradle中。

下面开始编辑新建的Java文件,为了少出错,咱们还是先选择复制粘贴他原本的代码,后期自己可以根据之身情况修改代码,为了方便复制代码和比对代码,将代码窗口切换传左右两侧显示。

将TestModule里面的代码复杂到我们刚刚创建的java文件中,这里Android Studio 通常会自动帮你导入相应的包,不需要单独复制。

以下是我 的代码

package com.example.test;
 
import android.util.Log;
 
import com.alibaba.fastjson.JSONObject;
 
import io.dcloud.feature.uniapp.annotation.UniJSMethod;
import io.dcloud.feature.uniapp.bridge.UniJSCallback;
import io.dcloud.feature.uniapp.common.UniModule;
 
public class testmode  extends UniModule {
    //run ui thread
    @UniJSMethod(uiThread = true)
    public void getTest(JSONObject options, UniJSCallback callback) {
        if(callback != null) {
            JSONObject data = new JSONObject();
            data.put("code", "success");
            data.put("code", "我是新来的");
            callback.invoke(data);
            //callback.invokeAndKeepAlive(data);
        }
    }
}

注册插件

这里需要在app\src\main\assets目录下打开dcloud_uniplugins.json文件,将我们刚刚创建的包名以及类名复制进去格式为"包名"+类名 ,type这里填module

引入插件

在插件项目app目录下的 build.gradle 文件中,添加我们刚刚注册的插件这里的名称就是我们刚刚创建的文件名称

在HBuider中编辑uniapp代码,这里requireNativePlugin引入的就是我们刚刚注册插件时填写的name,方法名就是我们在Java文件里面定义的方法名。

以下是我这边ext-module.nvue文件里的代码

<template>
	<div>
		<button type="primary" @click="MyTest">这是我刚写的</button>
		<button type="primary" @click="testAsyncFunc">testAsyncFunc</button>
		<button type="primary" @click="testSyncFunc">testSyncFunc</button>
		<button type="primary" @click="gotoNativePage">跳转原生Activity</button>
	</div>
</template>
 
<script>
	// 获取 module 
	var testModule = uni.requireNativePlugin("TestModule")
	var testmode = uni.requireNativePlugin("testmodule")
	const modal = uni.requireNativePlugin('modal');
	export default {
		onLoad() {
			plus.globalEvent.addEventListener('TestEvent', function(e){
				modal.toast({
					message: "TestEvent收到:"+e.msg,
					duration: 1.5
				});
			});
		},
		methods: {
			MyTest() {
				testmode.getTest({
						'name': 'unimp',
						'age': 1
					},
					(ret) => {
						modal.toast({
							message: ret,
							duration: 1.5
						});
					})
			},
			testAsyncFunc() {
				// 调用异步方法
				testModule.testAsyncFunc({
						'name': 'unimp',
						'age': 1
					},
					(ret) => {
						modal.toast({
							message: ret,
							duration: 1.5
						});
					})
			},
			testSyncFunc() {
				// 调用同步方法
				var ret = testModule.testSyncFunc({
					'name': 'unimp',
					'age': 1
				})
				modal.toast({
					message: ret,
					duration: 1.5
				});
			},
			gotoNativePage() {
				testModule.gotoNativePage();
			}
		}
	}
</script>

代码写完后重新打一个 打包APP资源,打包教程文章前半部分有,可以直接按照前面的方法打包。

打包完成后替换掉原来的打包文件,这里替换的是Android Studio里面的文件。

替换完成后重新运行一下,这里可以看到我们刚刚新加的功能已经能够正常使用

打包插件打包之前需要检测一下SDK版本,如果插件于uniapp里面的SDK里面设置的SDK版本不一致打包会报错。

点击Android Studio窗口右侧的Gradle在我们刚刚创建的文件夹下面找到assembleRelease,点击即可打包编译。

编译完成后左侧文件目录中找到目录为test/build/outputs/aar/test-release.aar 的打包产物

将打包产物复制到uniapp中,这里需要事先创建好一下文件目录 nativeplugins 、test、Android、package.json 其中Android、package.json为同等级。

目录创建完成后需要编辑一下package.json文件,这里的class就是我们注册插件时填写的class,name和type也是。

这是 package.json 里面的代码

{
    "name": "testmodule",
    "id": "testmodule",
    "version": "0.0.1",
    "description": "我是新来的",
    "_dp_type":"nativeplugin",
    "_dp_nativeplugin":{
        "android": {
            "plugins": [
                {
                    "type": "module",
                    "name": "testmodule",
                    "class": "com.example.test.testmode"
                }
            ],
			"integrateType": "aar",
            "parameters": {
                
            },
            "dependencies": []
        }
    }
}

下面就可以打包一个出来试试看,这里的证书就是我们前面申请的证书,

打包完成后安装到模拟器运行正常。

结束语

这也是我第一次写这么长的贴文,写的不是很好,希望各位见谅。

系统学习笔记

大小单位:px,dp,sp

px:手机屏幕的最小显示单位,它与设备的显示屏有关。一般来说,同样尺寸的屏幕(比如6英寸手机),如果看起来越清晰,则表示像素密度越高,以px计量的分辨率也越大。**
pd:有时也写作dip,指的是与设备无关的显示单位,它只与屏幕的尺寸有关。一般来说,同样
尺寸的屏幕以dp计量的分辨率是相同的,比如同样是6英寸手机,无论它由哪个厂家生产,其分辨
率换算成dp单位都是一个大小。
sp的原理跟dp差不多,但它专门用来设置字体大小,也是Android推荐的字号单位。手机在系
统设置里可以调整字体的大小(小、标准、大、超大)。设置普通字体时,同数值dp和sp的文字看
起来一样大:如果设置为大字体,用dp设置的文字没有变化,用sp设置的文字就变大了。

视图宽高:

**(1)match_parent:**表示与上级视图保持一致。上级视图的尺寸有多大,当前视图的尺寸就 有多大。 **(2)wrap_content:**表示与内容自适应。对于文本视图来说,内部文字需要多大的显示空间, 当前视图就要占据多大的尺寸。但最宽不能超过上级视图的宽度,一旦超过就要换行:最高不能超 过上级视图的高度,一旦超过就会被隐藏。 (3)以dp为单位的具体尺寸,比如300dp,表示宽度或者高度就是这么大。

项目规划

物联网项目文档

  1. 前言

    本文档为物联网设备管理平台(软硬件一体化)项目的全套工程文档,涵盖项目从立项调研到测试验收的全生命周期,包含软件工程与硬件设备开发工程相关内容。结合项目定位(软硬件一体化提供商,提供多场景物联网服务),文档采用“核心内容整合、软硬件模块分离”的结构——整体项目框架、立项、可行性分析等核心内容统一整合,软件、硬件的需求分析、设计、测试等专项内容独立分节,既保证项目整体性,又兼顾软硬件开发的专业性和独立性,解决“整合与分离”的核心需求。软件技术选型、硬件设备选型可根据自身规划,直接填充至对应章节的指定位置。

    项目核心定位:作为软硬件一体化物联网服务提供商,搭建支持MQTT、TCP、HTTP等多协议设备接入的物联网设备管理平台,面向智能家居、温湿度数据上报、智慧农业、智能工厂等多场景,为客户提供全流程软件+硬件一体化服务,实现设备接入、数据采集、远程控制、数据分析等核心功能。

    第一部分:立项文档

    1.1 项目立项报告

    1.1.1 项目名称

    物联网设备管理平台(软硬件一体化)建设项目

    1.1.2 项目发起单位/负责人

    发起单位:__________

    项目负责人:__________

    项目团队:__________(可补充开发、测试、硬件集成等核心成员)

    1.1.3 项目背景与意义

    1.1.3.1 研究背景

    随着物联网技术的快速普及,各行业设备联网规模呈现指数级增长,从工业生产线上的传感器、智能工厂的PLC控制器,到消费领域的智能家居终端,设备类型日益复杂,连接需求愈发多样。当前市场中,多数物联网服务存在“软件与硬件脱节”“设备接入协议单一”“场景适配性差”等痛点,传统设备管理模式依赖人工巡检,故障响应滞后,数据分散存储难以形成有效价值,且不同厂商设备协议差异大,系统集成难度高,常出现“哑设备”现象。同时,智能家居、智慧农业、智能工厂等领域对物联网服务的需求持续升级,客户亟需“一站式”软硬件一体化解决方案,而非单独采购软件平台与硬件设备后自行整合,这为软硬件一体化物联网服务提供商提供了广阔的市场空间。

    在此背景下,我们计划搭建物联网设备管理平台,支持MQTT、TCP、HTTP等多协议设备接入,整合自主研发/选型的硬件设备,为各行业客户提供从设备部署、数据采集到远程控制、数据分析的全流程服务,解决行业痛点,满足市场需求。

    1.1.3.2 项目意义

    1. 商业意义:立足软硬件一体化定位,填补市场“一站式”物联网服务空白,拓展智能家居、智慧农业、智能工厂等多场景客户群体,打造差异化竞争优势,实现商业价值变现;

    2. 技术意义:整合多协议接入技术、软硬件协同技术,形成可复用、可扩展的物联网设备管理体系,提升自身技术积累,为后续场景拓展奠定基础;

    3. 行业意义:助力各行业客户实现设备智能化管理,降低运维成本、提升数据利用价值,推动传统行业数字化转型,契合国家数字经济与物联网产业发展战略。

    1.1.4 项目目标

    1.1.4.1 总体目标

    搭建一套稳定、高效、可扩展的物联网设备管理平台,实现软硬件深度协同,支持多协议设备接入、多场景服务落地,成为专业的物联网软硬件一体化服务提供商,满足客户在设备管理、数据采集、远程控制等方面的核心需求,提升客户满意度与市场占有率。

    1.1.4.2 阶段性目标

    1. 立项调研阶段(1-2周):完成市场调研、技术调研,确定软硬件选型方案,完善可行性分析;

    2. 需求分析与设计阶段(3-4周):完成软件、硬件的需求分析,完成概要设计、详细设计,输出设计文档;

    3. 开发实现阶段(8-10周):完成软件平台开发、硬件设备选型与集成,实现软硬件协同联调;

    4. 测试验收阶段(2-3周):完成软件、硬件及系统集成测试,修复问题,通过验收,输出测试报告;

    5. 上线部署阶段(1-2周):完成平台上线、硬件部署,提供客户培训与技术支持,进入运维阶段。

    1.1.5 项目范围

    1. 软件范围:物联网设备管理平台开发,包括设备接入模块、数据采集与存储模块、远程控制模块、数据分析模块、用户管理模块、权限管理模块等,支持MQTT、TCP、HTTP等多协议接入;

    2. 硬件范围:硬件设备选型、集成与调试,包括温湿度传感器、控制终端、通信模块等,适配多场景部署,与软件平台实现无缝对接;

    3. 服务范围:为客户提供软硬件一体化部署、调试、培训、售后技术支持,覆盖智能家居、智慧农业、智能工厂等核心场景;

    4. 排除范围:不涉及硬件设备的核心芯片自主研发(仅做选型与集成),不涉及第三方平台的二次开发(除非客户特殊需求)。

    1.1.6 项目资源需求

    1. 人力资源:软件开发工程师、硬件工程师、测试工程师、产品经理、项目管理人员、运维工程师;

    2. 硬件资源:测试用硬件设备(传感器、控制终端、通信模块等)、服务器、网络设备;

    3. 软件资源:开发工具、测试工具、数据库、操作系统、协议调试工具等;

    4. 资金资源:研发资金、硬件采购资金、测试资金、培训资金等。

    1.1.7 立项审批意见

    审批人:__________

    审批意见:__________

    审批日期:__________

第二部分:可行性分析报告

2.1 概述

本报告针对物联网设备管理平台(软硬件一体化)项目,从市场、技术、经济、操作、风险五个维度进行可行性分析,判断项目是否具备实施条件,为项目决策提供科学依据。本次分析基于当前市场环境、技术水平及自身资源,结合项目目标与范围,确保分析结果真实、可靠、具有指导性。

2.2 市场可行性分析

  1. 市场需求:随着物联网技术在各行业的渗透,智能家居、智慧农业、智能工厂等领域对设备管理平台的需求持续增长,客户对“软硬件一体化”服务的需求日益迫切,避免了单独采购软硬件的整合成本与技术壁垒,市场空间广阔;

  2. 市场竞争力:当前市场中,多数物联网服务提供商要么只做软件平台,要么只做硬件设备,软硬件一体化提供商较少,项目凭借“多协议接入”“多场景适配”“一站式服务”的优势,可形成差异化竞争,契合市场需求;

  3. 市场前景:物联网产业处于快速发展阶段,政策支持力度大,各行业数字化转型加速,未来对物联网设备管理平台及软硬件一体化服务的需求将持续提升,项目具有良好的市场前景和可持续性。

2.3 技术可行性分析

  1. 技术成熟度:MQTT、TCP、HTTP等设备接入协议已成为物联网领域的主流协议,技术成熟、应用广泛;软件平台开发(如设备管理、数据存储、远程控制)、硬件设备选型与集成技术均已成熟,不存在难以突破的技术壁垒;

  2. 技术储备:项目团队已明确软件技术选型与硬件设备选型方案,具备相关的开发、集成、测试技术能力,可支撑项目顺利实施;

  3. 软硬件协同:采用成熟的软硬件协同技术,通过标准化接口实现软件平台与硬件设备的无缝对接,可确保数据传输稳定、控制指令高效执行,解决软硬件脱节问题;

  4. 可扩展性:软件平台采用模块化设计,硬件设备支持灵活替换与扩展,可根据后续市场需求,快速适配新的场景、新的设备类型,技术扩展性强。

2.4 经济可行性分析

  1. 成本估算:项目成本主要包括硬件采购成本、研发成本、人力成本、测试成本、培训成本、运维成本等,结合项目规模与阶段性目标,成本可控,可通过合理规划优化成本;

  2. 收益预测:项目收益主要来自软硬件一体化服务收费、设备销售、售后运维服务等,随着客户群体的拓展,收益将逐步提升,预计在项目上线后1-2年内实现盈利;

  3. 投资回报:综合成本与收益分析,项目投资回报率合理,风险可控,具备良好的经济可行性,符合企业长期发展战略。

2.5 操作可行性分析

  1. 团队能力:项目团队具备软件开发、硬件集成、测试、项目管理等相关能力,可熟练完成项目各阶段工作,确保项目顺利推进;

  2. 操作难度:软件平台操作界面简洁、易用,客户可快速掌握设备接入、数据查看、远程控制等操作;硬件设备部署简单、调试便捷,可适应不同场景的部署需求;

  3. 运维保障:制定完善的运维方案,配备专业的运维工程师,可及时处理软件故障、硬件故障,保障平台与设备的稳定运行,降低操作与运维难度。

2.6 风险可行性分析

2.6.1 潜在风险

  1. 技术风险:软硬件协同过程中可能出现兼容性问题,设备接入过程中可能出现协议适配问题,影响平台稳定性;

  2. 市场风险:市场需求变化过快,或竞争对手推出同类产品,影响项目市场占有率;

  3. 成本风险:硬件采购价格波动、研发成本超支,导致项目成本增加;

  4. 进度风险:项目各阶段工作推进滞后,影响项目上线时间。

2.6.2 风险应对措施

  1. 技术风险:提前进行技术调研与测试,完善软硬件选型方案,加强软硬件协同联调,建立技术问题应急处理机制,及时解决兼容性、协议适配等问题;

  2. 市场风险:持续关注市场需求变化,加强市场调研,及时优化产品与服务,打造差异化优势,加强客户维护,提升客户粘性;

  3. 成本风险:合理规划采购计划,与供应商签订长期合作协议,控制硬件采购成本;优化研发流程,避免研发成本超支,建立成本监控机制;

  4. 进度风险:制定详细的项目进度计划,明确各阶段工作任务与时间节点,加强项目进度监控,及时调整工作安排,确保项目按时推进。

2.7 可行性结论

综合市场、技术、经济、操作、风险五个维度的分析,本项目市场需求明确、技术成熟、成本可控、风险可应对,具备完全的可行性,建议立项实施。

第三部分:需求分析文档

本章节采用“整体需求+软件需求+硬件需求”的结构,整体需求明确项目核心诉求,软件、硬件需求分别独立阐述,兼顾整合性与独立性,确保需求清晰、可落地、可验证。

3.1 整体需求

  1. 多协议接入需求:支持MQTT、TCP、HTTP等多种设备接入协议,实现不同类型、不同厂商硬件设备的快速接入,解决设备接入标准化问题;

  2. 软硬件协同需求:软件平台与硬件设备无缝对接,实现数据实时采集、远程控制指令高效下发,确保软硬件协同稳定运行;

  3. 多场景适配需求:适配智能家居、温湿度数据上报、智慧农业、智能工厂等多场景,提供针对性的软硬件解决方案;

  4. 核心功能需求:实现设备管理、数据采集与存储、远程控制、数据分析、用户管理、权限管理等核心功能;

  5. 性能需求:平台运行稳定,数据传输延迟低,设备接入容量可扩展,硬件设备运行可靠、功耗合理;

  6. 易用性需求:软件平台操作简洁,硬件设备部署、调试便捷,客户可快速上手,降低使用成本;

  7. 安全性需求:保障设备接入安全、数据传输安全、数据存储安全,防止非法接入、数据泄露、指令篡改。

3.2 软件需求(独立模块)

3.2.1 功能需求

3.2.1.1 设备接入模块

  1. 支持MQTT、TCP、HTTP三种核心协议接入,可扩展其他物联网协议;

  2. 支持设备批量接入与单个接入,提供设备接入指引与配置工具;

  3. 实现设备接入验证(设备ID、密钥验证),防止非法设备接入;

  4. 支持设备在线状态监测,实时显示设备接入状态(在线、离线、异常),异常状态及时提醒;

  5. 支持设备接入日志记录,可查询设备接入时间、接入协议、接入状态等信息。

3.2.1.2 数据采集与存储模块

  1. 实时采集硬件设备上传的数据(如温湿度、设备运行参数、状态数据等),采集频率可配置;

  2. 支持数据格式解析与转换,确保不同设备的数据统一格式存储;

  3. 采用可靠的数据库存储数据,支持历史数据查询、导出,数据存储期限可配置;

  4. 支持数据异常检测,当数据超出预设阈值时,触发异常提醒;

  5. 保障数据传输过程中的完整性,防止数据丢失、篡改。

3.2.1.3 远程控制模块

  1. 支持通过软件平台向硬件设备下发控制指令(如开关控制、参数调节等);

  2. 实时反馈指令执行结果,显示设备执行状态;

  3. 支持控制指令日志记录,可查询指令下发时间、指令内容、执行结果;

  4. 支持批量控制多个设备,提高操作效率;

  5. 当指令执行失败时,提供失败原因提示,并支持重试功能。

3.2.1.4 数据分析模块

  1. 支持对采集的数据进行统计分析(如平均值、最大值、最小值、趋势分析等);

  2. 提供数据可视化展示(图表、报表等),便于客户直观查看数据变化;

  3. 支持自定义分析规则,根据客户需求生成针对性的分析报告;

  4. 针对不同场景(如智慧农业的土壤湿度分析、智能工厂的设备运行效率分析)提供专属分析功能。

3.2.1.5 用户管理与权限管理模块

  1. 支持用户注册、登录、密码重置、账号注销等功能;

  2. 支持用户信息管理(修改个人信息、绑定联系方式等);

  3. 支持权限分级管理,不同角色(管理员、普通用户、运维人员)拥有不同的操作权限;

  4. 管理员可管理所有用户、设备、数据,普通用户仅可查看自身绑定的设备与数据,运维人员可进行设备调试与故障处理。

3.2.1.6 系统管理模块

  1. 支持系统参数配置(如数据采集频率、异常阈值、存储期限等);

  2. 支持系统日志记录,可查询操作日志、设备日志、异常日志等;

  3. 支持系统升级与维护,确保系统稳定运行;

  4. 支持数据备份与恢复,防止数据丢失。

3.2.2 非功能需求

  1. 性能需求:平台响应时间≤1s,数据传输延迟≤500ms;支持至少1000台设备同时在线接入,可扩展至10000台以上;系统可用性≥99.9%;

  2. 安全性需求:采用加密技术(如SSL/TLS)保障数据传输安全;设备接入采用密钥验证,防止非法接入;数据存储加密,防止数据泄露;定期进行安全检测,及时修复安全漏洞;

  3. 可扩展性需求:采用模块化设计,支持功能模块扩展(如新增协议接入、新增分析功能);支持设备类型扩展,可适配新的硬件设备;

  4. 易用性需求:操作界面简洁、直观,导航清晰,客户可快速掌握操作方法;提供操作指引与帮助文档;

  5. 兼容性需求:支持Windows、Linux等主流操作系统,支持Chrome、Edge等主流浏览器;

  6. 可维护性需求:系统日志清晰,便于问题排查;模块之间低耦合,便于维护与升级。

3.2.3 接口需求

  1. 设备接入接口:支持MQTT、TCP、HTTP协议接口,用于设备与平台的数据交互;

  2. 硬件对接接口:提供标准化接口,用于软件平台与硬件设备的协同对接,支持数据采集与指令下发;

  3. 内部接口:各模块之间的接口,确保模块之间的数据交互顺畅;

  4. 外部接口(可选):提供API接口,支持与第三方平台对接,实现数据共享。

3.3 硬件需求(独立模块)

3.3.1 硬件设备类型及需求

3.3.1.1 感知设备

  1. 温湿度传感器:用于采集环境温湿度数据,精度≥±0.5℃(温度)、±5%RH(湿度);支持低功耗运行;支持MQTT/TCP/HTTP协议;适配室内外多种场景;

  2. 其他感知设备(可选):根据场景需求,配置土壤湿度传感器、光照传感器、压力传感器等,要求精度达标、运行稳定、支持对应协议。

3.3.1.2 控制终端

  1. 用于接收软件平台下发的控制指令,执行相应操作(如开关控制、参数调节);

  2. 支持与感知设备、通信模块对接,实现数据采集与指令执行;

  3. 运行稳定,响应迅速,指令执行延迟≤300ms;

  4. 支持低功耗模式,适配不同供电场景(市电、电池)。

3.3.1.3 通信模块

  1. 支持MQTT、TCP、HTTP三种核心协议,可实现设备与软件平台的数据传输;

  2. 通信稳定,信号强度强,支持远距离传输(根据场景需求,可选择Wi-Fi、4G/5G、LoRa等通信方式);

  3. 低功耗、小体积,便于部署;

  4. 支持自动重连功能,当网络中断后,可自动重新连接平台。

3.3.1.4 辅助设备

  1. 电源设备:为感知设备、控制终端、通信模块提供稳定供电,支持市电、太阳能、电池等多种供电方式;

  2. 部署支架:用于设备固定,适配室内外部署场景,防水、防尘、抗干扰。

3.3.2 硬件性能需求

  1. 运行可靠性:硬件设备平均无故障运行时间(MTBF)≥10000小时;

  2. 环境适应性:适应温度范围-20℃~60℃,湿度范围10%~90%RH;防水、防尘、抗电磁干扰;

  3. 功耗需求:感知设备、通信模块采用低功耗设计,电池供电模式下,续航时间≥6个月;

  4. 数据采集精度:各类传感器的数据采集精度符合行业标准,确保数据准确性;

  5. 兼容性:硬件设备之间可无缝对接,与软件平台通过标准化接口对接,支持协议适配。

3.3.3 硬件接口需求

  1. 通信接口:支持UART、SPI、I2C等常用接口,用于与通信模块、感知设备对接;

  2. 供电接口:标准化供电接口,支持不同供电方式接入;

  3. 扩展接口:预留扩展接口,便于后续新增设备或功能扩展。

3.3.4 硬件部署需求

  1. 部署便捷:设备体积小、重量轻,便于安装与部署,无需复杂施工;

  2. 维护便捷:设备支持远程调试、固件升级,减少现场维护工作量;

  3. 场景适配:根据智能家居、智慧农业、智能工厂等不同场景,提供对应的部署方案,确保设备运行稳定。

3.4 需求确认

需求提出人:__________

需求确认人:__________

确认日期:__________

核心逻辑:三层结构

文档可以理解为三个逻辑层次:

  1. 共识层(第1-3章):对齐背景、目标、用户,回答 “为什么做”和“为谁做”
  2. 定义层(第4-7章):详细描述解决方案,回答 “做什么”和“做成什么样”
  3. 约束与收尾层(第8-9章):明确边界和未尽事宜,回答 “在什么限制下做”和“如何跟踪”

各章节详细作用分析

1. 文档概述:建立沟通基础与版本控制

  • 1.1 修订历史:【核心作用】记录每一次修改的作者、日期、原因和变更内容。这是文档的“时光机”,确保所有干系人看到的都是最新版本,且任何变更都有据可查,是责任追溯和变更管理的关键。
  • 1.2 项目背景与目标:【核心作用】阐明项目的商业驱动因素和价值。解释“为什么要启动这个项目”,以及项目成功的核心衡量指标。这是统一团队思想的基石,防止开发偏离业务初衷。
  • 1.3 文档目的与范围:【核心作用】定义本文档的读者对象和用途,并清晰划定项目的边界。明确指出“包含什么”和“不包含什么”(在范围外),这是管理需求蔓延、控制项目范围的第一道防线
  • 1.4 名词术语解释:【关键作用】建立团队内部统一的语言体系。避免因业务术语、技术简称或行业黑话理解不一致导致的沟通成本和质量问题。
  • 1.5 参考文献:【辅助作用】列出撰写本文档所依据的会议纪要、市场报告、战略文档等,增加文档的可信度和可追溯性。

2. 干系人与用户分析:明确服务对象与场景

  • 2.1 干系人列表与关注点:【核心作用】识别所有利益相关方及其核心诉求与影响力。确保在决策和沟通中不遗漏任何关键角色,是项目管理的基础
  • 2.2 用户角色画像:【核心作用】将抽象的用户群体具体化为有姓名、背景、目标的虚拟人物。帮助团队始终站在用户角度思考,确保产品设计是为人服务的。
  • 2.3 用户场景概述:【关键作用】描述用户画像在什么情境下会遇到什么问题、触发使用产品的动机。将用户需求和产品功能置于真实的故事场景中,让需求更鲜活。

3. 总体概述:描绘产品全景图

  • 3.1 产品愿景:【激励作用】用一句简洁有力的话描述产品的长期目标和最终状态,激发团队共鸣和热情。
  • 3.2 核心业务流程:【核心作用】通过跨职能流程图,展示不同角色如何协作完成一个端到端的核心业务。这是理解业务全貌的最佳工具,帮助技术人员理解业务上下文。
  • 3.3 系统上下文图:【关键作用】用一张图展示本系统与外部所有系统/用户的交互关系(数据流入流出)。明确系统在IT生态系统中的位置和接口,是系统架构设计的重要输入。
  • 3.4 假设与依赖:【风险管理作用】提前声明项目成功所依赖的外部条件(如“某第三方数据接口需在X月X日前提供”),识别早期风险。

4. 功能性需求:定义产品的具体能力(核心交付物)

  • 这是开发、测试工作的主要依据。
  • 4.x.x 概述:简要说明该功能模块的目的。
  • 4.x.x 用户故事/用例:【核心作用】以用户视角描述功能价值,是沟通的通用语言
  • 4.x.x 业务流程详述:【核心作用】用活动图、流程图或步骤描述,详细说明功能的正常流程、备选流程和异常流程。这是将用户故事转化为可执行逻辑的关键。
  • 4.x.x 业务规则:【关键作用】明确功能背后的逻辑判断条件、计算公式、约束限制。是开发业务逻辑层和测试用例的直接输入
  • 4.x.x 界面原型与说明:【可视化作用】将文字需求可视化,提前对齐UI/UX设计预期,减少返工。说明部分应解释关键交互和元素规则。
  • 4.x.x 验收标准:【合同作用】定义该功能“完成”的具体、可验证的条件。这是产品、开发、测试三方达成共识的“契约”,是测试用例的来源。

5. 非功能性需求:定义产品的品质与体验

  • 至关重要,常被忽视 决定产品在真实世界中是否“好用”和“耐用”。性能不好、不安全、难用的产品,功能再强也注定失败。
  • 5.1-5.6:分别从性能、安全、可用性、可靠性、兼容性、可维护性等维度提出可量化的指标(如“95%的页面加载时间小于2秒”),是测试和运维团队的工作基准。

6. 数据需求:定义系统的核心资产

  • 承上启下作用 业务需求的实现最终会体现在数据的产生、流转和存储上。此部分为数据库设计和前后端数据交互提供直接依据。

7. 外部接口需求:定义系统的协作方式

  • 集成作用】详细定义本系统与外界(用户、硬件、其他软件)通信的契约,包括API的格式、协议、频率、数据样例等。是系统联调和集成测试的圣经。

8. 约束与假设:划定方案的设计边界

  • 现实主义作用】明确告知开发团队,必须在哪些既定框架内进行设计(如“必须使用Oracle数据库”)。假设则是记录当前决策所基于的、尚未验证的判断,未来需要跟踪确认。

9. 附录:管理不确定性并提供追溯

  • 9.1 待确定问题列表:【透明化管理作用】公开记录所有悬而未决的问题(TBD),并指定负责人和解决日期。避免问题被遗忘,促进问题闭环。
  • 9.2 需求优先级矩阵:【指导迭代作用】清晰展示每个功能的优先级(如MoSCoW分类),是版本规划和迭代开发的直接输入
  • 9.3 需求追踪矩阵:【质量保障作用】建立从需求源头(如用户故事ID)到设计文档、测试用例的双向链接。确保每个需求都被实现和验证,是应对变更、进行影响分析的强大工具。

总结

  1. 先同步“为什么”(愿景、目标),建立共识。
  2. 再明确“为谁做”(用户、干系人),聚焦服务对象。
  3. 然后详细描述“做什么”和“做多好”(功能与非功能需求),提供明确指导。
  4. 最后界定“在什么框架下做”和“如何确认做完”(约束、验收、追踪),管理风险与质量。
1. 文档概述
   1.1. 修订历史
   1.2. 项目背景与目标
   1.3. 文档目的与范围
   1.4. 名词术语解释
   1.5. 参考文献

2. 干系人与用户分析
   2.1. 干系人列表与关注点
   2.2. 用户角色画像
   2.3. 用户场景概述

3. 总体概述
   3.1. 产品愿景
   3.2. 核心业务流程(流程图)
   3.3. 系统上下文图(系统与外部实体的关系)
   3.4. 假设与依赖

4. 功能性需求
   4.1. 模块/功能点A
       4.1.1. 概述
       4.1.2. 用户故事/用例
       4.1.3. 业务流程详述
       4.1.4. 业务规则
       4.1.5. 界面原型与说明
       4.1.6. 验收标准
   4.2. 模块/功能点B
   (结构同上)

5. 非功能性需求
   5.1. 性能需求(响应时间、吞吐量、并发用户数)
   5.2. 安全性需求(认证、授权、审计、数据加密)
   5.3. 可用性需求(易用性、可访问性、帮助文档)
   5.4. 可靠性需求(可用率、平均故障间隔时间、数据备份)
   5.5. 兼容性需求(浏览器、操作系统、设备、第三方系统)
   5.6. 可维护性与可扩展性需求

6. 数据需求
   6.1. 数据实体与关系
   6.2. 关键数据字段定义
   6.3. 数据管理需求(初始化、迁移、保留、归档)

7. 外部接口需求
   7.1. 用户接口(UI风格指南)
   7.2. 硬件接口
   7.3. 软件接口(API规范、第三方系统集成方式)
   7.4. 通信接口(协议、数据格式)

8. 约束与假设
   8.1. 技术约束(指定技术栈、平台等)
   8.2. 业务约束(合规性要求、运营限制)
   8.3. 项目约束(预算、工期、资源)
   8.4. 项目假设清单

9. 附录
   9.1. 待确定问题列表
   9.2. 需求优先级矩阵(MoSCoW分类)
   9.3. 需求追踪矩阵(链接到设计、测试用例)xxxxxxxxxx 初期(6个月):- 设备数量:10,000台- 日均消息数:1000万条(每台设备10秒上报一次)- 日均数据量:10GB(每条消息1KB)- 存储需求:热数据7天≈70GB,全年数据≈3.6TB中期(2年):- 设备数量:100,000台- 日均消息数:1亿条- 日均数据量:100GB- 存储需求:全年数据≈36TB

第四部分:概要设计文档

本章节延续“整体概要设计+软件概要设计+硬件概要设计”的结构,整体设计明确项目架构框架,软件、硬件概要设计分别独立阐述,明确各模块的核心设计思路、接口设计、模块间交互关系,为详细设计奠定基础。

4.1 整体概要设计

4.1.1 项目架构设计

项目采用“云-边-端”协同的四层架构,实现软硬件一体化协同运行,具体架构如下:

  1. 感知层:由各类硬件设备组成(感知设备、控制终端、通信模块等),负责数据采集与指令执行,是项目的“终端入口”;

  2. 边缘层:部署在设备侧的边缘计算节点,承担协议转换、数据预处理、本地规则计算等功能,减少网络带宽占用,实现本地故障快速响应;

  3. 平台层:即物联网设备管理软件平台,包含各类核心功能模块,负责设备管理、数据处理、远程控制、数据分析等,是项目的“核心大脑”;

  4. 应用层:面向不同场景的可视化界面与服务,为客户提供设备操作、数据查看、分析报告等服务,适配智能家居、智慧农业、智能工厂等多场景。

4.1.2 软硬件协同架构

软件平台与硬件设备通过标准化接口实现协同,具体交互流程如下:

  1. 硬件设备通过通信模块,采用MQTT/TCP/HTTP协议接入软件平台,完成身份验证;

  2. 感知设备采集数据后,通过通信模块上传至软件平台,软件平台对数据进行解析、存储与分析;

  3. 软件平台下发控制指令,通过通信模块传输至控制终端,控制终端执行指令,并将执行结果反馈至软件平台;

  4. 边缘层负责数据预处理与本地决策,当出现紧急情况时,可直接控制硬件设备,同时将相关信息上报至软件平台。

4.1.3 设计原则

  1. 模块化设计:软件、硬件均采用模块化设计,便于功能扩展、维护与升级;

  2. 标准化设计:接口、协议采用行业标准,确保软硬件兼容性与可扩展性;

  3. 稳定性设计:优先选择成熟技术与设备,确保系统与设备运行稳定,降低故障发生率;

  4. 安全性设计:融入安全设计理念,保障设备接入、数据传输、存储的安全性;

  5. 易用性设计:兼顾软件操作与硬件部署的易用性,降低客户使用与维护成本。

4.2 软件概要设计(独立模块)

4.2.1 软件架构设计

软件平台采用分层架构设计,从上至下分为应用层、业务逻辑层、数据访问层、数据存储层,各层独立运行、相互协作,具体如下:

  1. 应用层:面向用户的操作界面,包括设备管理界面、数据查看界面、远程控制界面、数据分析界面等,负责用户交互;

  2. 业务逻辑层:核心业务处理层,包含设备接入、数据采集、远程控制、数据分析、用户管理等功能模块,负责业务逻辑处理;

  3. 数据访问层:负责与数据存储层交互,实现数据的查询、新增、修改、删除等操作,为业务逻辑层提供数据支持;

  4. 数据存储层:采用数据库存储各类数据(设备数据、用户数据、日志数据等),确保数据安全、可靠。

4.2.2 核心模块设计

4.2.2.1 设备接入模块

  1. 模块功能:负责设备接入验证、协议解析、在线状态监测、接入日志记录;

  2. 核心逻辑:设备发起接入请求→模块验证设备身份(设备ID、密钥)→验证通过后,根据接入协议解析数据→记录接入日志,更新设备在线状态;

  3. 依赖模块:数据访问层(存储设备信息、接入日志)、数据采集模块(接收设备上传的数据)。

4.2.2.2 数据采集与存储模块

  1. 模块功能:接收设备上传的数据、解析数据格式、存储数据、异常数据检测、数据备份与恢复;

  2. 核心逻辑:接收设备数据→解析数据格式(统一为标准格式)→检测数据是否异常→将正常数据存储至数据库,异常数据触发提醒并记录→定期进行数据备份;

  3. 依赖模块:设备接入模块(获取设备数据)、数据访问层(存储数据)、数据分析模块(提供数据支持)。

4.2.2.3 远程控制模块

  1. 模块功能:下发控制指令、接收指令执行结果、记录指令日志、指令重试;

  2. 核心逻辑:用户发起控制指令→模块生成标准化指令→通过通信接口下发至设备→接收设备执行结果→记录指令日志,若执行失败则提醒并支持重试;

  3. 依赖模块:设备接入模块(获取设备在线状态)、数据访问层(存储指令日志)。

4.2.2.4 数据分析模块

  1. 模块功能:数据统计分析、数据可视化、自定义分析规则、生成分析报告;

  2. 核心逻辑:从数据存储层获取数据→根据预设规则或自定义规则进行统计分析→生成可视化图表与分析报告→提供数据查询与导出功能;

  3. 依赖模块:数据采集与存储模块(获取数据)、数据访问层(存储分析结果)。

4.2.2.5 用户管理与权限管理模块

  1. 模块功能:用户注册、登录、信息管理、权限分配、角色管理;

  2. 核心逻辑:用户注册/登录→验证用户信息→根据角色分配权限→用户可修改个人信息,管理员可管理用户与权限;

  3. 依赖模块:数据访问层(存储用户信息、权限信息)。

4.2.2.6 系统管理模块

  1. 模块功能:系统参数配置、日志管理、系统升级、数据备份与恢复;

  2. 核心逻辑:管理员配置系统参数→模块记录各类日志→支持系统在线升级→定期进行数据备份,出现异常时可恢复数据;

  3. 依赖模块:数据访问层(存储系统参数、日志数据、备份数据)。

4.2.3 接口设计

4.2.3.1 设备接入接口

  1. MQTT接口:用于设备与平台的数据交互,采用MQTT 3.1.1协议,端口:1883(TCP)、8883(SSL);

  2. TCP接口:用于设备与平台的双向通信,端口:8080,数据格式:JSON;

  3. HTTP接口:用于设备上传数据与接收指令,请求方式:POST/GET,数据格式:JSON。

4.2.3.2 内部模块接口

  1. 设备接入模块→数据采集模块:提供设备数据接口,传递设备上传的数据;

  2. 远程控制模块→设备接入模块:提供指令下发接口,传递控制指令;

  3. 各模块→数据访问层:提供数据查询、新增、修改、删除接口,实现数据交互。

4.2.3.3 外部接口(可选)

提供RESTful API接口,支持与第三方平台对接,数据格式:JSON,采用API密钥验证,确保接口安全。

4.2.4 数据存储设计

  1. 数据库选型:__________(根据自身技术选型填写,如MySQL、MongoDB等);

  2. 数据分类存储:

(1)设备数据:存储设备ID、设备类型、接入协议、在线状态、部署位置等信息;

(2)采集数据:存储设备上传的温湿度、运行参数等数据,按时间戳排序;

(3)用户数据:存储用户账号、密码(加密存储)、个人信息、角色权限等信息;

(4)日志数据:存储设备接入日志、操作日志、指令日志、异常日志等信息;

(5)系统数据:存储系统参数、配置信息、备份数据等信息。

4.2.5 技术选型补充

  1. 开发语言:__________(如Java、Python、Go等);

  2. 前端框架:__________(如Vue、React等);

  3. 服务器:__________(如阿里云、腾讯云服务器等);

  4. 其他技术:__________(如消息队列、缓存技术等,根据自身选型填写)。

4.3 硬件概要设计(独立模块)

4.3.1 硬件整体架构设计

硬件系统由感知层设备、控制终端、通信模块、辅助设备组成,各设备通过标准化接口对接,形成完整的硬件体系,具体架构如下:

  1. 感知层:由温湿度传感器、其他场景化传感器组成,负责采集环境与设备数据;

  2. 控制层:由控制终端组成,负责接收软件平台指令,控制感知设备与执行器;

  3. 通信层:由通信模块组成,负责实现硬件设备与软件平台的数据传输与指令交互;

  4. 辅助层:由电源设备、部署支架等组成,为硬件系统提供供电与部署支持。

4.3.2 核心硬件设备设计

4.3.2.1 感知设备设计

  1. 温湿度传感器:

(1)核心组件:传感器芯片、数据处理单元、接口单元;

(2)工作原理:传感器芯片采集温湿度数据,经数据处理单元转换为标准格式,通过接口单元传输至控制终端;

(3)选型补充:__________(根据自身硬件选型填写传感器型号、厂商等)。

4.3.2.2 控制终端设计

  1. 核心组件:主控芯片、接口单元、执行单元、电源单元;

  2. 工作原理:主控芯片接收通信模块传输的控制指令,控制执行单元执行相应操作,同时通过接口单元获取感知设备的数据,上传至通信模块;

  3. 选型补充:__________(根据自身硬件选型填写主控芯片型号、厂商等)。

4.3.2.3 通信模块设计

  1. 核心组件:通信芯片、天线、接口单元、电源单元;

  2. 工作原理:通过通信芯片实现与软件平台的协议对接,接收平台指令并传输至控制终端,同时将控制终端上传的数据传输至平台;支持自动重连功能;

  3. 选型补充:__________(根据自身硬件选型填写通信模块型号、厂商、通信方式等)。

4.3.2.4 辅助设备设计

  1. 电源设备:采用市电+电池双供电模式,确保供电稳定;电池采用锂电池,支持充电与低功耗保护;

  2. 部署支架:采用防水、防尘、抗干扰设计,适配室内外部署,便于安装与固定。

4.3.3 硬件接口设计

  1. 感知设备与控制终端接口:采用UART接口,用于数据传输,波特率:9600bps;

  2. 控制终端与通信模块接口:采用SPI接口,用于指令与数据传输;

  3. 电源接口:采用DC 5V接口,支持市电与电池接入;

  4. 扩展接口:预留I2C接口,用于后续新增设备扩展。

4.3.4 硬件协同设计

  1. 数据传输流程:感知设备采集数据→通过UART接口传输至控制终端→控制终端处理数据→通过SPI接口传输至通信模块→通信模块通过MQTT/TCP/HTTP协议上传至软件平台;

  2. 指令执行流程:软件平台下发指令→通信模块接收指令→通过SPI接口传输至控制终端→控制终端解析指令→控制执行单元执行操作→将执行结果反馈至软件平台;

  3. 异常处理:当硬件设备出现故障时,控制终端触发异常提醒,通过通信模块上传至软件平台,同时启动备用机制(如备用电源、本地控制),确保业务连续性。

4.4 概要设计评审

评审人:__________

评审意见:__________

评审日期:__________

第五部分:详细设计文档

本章节在概要设计基础上,对软件各模块、硬件各部件、数据库、接口等进行细化设计,明确具体实现逻辑、数据结构、流程细节、硬件电路与固件设计,为编码与硬件集成提供直接依据。

5.1 软件详细设计(独立模块)

5.1.1 设备接入模块详细设计

5.1.1.1 模块类设计(以Java/Spring为例)

类名职责关键方法
DeviceAuthService设备身份验证authenticate(deviceId, secret)
MqttGatewayMQTT协议处理handleMqttMessage(topic, payload)
TcpServerHandlerTCP长连接处理channelRead(ChannelHandlerContext, Object)
HttpDeviceControllerHTTP设备接入接口uploadData(@RequestBody DeviceData)
DeviceStatusManager设备在线状态管理updateStatus(deviceId, status), heartbeat(deviceId)

5.1.1.2 设备接入流程

  1. MQTT接入:设备连接Broker(EMQX/Mosquitto)→ 携带用户名/密码 → Broker回调MqttGateway → 验证设备身份 → 订阅系统主题 → 记录接入日志 → 更新在线状态。
  2. TCP接入:设备建立Socket连接 → 发送认证JSON({deviceId, secret})→ 服务端解析验证 → 维持长连接 → 心跳保活(每30秒)。
  3. HTTP接入:设备POST /api/device/upload → 携带X-Device-IdX-Token → 验证通过后返回200 → 数据进入采集队列。

5.1.1.3 状态管理机制

  • 使用Redis存储设备在线状态,key:device:status:{deviceId},value:online/offline,TTL:90秒(心跳超时)。
  • 监听MQTT的$SYS/brokers/+/clients/+/disconnect事件主动感知离线。
  • TCP连接断开时自动更新状态。

5.1.2 数据采集与存储模块详细设计

5.1.2.1 数据采集流程

  1. 设备上报数据 → 模块接收(同步或异步消息队列Kafka/RabbitMQ)→ 格式校验 → 解析为标准JSON结构:

    json

    {
      "deviceId": "xxx",
      "timestamp": 1700000000000,
      "data": {"temperature": 25.6, "humidity": 60}
    }
    
  2. 异常检测:根据预设阈值(如温度>50℃)触发告警,写入告警表。

  3. 存储策略:高频采集数据(秒级)写入时序数据库(如InfluxDB/TimescaleDB);设备配置、元数据写入关系库(MySQL/PG)。

5.1.2.2 数据清理与备份

  • 原始数据保存30天,自动转存到冷存储(对象存储)或删除。
  • 每日凌晨2点执行数据备份(全量+增量),备份保留7天。

5.1.3 远程控制模块详细设计

5.1.3.1 控制指令下发流程

  1. 用户在Web端点击“关闭开关” → 前端调用POST /api/control/send
  2. 后端生成指令ID(UUID) → 记录到control_log表,状态为pending
  3. 根据设备协议类型:
    • MQTT:发布到设备专属topic device/{deviceId}/control
    • TCP:通过对应Channel写入指令JSON
    • HTTP:调用设备提供的回调URL
  4. 设备执行后回复ACK → 模块更新指令状态为succeeded/failed
  5. 若10秒内未收到ACK,触发重试(最多3次),仍失败则状态置为failed并告警。

5.1.3.2 批量控制设计

  • 支持选择多个设备 → 后台并发调用单设备控制逻辑,使用线程池(最大10线程)。
  • 记录批量任务ID,可查询每个设备的执行结果。

5.1.4 用户管理与权限模块详细设计

5.1.4.1 权限模型(RBAC)

  • 表结构:userrolepermissionuser_rolerole_permission
  • 预置角色:
    • 管理员:所有权限
    • 普通用户:仅查看自己设备的实时数据及历史数据
    • 运维人员:设备调试、固件升级、故障日志查看

5.1.4.2 认证与授权

  • JWT令牌,有效期24小时,刷新令牌7天。
  • 接口权限使用Spring Security注解@PreAuthorize("hasPermission(...)")
  • 设备级权限:用户与设备通过user_device表关联,查询时自动过滤。

5.1.5 接口详细设计(RESTful API示例)

5.1.5.1 设备注册接口

text

POST /api/device/register
Request Body: { "deviceName": "sensor_01", "protocol": "MQTT", "productKey": "xxx" }
Response: { "deviceId": "d_xxx", "secret": "xxxx" }

5.1.5.2 数据查询接口

text

GET /api/data/latest?deviceId=xxx
Response: { "deviceId": "xxx", "data": {...}, "timestamp": 1700000000 }

5.1.5.3 控制指令接口

text

POST /api/control/send
Request: { "deviceId": "xxx", "command": "turn_off", "params": {} }
Response: { "commandId": "cmd_xxx", "status": "pending" }

5.2 硬件详细设计(独立模块)

5.2.1 感知设备详细设计(以温湿度传感器为例)

5.2.1.1 硬件选型(示例)

组件型号/规格说明
传感器芯片SHT30精度:±0.3℃ / ±2%RH
主控MCUESP32-C3支持Wi-Fi/BLE,低功耗
通信模块内置Wi-Fi支持MQTT/TCP
电源3.7V锂电池 + 充电管理TP4056续航约6个月(每小时上报一次)

5.2.1.2 电路连接

  • SHT30的SCL→ESP32的IO22,SDA→IO21,VCC→3.3V,GND→GND
  • 电池正极→TP4056的BAT+,TP4056的OUT+→ESP32的VIN
  • 预留UART0作为调试口

5.2.1.3 固件设计

  • 采用Arduino/ESP-IDF开发
  • 主循环:读取传感器(每10秒一次)→ 平均值计算(每分钟)→ 通过MQTT上报 → 进入深度睡眠(剩余时间)
  • 上报频率可远程配置(默认60秒)
  • 支持OTA升级

5.2.2 控制终端详细设计

5.2.2.1 硬件组成

  • 主控:STM32F103C8T6
  • 继电器模块(控制220V设备)
  • 通信接口:SPI接ESP8266(透传MQTT)
  • 本地存储:AT24C02(保存设备配置)

5.2.2.2 控制逻辑

  • 监听通信模块转发的控制指令 → 解析指令类型(开关、PWM调光等)→ 驱动GPIO/继电器 → 读取传感器反馈(可选)→ 返回执行结果。

5.2.3 硬件协同时序

text

[传感器] --> UART --> [控制终端] --> SPI --> [通信模组] --> MQTT --> [平台]
[平台]   --> MQTT --> [通信模组] --> SPI --> [控制终端] --> GPIO --> [执行器]

5.3 数据库详细设计

5.3.1 关系型数据库表设计(MySQL)

5.3.1.1 设备表 device

字段类型说明
device_idVARCHAR(32) PK设备唯一标识
device_nameVARCHAR(64)设备名称
protocolENUM('MQTT','TCP','HTTP')接入协议
product_keyVARCHAR(32)产品型号
secretVARCHAR(64)设备密钥(加密存储)
statusTINYINT0-离线,1-在线
last_active_timeDATETIME最后心跳时间
created_timeDATETIME注册时间

5.3.1.2 用户表 user

字段类型说明
user_idINT AUTO PK
usernameVARCHAR(32) UNIQUE
passwordVARCHAR(128)bcrypt加密
role_idINT关联角色表
......

5.3.1.3 指令日志表 control_log

字段类型说明
command_idVARCHAR(36) PK
device_idVARCHAR(32)
commandTEXT指令内容
statusVARCHAR(16)pending/succeeded/failed
retry_countINT重试次数
create_timeDATETIME
finish_timeDATETIME

5.3.2 时序数据库设计(InfluxDB)

  • 测量名:device_data
  • Tag:device_idsensor_type
  • Field:value(数值),unit(单位)
  • Timestamp:毫秒级时间戳

示例查询:SELECT mean(value) FROM device_data WHERE device_id='xxx' AND time > now()-1d

第六部分:测试文档

6.1 测试计划

6.1.1 测试范围与策略

  • 单元测试:软件各模块方法级测试,覆盖率≥80%
  • 集成测试:模块间接口、软硬件协同通信
  • 系统测试:端到端功能、性能、安全性、兼容性
  • 硬件测试:传感器精度、通信距离、功耗、环境适应性

6.1.2 测试环境

  • 软件:测试服务器(4C8G)、MySQL、InfluxDB、EMQX、Chrome浏览器
  • 硬件:温湿度传感器5,控制终端3,通信模组*3,可调温湿箱,直流电源

6.1.3 测试里程碑

阶段时间输出物
单元测试第9-10周单元测试报告
集成测试第11周集成测试报告
系统测试第12周系统测试报告、缺陷清单
硬件测试并行硬件测试报告
验收测试第13周验收测试报告

6.2 测试用例

6.2.1 功能测试用例(部分)

用例ID模块测试项前置条件输入/操作预期结果优先级
TC-SW-001设备接入MQTT设备正常接入平台已部署,MQTT Broker运行设备使用正确ID/Secret连接连接成功,设备状态变更为“在线”P0
TC-SW-002设备接入错误密钥拒绝同上使用错误Secret连接连接拒绝,日志记录失败P1
TC-SW-003数据采集接收并存储温湿度设备已在线设备上报温湿度数据数据在InfluxDB可查,前端显示正确P0
TC-SW-004数据采集异常数据告警设备上报温度>80℃同上系统产生告警记录,前端提示P1
TC-SW-005远程控制下发开关指令控制终端在线点击“关闭”按钮设备执行关闭,指令状态成功P0
TC-SW-006远程控制离线设备控制设备离线下发指令提示设备离线,指令状态失败P1
TC-SW-007权限管理普通用户访问其他设备用户A只绑定设备1用户A尝试查看设备2数据返回403或无权限提示P0
TC-SW-008数据分析历史数据曲线已有24小时数据选择设备、时间范围展示正确的折线图P1

6.2.2 性能测试用例

用例ID测试项负载条件指标预期结果
TC-PERF-001并发设备接入1000个MQTT设备同时连接连接成功率≥99.5%成功率达标,CPU≤70%
TC-PERF-002数据上报吞吐500设备同时每秒上报1条消息处理延迟≤500ms无积压,延迟达标
TC-PERF-003控制指令并发100个并发指令响应时间≤1s95%指令在1s内返回

6.2.3 硬件测试用例

用例ID测试项方法判定标准
TC-HW-001温度精度与标准温度计对比(0℃,25℃,50℃)误差≤±0.5℃
TC-HW-002湿度精度与标准湿度计对比(30%,60%,90%RH)误差≤±5%RH
TC-HW-003功耗测试电池供电,每小时上报一次续航≥6个月(实测计算)
TC-HW-004通信距离开阔场地测试Wi-Fi连接距离≥50米稳定连接
TC-HW-005高低温工作-20℃~60℃恒温箱运行2小时设备不宕机,数据正常

6.2.4 安全测试用例

用例ID测试项操作预期结果
TC-SEC-001未授权访问不带Token访问API返回401
TC-SEC-002SQL注入在设备ID参数中输入 ' OR '1'='1查询失败或转义,不泄露数据
TC-SEC-003通信加密抓包MQTT数据应看到TLS加密,不能明文看到密码

6.3 测试报告(模板)

6.3.1 测试概要

  • 测试版本:v1.0
  • 测试周期:202X年X月X日 - 202X年X月X日
  • 总用例数:120,通过:115,失败:5,阻塞:0
  • 缺陷总数:8(严重2,一般4,轻微2)

6.3.2 缺陷分析

缺陷ID模块描述严重程度状态
BUG-01设备接入TCP连接偶尔掉线不重连严重已修复
BUG-02远程控制批量控制时部分设备未收到指令一般已修复
...............

6.3.3 测试结论

  • 核心功能满足需求,性能指标达标,硬件精度符合标准。
  • 建议修复剩余轻微问题后上线。

第七部分:部署与运维文档

7.1 部署方案

7.1.1 软件部署架构

  • 负载均衡:Nginx(HTTPS卸载)
  • 后端服务:Spring Boot(jar包),systemd管理,3节点集群
  • 前端:Vue打包静态文件,Nginx托管
  • 数据库:MySQL主从+读写分离,InfluxDB集群
  • 消息中间件:Kafka(数据采集缓冲)
  • MQTT Broker:EMQX集群(3节点)

7.1.2 部署步骤(摘要)

  1. 安装Docker及docker-compose(或K8s)
  2. 拉取镜像:MySQL, InfluxDB, EMQX, Redis, Kafka, 后端服务镜像
  3. 配置环境变量:数据库连接、JWT密钥、MQTT地址
  4. 执行数据库初始化脚本(schema.sql,seed.sql)
  5. 启动所有容器,验证健康检查
  6. 配置Nginx反向代理与SSL证书(Let's Encrypt)
  7. 硬件设备配置:烧录固件,配置平台域名

7.1.3 硬件部署指导

  • 温湿度传感器:室内壁挂,离地1.5米,避免阳光直射
  • 控制终端:靠近被控设备,确保Wi-Fi信号强度≥-70dBm
  • 通信模块天线竖直向上,远离金属遮挡

7.2 运维手册

7.2.1 日常巡检项

  • 每日:检查服务进程、磁盘使用率、数据库连接数
  • 每周:查看错误日志,清理过期数据
  • 每月:安全补丁更新,性能容量评估

7.2.2 常见问题处理

问题现象可能原因处理步骤
设备无法接入Broker挂掉docker ps 检查EMQX,重启容器
数据显示延迟Kafka积压增加消费者实例或扩容
控制指令超时设备网络差检查设备RSSI,建议移近路由器

7.2.3 备份与恢复

  • 数据库每日全量备份脚本(mysqldump + influx backup)
  • 备份保留到OSS,保留30天
  • 恢复:停止服务 → 恢复备份 → 重启验证

第八部分:用户手册(概要)

8.1 平台操作指南

8.1.1 登录与注册

  • 访问 https://iot.xxx.com,首次使用需注册企业账号
  • 登录后进入仪表盘

8.1.2 设备管理

  • 添加设备:点击“设备管理”→“添加设备”→输入设备ID和密钥(设备外壳标签上)→选择协议→完成
  • 查看设备:列表显示设备状态,点击可查看实时数据与历史曲线

8.1.3 远程控制

  • 在设备详情页,点击“控制”选项卡 → 选择控制命令(开关、调节等)→ 确认发送 → 显示执行结果

8.1.4 数据分析

  • “数据报表”菜单 → 选择设备、时间范围、数据类型 → 生成图表,可导出Excel

8.2 硬件安装指南(以温湿度传感器为例)

  1. 打开包装,取出传感器主体和支架
  2. 使用附赠的Micro-USB线充电2小时(红灯充电,绿灯满电)
  3. 下载配网App或通过微信小程序,长按设备按键5秒进入配网模式
  4. 输入Wi-Fi密码,等待提示“配网成功”
  5. 登录平台查看设备是否在线

第九部分:项目验收文档

9.1 验收计划

  • 验收时间:测试阶段结束后3个工作日内
  • 参与人员:客户代表、项目负责人、测试经理
  • 验收标准:需求文档中的所有功能均已实现且通过测试;性能指标达标;硬件精度符合规格;文档齐全

9.2 验收清单

编号验收项是否满足(是/否)备注
1软件平台所有功能模块可正常使用
2支持MQTT/TCP/HTTP三种协议设备接入
3设备在线状态实时更新
4数据采集延迟≤500ms
5远程控制响应时间≤1s
6并发1000设备在线,系统稳定
7硬件传感器精度达标
8硬件通信距离≥50米
9提供完整文档(需求、设计、测试、部署、用户手册)
10提供源代码与固件代码

9.3 验收结论

  • 验收通过 □ 不通过 □
  • 遗留问题及处理计划:__________
  • 验收签字:
    • 客户代表:__________
    • 项目负责人:__________
    • 日期:__________

chat1:

物联网项目文档编写,我打算实现一个物联网设备管理平台,这个平台每个用户可以通过mqtt,tcp,http等方式将设备接入到平台。我打算作为一个软硬件一体化提供商,提供物联网服务,如智能家居控制,温湿度等硬件设备数据上报,智慧农业,智能工厂等,为客户提供软件和硬件服务。现在需要把文档从立项到测试所有文档进行实现。同时是否要区分软件和硬件文档,还是整合在一起。软件技术选型和硬件设备选型心里已经有底了,软硬件各种文档是不是要独立出来?还是整合到一个,尽你所能帮我把文档完成,要求从研究背景,可行性分析,需求分析,概要设计,详细设计,测试用例等一整套软件工程,以及硬件设备开发工程文档

物模型

功能说明

物模型是物理空间中的实体(如传感器、车载装置、楼宇、工厂等)在云端的数字化表示,从属性、服务和事件三个维度,分别描述了该实体是什么、能做什么、可以对外提供哪些信息。定义了物模型的这三个维度,即完成了产品功能的定义

功能类型说明
属性(Property)用于描述设备运行时具体信息和状态。例如,环境监测设备所读取的当前环境温度、智能灯开关状态、电风扇风力等级等。属性可分为读写和只读两种类型。读写类型支持读取和设置属性值,只读类型仅支持读取属性值。
服务(Service)指设备可供外部调用的指令或方法。服务调用中可设置输入和输出参数。输入参数是服务执行时的参数,输出参数是服务执行后的结果。相比于属性,服务可通过一条指令实现更复杂的业务逻辑,例如执行某项特定的任务。服务分为异步和同步两种调用方式。
事件(Event)设备运行时,主动上报给云端的信息,一般包含需要被外部感知和处理的信息、告警和故障。事件中可包含多个输出参数。例如,某项任务完成后的通知信息;设备发生故障时的温度、时间信息;设备告警时的运行状态等。事件可以被订阅和推送。

物联网平台支持为产品定义多组功能(属性、服务和事件)。一组功能定义的集合,就是一个物模型模块。多个物模型模块,彼此互不影响。

物模型模块功能,解决了工业场景中复杂的设备建模,便于在同一产品下,开发不同功能的设备。

例如,电暖扇产品的功能属性有电源开关档位(高、中、低)室内温度,您可以在一个模块添加前2个属性,在另一个模块添加三个属性,然后分别在不同设备端,针对不同物模型模块功能进行开发。此时,该产品下不同设备就可以实现不同功能。

CREATE TABLE thing_model (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    model_id VARCHAR(64) UNIQUE NOT NULL COMMENT '物模型ID',
    model_name VARCHAR(100) NOT NULL COMMENT '物模型名称',
    model_version VARCHAR(20) NOT NULL DEFAULT '1.0' COMMENT '模型版本',
    product_key VARCHAR(64) COMMENT '产品标识(与产品唯一绑定)',
    product_id VARCHAR(64) COMMENT '产品ID',
    category VARCHAR(50) COMMENT '设备品类',
    protocol_type VARCHAR(30) COMMENT '协议类型(MQTT/CoAP/HTTP等)',
    description TEXT COMMENT '模型描述',
    status TINYINT DEFAULT 1 COMMENT '状态:0-启用,1-禁用',
    is_standard TINYINT DEFAULT 0 COMMENT '是否标准模型',
    extend_data JSON COMMENT '扩展字段',
   `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
   `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者',
   `create_time` datetime DEFAULT NULL COMMENT '创建时间',
   `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新者',
   `update_time` datetime DEFAULT NULL COMMENT '更新时间'
) COMMENT='物模型';
CREATE TABLE thing_property (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    model_id VARCHAR(64) NOT NULL COMMENT '物模型ID',
    identifier VARCHAR(100) NOT NULL COMMENT '属性标识符',
    name VARCHAR(100) NOT NULL COMMENT '属性名称',
    data_type VARCHAR(30) NOT NULL COMMENT '数据类型:int/float/double/string/bool/enum/date/struct等',
    data_spec JSON COMMENT '数据规范(JSON格式)',
    unit VARCHAR(20) COMMENT '单位',
    access_mode TINYINT NOT NULL COMMENT '访问模式:1-只读,2-只写,3-读写',
    required TINYINT DEFAULT 0 COMMENT '是否必选:0-否,1-是',
    is_primary TINYINT DEFAULT 0 COMMENT '是否为主属性',
    description TEXT COMMENT '属性描述',
    sort_order INT DEFAULT 0 COMMENT '排序',
    extend_config JSON COMMENT '扩展配置',
    `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
   `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者',
   `create_time` datetime DEFAULT NULL COMMENT '创建时间',
   `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新者',
   `update_time` datetime DEFAULT NULL COMMENT '更新时间'
) COMMENT='属性定义表';
功能说明
项目管理
设备接入
设备管理

时序数据库(TSDB)

物联网设备上报的数据具有写多读少、高并发、数据带有时间戳、极少更新的特点。时序数据库正是为此设计的。

TimescaleDB ,InfluxDB等

利用 Redis 做缓冲

  • 架构模式
    1. 设备上报 -> MQTT Broker (如EMQX) -> Redis 队列/消息队列
    2. 消费组 -> 批量写入 -> MySQL (分库分表)
物模型(thing_model)
    │
    ├── 产品(product) ←─────── 设备(device)
    │        │                      │
    │        ├── 产品模型关联         ├── 设备模型关联
    │        │                      │
    │        └── 分组(group) ←───────┼── 设备分组关系
    │                                │
    ├── 属性(property)               ├── 属性值快照
    ├── 服务(service)                ├── 动态属性/服务
    └── 事件(event)                  └── 设备影子

该项目要求针对不同的企业提供接入功能。

项目

每个账号下面都有项目

设备管理

设备接入

设备

1. 背景

现在的md笔记工具,例如语雀等。他们的web版本快捷方式和typora区别很大。对于习惯了typora的使用者很不友好。所以现在现在打算做一个md笔记编辑管理项目。以方便使用。

2. 可行性分析

  • 可行!
  • 主打开源免费。
  • 付费功能,内网穿透。

3. 需求

3.1 技术要求

  • 前端后端不分离。
  • 要求必须轻量级直接部署。
  • 支持跨平台,win,linux都支持。
    • win直接exe启动。
    • linux直接命令行启动。

3.2 功能要求

  • 登录功能:后端地址需要填写,默认本机,为空为本机。密码需要填写。密码在配置文件中,可以在部署的机器上的配置文件做配置。后端地址配置之后应该要写入配置文件,做保存。应该要记录以前配置过的后端地址。

  • 编辑功能:首先编辑器必须要完成类似typora快捷键。

  • 穿透(前期先不做):

    • 后端穿透:只将后端接口穿透出来
    • 全部穿透:将前端,后端所有功能穿透出来。
  • 穿透/付费功能(前期先不做):

    • 使用我官方前端地址,只需要配置后端已经完成穿透的后端域名和端口。
    • 自己配置穿透,直接走自己的穿透地址。

2026.03.23:

  • jenkins-pipeline使用
  • spring-security框架
  • OTA升级流程,java后端设计,设备端功能设计

03.24

  • 回调函数和状态机
  • kafka
  • 单片机烧录bin文件hex文件解析。

03.31

Ai+语言+工具控制+硬件设备,智能化。

arm-linux-gcc 交叉编译器的版本多样,主要区别在于目标架构、浮点支持和ABI等方面。以下是详细的分类和说明:

一、按目标架构位数分类

1. 32位ARM架构

  • 命名模式arm-linux-gcc(传统命名)
  • 目标:ARMv4T到ARMv7-A/R的32位CPU
  • 常见前缀
    • arm-linux-(传统)
    • arm-none-linux-gnueabi-(EABI)
    • armv7l-linux-gnueabihf-(带硬浮点)
  • 支持平台:Cortex-A7/A8/A9/A15, ARM9, ARM11等

2. 64位ARM架构(AArch64)

  • 命名模式aarch64-linux-gcc
  • 目标:ARMv8-A及以上的64位CPU
  • 常见前缀
    • aarch64-linux-gnu-
    • aarch64-none-linux-gnu-
  • 支持平台:Cortex-A53/A57/A72/A73/A75/A76等

二、按浮点支持方式分类(针对32位ARM)

1. 软浮点(Soft-float)

  • 命名arm-linux-gnueabi(无"hf"后缀)
  • 特点
    • 浮点运算通过软件库模拟
    • 无硬件FPU要求
    • 代码体积大,速度慢
  • 使用场景
    • 无FPU的旧ARM芯片(ARM9, ARM11早期)
    • 兼容性要求高的场景

2. 硬浮点(Hard-float)

  • 命名arm-linux-gnueabihf(带"hf"后缀)
  • 特点
    • 直接使用硬件FPU指令
    • 需要目标CPU有FPU
    • 速度快,效率高
  • ABI差异
    • 浮点参数通过FPU寄存器传递
    • 与软浮点ABI不兼容
  • 使用场景
    • Cortex-A系列(有NEON/FPU)
    • 需要高性能浮点运算

3. 软浮点ABI兼容硬浮点

  • 命名:较少见,需要特定配置
  • 特点
    • 使用硬浮点指令
    • 但保持软浮点ABI(通过寄存器传递参数)
    • 兼容性较好

三、按EABI版本分类

1. OABI(旧ABI)

  • 命名arm-linux-(简单命名)
  • 特点
    • 旧的ARM ABI标准
    • 结构体对齐、系统调用等与EABI不同
    • 已基本淘汰

2. EABI(嵌入式ABI)

  • 命名arm-none-linux-gnueabi
  • 特点
    • 现代ARM标准ABI
    • 更好的性能,更小的代码体积
    • 支持位置无关代码(PIC)

四、常见版本具体示例

text

# 32位ARM,软浮点
arm-none-linux-gnueabi-gcc          # 最常用软浮点
arm-linux-gnueabi-gcc               # 简化命名

# 32位ARM,硬浮点
arm-none-linux-gnueabihf-gcc        # 最常用硬浮点
arm-linux-gnueabihf-gcc             # 简化命名

# 特定架构优化版本
armv5l-linux-gnueabi-gcc            # ARMv5,软浮点
armv7l-linux-gnueabihf-gcc          # ARMv7,硬浮点

# 64位ARM
aarch64-linux-gnu-gcc               # 标准64位
aarch64-none-linux-gnu-gcc          # Bare-metal风格命名

五、选择指南

判断方法:

  1. 查看目标CPU规格

    bash

    # 在目标板上执行
    cat /proc/cpuinfo
    uname -m
    
  2. 检查FPU支持

    bash

    # 如果有以下内容,支持硬浮点
    Features : swp half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 
    # vfp/neon表示FPU支持
    

选择矩阵:

目标平台推荐工具链说明
ARM9, ARM11(无FPU)arm-linux-gnueabi必须用软浮点
Cortex-A5/A7/A8arm-linux-gnueabihf有VFP/NEON
Cortex-A9/A15arm-linux-gnueabihf性能最优
ARMv8 64位aarch64-linux-gnu64位系统
未知平台arm-linux-gnueabihf现代ARM通用

核心概念:值传递(Pass by Value) vs. 引用/指针传递(Pass by Reference/Pointer)

在分析具体语言之前,必须理解这个根本区别:

  • 值传递 (Pass by Value): 函数接收到的是实参的一个副本。在函数内部修改这个副本,不会影响原始的实参。
  • 引用/指针传递 (Pass by Reference/Pointer): 函数接收到的是实参的内存地址(引用可以理解为语法糖形式的指针)。通过这个地址直接操作原始数据,在函数内部的修改会影响原始的实参。

1. Java 的情况

Java的语言规范规定,所有方法的参数传递都是“值传递”

java

class MyClass {
    public int value;
}

public class Test {
    public static void modifyObject(MyClass obj) { // obj是传入引用的一个副本
        obj.value = 10; // 通过副本找到原始对象,修改其属性
        obj = new MyClass(); //** 这里让副本指向一个新对象,但原始实参的指向不变
        obj.value = 20;
    }

    public static void main(String[] args) {
        MyClass myObj = new MyClass();
        myObj.value = 5;
        System.out.println("Before: " + myObj.value); // Output: Before: 5

        modifyObject(myObj); // **将myObj保存的引用(地址)复制一份传给obj

        System.out.println("After: " + myObj.value); // Output: After: 10
    }
}

分析:

  1. myObj 是一个引用变量,它的值是一个指向堆中MyClass对象的地址。
  2. 调用modifyObject(myObj)时,传递的是myObj这个引用变量里存储的地址值的副本。所以形参obj和实参myObj指向同一个对象
  3. obj.value = 10; 通过副本地址找到了原始对象,并修改了它的属性。所以外部的myObj.value也变成了10。
  4. obj = new MyClass(); 这一步是让形参obj这个副本指向了一个全新的对象。这与外部的myObj已经完全无关了。myObj仍然指向原来的对象。
  5. 结论: Java通过值传递引用的副本。你可以通过这个副本修改它所指向的对象的内容,但你不能通过修改副本本身(让它指向新对象)来影响实参的指向。

简单说:Java中,对于对象,你可以修改对象的“内容”,但不能修改外部变量的“指向”。

2. C++ 的情况

C++提供了更灵活的参数传递方式,这也是它比Java更复杂但也更强大的地方。

a) 值传递一个对象

cpp

#include <iostream>
using namespace std;

class MyClass {
public:
    int value;
};

void modifyByValue(MyClass obj) { // 值传递,调用拷贝构造函数生成obj副本
    obj.value = 10; // 修改的是副本的属性
} // 函数结束,副本obj被销毁

int main() {
    MyClass myObj;
    myObj.value = 5;
    cout << "Before: " << myObj.value << endl; // Output: Before: 5

    modifyByValue(myObj);

    cout << "After: " << myObj.value << endl; // Output: After: 5 (未改变)
    return 0;
}

分析: 和内置类型(如int)的值传递完全一样。myObj被完整地复制了一份给形参obj。函数里修改的是副本,原对象myObj丝毫未受影响。效率较低,尤其是对于大型对象,因为涉及拷贝构造。

b) 传递一个对象指针

cpp

void modifyByPointer(MyClass *objPtr) { // 传递的是对象地址的副本
    objPtr->value = 10; // 通过地址修改原始对象的属性
    // objPtr = new MyClass(); // 如果这样做,和Java一样,只修改了地址副本,不影响实参指针
    // delete ...; // 需要手动管理新对象的内存,容易出错
}

int main() {
    MyClass myObj;
    myObj.value = 5;
    cout << "Before: " << myObj.value << endl; // Output: Before: 5

    modifyByPointer(&myObj); // 传递myObj的地址

    cout << "After: " << myObj.value << endl; // Output: After: 10 (已改变)
    return 0;
}

分析: 传递的是指针(地址)的值。objPtr&myObj这个地址值的一个副本。通过这个地址副本,可以找到并修改原始对象myObj效果上是“引用传递”的效果,但机制上仍然是“值传递”(传递的是指针这个值本身)

c) 引用传递一个对象

cpp

void modifyByReference(MyClass &objRef) { // objRef是myObj的别名(引用)
    objRef.value = 10; // 直接操作原始对象
    // objRef = MyClass(...); // 这里赋值会直接改变原始对象!
}

int main() {
    MyClass myObj;
    myObj.value = 5;
    cout << "Before: " << myObj.value << endl; // Output: Before: 5

    modifyByReference(myObj); // 直接传递对象本身,语法更简洁

    cout << "After: " << myObj.value << endl; // Output: After: 10 (已改变)
    return 0;
}

分析这是真正的“引用传递”。形参objRef不是副本,而是实参myObj的一个别名。它们根本就是同一个东西。函数内对objRef的任何操作都等价于直接对myObj操作。语法简洁,效率高(无拷贝开销),是C++中修改入参的推荐方式


3. C 语言的情况

C语言没有引用和类的概念,只有结构体(struct)和指针。

a) 将结构体作为参数值传递

c

#include <stdio.h>

typedef struct {
    int value;
} MyStruct;

void modifyStruct(MyStruct s) { // 值传递,发生结构体拷贝
    s.value = 10; // 修改的是副本
}

int main() {
    MyStruct myStruct;
    myStruct.value = 5;
    printf("Before: %d\n", myStruct.value); // Output: Before: 5

    modifyStruct(myStruct);

    printf("After: %d\n", myStruct.value); // Output: After: 5 (未改变)
    return 0;
}

分析: 和C++的值传递对象完全一样。整个结构体的内容被复制到形参s中,修改副本不影响原始结构体。对于大型结构体,性能开销很大

b) 将结构体指针传递

c

void modifyStructByPointer(MyStruct *sPtr) { // 传递结构体指针(地址)
    sPtr->value = 10; // 通过指针(地址)修改原始结构体
}

int main() {
    MyStruct myStruct;
    myStruct.value = 5;
    printf("Before: %d\n", myStruct.value); // Output: Before: 5

    modifyStructByPointer(&myStruct); // 传递myStruct的地址

    printf("After: %d\n", myStruct.value); // Output: After: 10 (已改变)
    return 0;
}

传参为结构体数组

int delete_contact(struct person contacts[], int count){
	//传入的是数组的首地址。也就是指针指向这个地址
}

分析: 这是C语言中实现“函数内部修改外部结构体”的标准做法。机制和效果与C++的指针传递完全相同。传递的是地址值的副本,通过这个地址副本可以找到并修改原始数据。


总结与对比

语言传递方式语法示例是否修改原对象效率说明
Java对象引用的值传递modify(Object obj)可以修改对象内容传递引用的副本。可修改指向的对象,但不能让实参指向新对象。
C++对象值传递modify(Class obj)不能创建完整副本,修改不影响原对象。
C++对象指针的值传递modify(Class *objPtr)可以传递指针副本。通过指针修改原对象。机制同C。
C++对象引用传递modify(Class &objRef)可以推荐方式。形参是实参的别名,操作的是原对象。
C结构体值传递modify(Struct s)不能创建完整副本,修改不影响原结构体。
C结构体指针传递modify(Struct *sPtr)可以标准做法。传递指针副本,通过指针修改原结构体。

核心记忆点:

  1. Java: 只有一种方式(值传递引用),效果是“半修改”(能改内容,不能改指向)。
  2. C++: 有三种方式。想要在函数内修改外部对象,优先使用引用传递(&)。指针传递也可以,但语法更繁琐。值传递通常不用于需要修改的场景。
  3. C: 只有两种方式。必须使用指针传递(*)来修改外部结构体。值传递只用于不需要修改且结构体很小的场景。

针对java:传参可以理解成指针的副本,myClass clazz = new myClass(), 将clazz参数传入方法。可以类比成,clazz是一个指针指向一个对象(new myClass()),传入时类比成新建了一个指针也只向new myClass()。所以就能修改属性。

针对java:基本数据类型,int,long,char之类的8种基本数据类型。采用的是真正的值传递

创建

  1. 检查现状 确认当前无Swap,并确保有足够磁盘空间。 swapon --show df -h /

  2. 创建Swap文件 创建大小为2GB的Swap文件。 fallocate -l 2G /swap

  3. 有些文件系统不支持fallocate指令使用以下命令 2048为2g dd if=/dev/zero of=/swap bs=1M count=2048 status=progress

  4. 设置权限 锁定文件权限,防止被随意读取。 chmod 600 /swap

  5. 格式化 将文件标记为交换空间格式。 mkswap /swap

  6. 立即启用 激活Swap。 swapon /swap

  7. 验证启用 检查Swap是否成功启用并查看大小。 swapon --show free -h

  8. 永久生效 配置系统启动时自动挂载此Swap。 echo '/swap none swap sw 0 0' | sudo tee -a /etc/fstab

  9. 设置交换积极性 echo 'vm.swappiness=60' | sudo tee -a /etc/sysctl.conf

  10. 确定交互积极性 sysctl -p

移除Swap文件步骤

  1. 查看当前状态 确认正在使用的Swap文件路径。 sudo swapon --show

  2. 停用Swap 关闭所有Swap交换空间。 sudo swapoff /swap

  3. 验证已停用 确认Swap已完全关闭。 sudo swapon --show free -h

  4. 删除自动挂载 从系统配置中移除Swap,使其开机不启动。 打开文件 vim /etc/fstab
    删除这行 /swap none swap sw 0 0

  5. 删除Swap文件 永久删除磁盘上的文件以释放空间。 rm /swap

  6. (可选)清理内核参数 移除之前调整的swappiness优化设置。 打开文件 vim /etc/sysctl.conf 修改vm.swappiness=0

  7. 重启生效 sysctl -p

服务端

  • 安装
apt-get install wireguard
  • 开启ipv4流量转发

    /etc/sysctl.conf

net.ipv4.ip_forward = 1 #设置为1
  • 进入到wireguard目录

    /etc/wireguard

chmod -R 777 /etc/wireguard
  • 生成服务端密钥对(私钥和公钥)
wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey
  • 创建 /etc/wireguard/wg0.conf
touch /etc/wireguard/wg0.conf
[Interface]
# 服务器的虚拟IP地址(选择一个不冲突的内网段)
Address = 10.1.1.1/24
# 监听端口(默认51820)
ListenPort = 51820
# 服务器的私钥(填入刚才生成的私钥内容)
PrivateKey = <服务器的私钥>

# 开启IP转发(为了能让客户端之间互相通信)  eth0记得换
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# --- 下面开始添加客户端(对等点) ---

# 客户端1:你的电脑
[Peer]
# 客户端1的公钥
PublicKey = <客户端1的公钥>
# 允许客户端1使用的IP地址,相当于给它分配了固定IP 10.1.1.2
AllowedIPs = 10.1.1.2/32

# 客户端2:你的另一台设备
[Peer]
PublicKey = <客户端2的公钥>
AllowedIPs = 10.1.1.3/32
  • 启动服务端
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0

客户端

Windows

  • 下载安装
https://www.wireguard.com/install/

界面操作->在隧道列表 新增隧道

写入以下

名称

[Interface]
PrivateKey = SHd3t5Tz038WY8yQWjZxisL4LzNRQRxvPlcRC56B/Gc=
Address = 10.1.1.2/24

[Peer]
PublicKey = /mw8vj3YqmzNxcKlKFdaIw9b/uB5B9+Zpy+IRE6umF4=
AllowedIPs = 10.1.1.0/24
Endpoint = 120.25.167.228:51820
PersistentKeepalive = 25

[Interface]

PrivateKey:这是新建时自动生成的。

Address: 这个是配置当前客户端的ip地址

DNS: dns服务器地址

[Peer]

PublicKey:服务端公钥,在服务端/etc/wireguard/publickey 文件下

AllowedIPs:允许的IP,也就是说访问什么ip时走wireguard。0.0.0.0/0 代表所有流量都走VPN(全隧道)

Endpoint:服务器ip和端口

PersistentKeepalive:保持连接活跃(对于NAT很重要)

Linux

  • 安装
apt-get install wireguard
  • 配置
cd /etc/wireguard
# 生成私钥并设置权限
wg genkey | tee privatekey && chmod 600 privatekey
# 根据私钥生成公钥
wg pubkey < privatekey > publickey
  • 配置到服务端,服务端配置文件/etc/wireguard/wg0.conf添加下面内容
  • 重启服务端
[Peer]
PublicKey = mOeMOFPG7wsa/KkTuXlZkQnlFYZFwcur8xVCNVusGFw=  #客户端公钥
AllowedIPs = 10.1.1.5/32
  • 客户端新增/etc/wireguard/wg0.conf文件
touch /etc/wireguard/wg0.conf
[Interface]
# 客户端虚拟 IP(必须与服务端分配的 IP 一致,注意 /32 表示单个 IP)
Address = 10.1.1.5/32
# 客户端私钥(从刚才生成的 privatekey 文件中复制)
PrivateKey = mLs1eUCJXhb9J+aiMm50PfmD21F2RYiMotIkZt3fHkE=
# DNS 服务器(可选,例如使用公共 DNS 或服务端内网 DNS)
#DNS = 8.8.8.8

[Peer]
# 服务端的公钥(从服务端获取)
PublicKey = /mw8vj3YqmzNxcKlKFdaIw9b/uB5B9+Zpy+IRE6umF4=
# 服务端的公网 IP 和端口
Endpoint = 120.25.167.228:51820
# 允许的 IP:这里设置 10.1.1.0/24 表示只有访问 VPN 网段的流量才走隧道
# 如果想所有流量都走 VPN,可以改为 0.0.0.0/0
AllowedIPs = 10.1.1.0/24
# 保持连接的存活时间(对于 NAT 后面的客户端很重要)
PersistentKeepalive = 25
  • 重启服务
systemctl restart wg-quick@wg0
或者
wg-quick up wg0
  • 启动连接测试**(记得重启或重新加载服务端文件**)
ping 10.1.1.1 或者其他
  • 设置开机自启
systemctl enable wg-quick@wg0

Android

下载

https://www.wireguard.com/install/

界面配置

本地
 - 名称:
 - 私钥:自己生成
 - 公钥:自己生成,要复制到服务端
 - 局域网ip:自己的ip 注意x.x.x.x/24  这个24
 - dns服务器:可以不填
 
添加节点
远程
 - 公钥:服务器公钥
 - 保活间隔:25s一次心跳
 - 对端:Endpoint就这个配置,120.25.167.228:51820
 - 路由的ip地址段:10.1.1.0/24 注意x.x.x.x/24  这个24

moon 服务器配置

  • 进入 ZeroTier 配置文件目录。

    cd /var/lib/zerotier-one
    
  • 生成 moon.json 签名文件。

    sudo -s zerotier-idtool initmoon identity.public >>moon.json
    
  • 编辑

    vim moon.json
    

    可以看到如下信息:

    moon.json

    
      {
       "id": "0123456789",
       "objtype": "world",
       "roots": [
        {
         "identity": "xxxxxxxx:0:xxxxxxxx",
         "stableEndpoints": []
        }
       ],
       "signingKey": "xxxxxxxx",
       "signingKey_SECRET": "xxxxxxxx",
       "updatesMustBeSignedBy": "xxxxxxxx",
       "worldType": "moon"
      }
        
    

    注意:记录下 moon.json 文件中的 id 。

  • 修改信息。

    找到 "stableEndpoints": []

    添加 "IPv4地址/9993" 或者 "IPv4地址/9993","IPv6地址/9993"

    示例:

    "stableEndpoints": ["1.2.3.4/9993","2001:abcd:abcd::1/9993" ]
    
  • 生成 .moon 签名文件

    zerotier-idtool genmoon moon.json
    

    显示下面内容成功:

    wrote 0000006xxxxxxxxx.moon (signed world with timestamp 15xxxxxxxxxx7)
    
  • 创建 moon 结点文件夹。

    mkdir /var/lib/zerotier-one/moons.d
    
  • 将签名文件复制到 moons.d 文件夹中

    cp 0000006xxxxxxxxx.moon moons.d/
    
  • 重启 ZeroTier 服务

    /etc/init.d/zerotier-one restart
    或者
    systemctl restart zerotier-one.service
    

设备连入 moon 服务器

自动配置

  • 输入命令:

    sudo zerotier-cli orbit [moon.json 文件中的 id] [moon.json 文件中的 id]
    
    示例
    zerotier-cli orbit f63861d692 f63861d692  //这里的f63861d692是前面moon.json中的id
    200 orbit OK
    
  • Windows 有一点不同,需要使用管理员权限的 PowerShell 输入:

    zerotier-cli orbit [moon.json 文件中的 id] [moon.json 文件中的 id]
    

手动配置

各个系统平台下 ZeroTier 目录位置 :

Windows: C:\ProgramData\ZeroTier\One
Macintosh: /Library/Application Support/ZeroTier/One (在 Terminal 中应为 /Library/Application\ Support/ZeroTier/One)
 Linux: /var/lib/zerotier-one
 FreeBSD/OpenBSD: /var/db/zerotier-one
  • 在对应系统平台下的 ZeroTier 目录位置,创建 moons.d 文件夹。

    sudo mkdir /var/lib/zerotier-one/moons.d
    
  • 将 moon 服务器生成的 0000006xxxxxxxxx.moon 拷贝到 moons.d 文件夹下。

  • 重启 zerotier-one 服务。

    • Linux :/etc/init.d/zerotier-one restart
    • Windows :
      • 按下 windows键+r ,打开 “运行” 窗口。
      • 输入 services.msc 回车。
      • 找到 ZeroTier One 服务,右键选择 “重新启动” 。

检测生效

  • 在非 moon 的客户端,输入命令:

    zerotier-cli listpeers
    
  • Windows 有一点不同,需要使用管理员权限的 PowerShell 输入:

    zerotier-cli listpeers
    

如果出现如下情况:

  • moon 服务器的 ID 、IP 地址出现在列表中,证明联通 moon 服务器。
200 listpeers <ztaddr> <path> <latency> <version> <role>
...................
200 listpeers 6xxxxxxxxx [moon IPv4地址]/60723;11450;11405 -1 1.4.6 MOON
...................

//这个不对,当其他的节点进行互相访问时,会尝试使用f63861d692 120.25.167.228/9993 ,成功后LEAF变成MOON
200 listpeers f63861d692 120.25.167.228/9993;16760;17744 -984 1.14.2 LEAF 
//完成后的
200 listpeers f63861d692 120.25.167.228/9993;4101;4101 15 1.14.2 MOON

📒 笔记文档使用指南

这是什么

这是一个基于 mdBook 构建的静态笔记站点。所有笔记以 Markdown 格式保存在 Git 仓库中,通过 mdBook 生成漂亮的静态 HTML 页面。

🤖 给 AI 机器人(QQ 机器人)的操作说明

一键更新笔记

/usr/share/nginx/html/blog/update.sh

这个脚本会自动执行:

  1. git pull — 从 Gitee 拉取最新笔记
  2. 编译 s.c — 生成侧边栏文件 SUMMARY.md
  3. mdbook build — 构建静态站点

服务器文件结构

/usr/share/nginx/html/
├── blog/                  ← mdBook 项目根目录
│   ├── book/              ← 构建输出 HTML(nginx 指向这里)
│   │   ├── index.html     ← 首页
│   │   ├── notes/         ← 生成的笔记页面
│   │   ├── css/           ← 样式文件
│   │   ├── FontAwesome/   ← 图标字体(本地化)
│   │   └── searchindex.js ← 全文搜索索引
│   ├── src/
│   │   ├── notes/         ← Markdown 笔记源文件(Gitee 仓库)
│   │   └── SUMMARY.md     ← 侧边栏目录(由 s.c 自动生成)
│   ├── theme/
│   │   ├── custom.css     ← 自定义样式
│   │   └── custom.js      ← 侧边栏折叠功能
│   ├── s.c                ← 侧边栏生成器源代码(C语言)
│   ├── s                  ← 编译后的二进制
│   ├── book.toml          ← mdBook 配置文件
│   └── update.sh          ← 一键更新脚本
└── blog.backup/           ← 备份

Nginx 配置

  • 根目录: /usr/share/nginx/html/blog/book
  • 域名: www.openso.top
  • 反向代理路径: /sc, /test/, /shuiChan, /mqtt

📝 给人类用户的说明

浏览笔记

  • 侧边栏: 左侧显示目录结构,点击 📁 文件夹标题展开/收起,点击 📄 文件标题查看内容
  • 搜索: 点击顶部搜索图标 🔍 或按 s 键,可全文搜索
  • 主题切换: 点击顶部 🖌️ 图标,可选择 Light / Rust / Coal / Navy / Ayu 五种风格
  • 侧边栏开关: 点击 ☰ 图标可收起/展开侧边栏

笔记格式

所有笔记使用 Markdown 编写,支持:

  • 标题(# ~ ######
  • 代码块(```
  • 表格、列表、引用
  • 图片(![]()
  • 链接([]()

关于 mdBook

mdBook 是一个由 Rust 编写的静态站点生成器,专门用于创建文档/笔记网站。

特点:

  • 纯静态 HTML,无需后端服务
  • 内置全文搜索
  • 支持多主题切换
  • 响应式设计,支持手机端
  • 侧边栏自动从目录结构生成

配置文件: blog/book.toml

[book]
title = "📒 笔记文档"
src = "src"

[output.html]
default-theme = "ayu"
print-enable = false
prev-next-buttons = false

侧边栏生成器(s.c)

s.c 是一个用 C 语言编写的工具,用于从笔记目录结构自动生成 SUMMARY.md(mdBook 使用的侧边栏定义文件)。

功能:

  • 深度优先遍历目录,正确维护父子层级
  • 目录优先于文件排序,各自按字母序
  • 自动生成 README.md 作为目录的入口页
  • URL 中的空格自动编码为 %20
  • 无 .md 文件的目录自动跳过(不会出现在侧边栏)

最后更新: 2026-05-27

开闭原则

对扩展开放,对修改关闭。 当需求发生变化时,应尽量不修改原有代码,而是通过新增功能来满足需求。 实现方式:对业务进行抽象。例如支付功能,初期可能只支持微信支付,后续可能扩展其他支付方式,此时应将支付行为抽象为接口或抽象类,以支持灵活扩展。

里氏替换原则

用于指导何时使用继承。 子类可以扩展父类的功能,但不能改变父类原有的行为。任何父类出现的地方,都可以用子类替换,且程序行为不会发生错误。

依赖倒置原则

高层模块不应依赖低层模块,两者都应依赖其抽象。 例如在常见的分层架构中,Controller 层引入的是 Service 接口,而非具体的实现类。

单一职责原则

一个类只应承担一个职责。 例如员工类可以有打卡方法,但不应包含计算工资的方法,后者应交由专门的薪资类处理。

接口隔离原则

接口应尽量做到“最小化”,即一个接口中只包含与其职责相关的抽象方法。 如果某个接口中存在与自身无关的方法,则应将这部分方法拆分到另一个接口中。

迪米特法则(最少知识原则)

一个对象应尽可能少地与其他对象发生交互,只与直接的朋友通信。 例如明星与经纪人之间的关系,外界只需与经纪人沟通,无需直接接触明星,从而降低耦合。

合成复用原则

优先使用组合(合成)或聚合,而不是继承来实现复用。 例如人有姓名、年龄等属性,同时拥有汽车。不应将汽车属性直接写入人类,而应将汽车独立为一个类,在人类中通过组合的方式引用汽车对象,使结构更清晰、复用性更强。