408
计算机网络
TCP/IP模型是基于广泛应用的互联网协议族(TCP/IP协议栈)而制定的,是现代互联网的事实标准。它更加精简,通常被划分为5层(为便于教学,将链路层和物理层分开)或4层。
5层模型(最常用于教学和描述)
- 物理层:与OSI物理层完全相同。
- 数据链路层:与OSI数据链路层基本相同。
- 网络层:对应OSI的网络层。核心协议是IP。
- 传输层:对应OSI的传输层。核心协议是TCP和UDP。
- 应用层:融合了OSI的应用层、表示层和会话层的功能。所有面向用户的应用程序和协议都在这一层。
4层模型(TCP/IP原模型)
- 网络接口层: 对应OSI的物理层+数据链路层。
- 网络层: 对应OSI的网络层。
- 传输层: 对应OSI的传输层。
- 应用层: 对应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笔记.md
- [gcc g++ make cmake关系.md](gcc g++ make cmake关系)
- gcc预处理编译汇编链接.md
- makefile.md
- 嵌入式链接地址概念作用过程.md
- 编译原理.md
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:3306Dockerfile
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"]
构建最简单命令
- 编译hello.c文件,生成hello执行文件
- 新建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
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 *容器不创建网卡,性能更好延迟更低
直接把容器当作一个进程了。会占用端口
-
none
没有网络,也就是说,这个容器没法与外界进行交流,而且也不会有网卡。
-
container
和已经存在的容器共享一个网卡。不会创建网卡和ip。如果已经存在的容器可以和宿主机进行网络数据交换,那么新建的容器也能和宿主机进行网络数据交换。
- 自定义桥接
这种可以指定桥接方式
Embedded
- 3.5tft-原生代码实现.md
- embedded-linux-imx6ull
- IIC.md
- Linux-Framebuffer编程.md
- Linux-input-event.md
- linux系统常用头文件.md
- linux驱动
- lvgl
- mcu
- mmap.md
- rk-linux
- rootfs
- SPI.md
- stc8g1k08a芯片io扩展
- Timer-stm32f103.md
- 扇区-块-页.md
- 文件系统.md
- 模块
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的目录
- cmake.sourceDirectory:cmake那个
-
移除不需要的文件/文件夹
- 删除所有目录下的CMakeFile.txt
- 删除其他无用文件。如图:
-
测试
-
编写测试程序
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配置文件
-
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 #查看驱动卸载情况
-
查看挂载情况
lsmodcat /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 0chmod 666 /dev/004_register_cdevls /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下的设备节点
流程
-
编译和挂载
makeinsmod 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 0cat /proc/devices # 命令 Character devices: 237 005_udev_mdevls /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.c和driver.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-leds | LED 亮灭 | /sys/class/leds/ |
gpio-beeper | 蜂鸣器 | /sys/class/input/inputX/ |
regulator-fixed | 电源开关 | regulator 框架 |
gpio-poweroff | 关机断电 | 内核 poweroff 流程自动触发 |
gpio-restart | 重启 | 内核 restart 流程自动触发 |
gpio-mux | GPIO 选通 | 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 = 4DRM 面板驱动把背光禁用了
-
在设备树的树根/下配置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内部处理模块。功能如下:
- 图层合成 — 支持多个显示图层(video layer、UI layer 等),将它们叠加合成输出
- 格式转换 — 将帧缓冲区的数据转换成显示接口所需的格式
- 输出路由 — 把信号路由到不同的显示接口(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测试
-
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 -
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显示,比如翻转,改变分辨率之类操作。
流程
- SPI初始化
- 开启背光
- 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_1v8 和 vcc_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文件,用来操作硬件设备需要以下步骤。
- 创建xxx_device.c和xxx_device.h来描述设备。比如某个寄存器。
- 创建驱动xxx_driver.c和xxx_driver.h操作描述的设备。比如寄存器写入和读取。
- 内核编译和引入。(需补充详细过程)
有设备树之后
要想建立一个/dev/xxx文件,用来操作硬件设备需要以下步骤。
- 创建设备树,用来描述设备,代替前面的第一步。
- 创建驱动xxx_driver.c和xxx_driver.h操作描述的设备。比如寄存器写入和读取。更多时候编译成模块。
- 内核编译和引入。(需补充详细过程)
对于自己编写的驱动,设备树要描述这个驱动,设备树和驱动代码必须做关联联动。
而对于常见的驱动,比如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
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插入,调用用户空间程序,用户空间程序自动挂载等
机制
有devfs,udev,mdev,devfs基本不用了。mdev是udev简化版本。
udev是基于netlink机制实现的。通过监听内核发送的uevent来执行相应的热插拔操作。mdev是基于uevent_helper机制,内核产生的uevent会调用uevent_helper所指向的用户程序,mdev执行热插拔动作。
| 特性 | Netlink (NETLINK_KOBJECT_UEVENT) | uevent_helper |
|---|---|---|
| 通信方式 | 基于套接字(Socket)的异步消息通信 | 内核直接执行用户空间程序 |
| 实现方式 | 内核广播消息,用户态进程监听接收 | 内核为每个事件 fork() 一个新进程执行助手程序 |
| 效率 | 高(消息传递,无进程创建开销) | 极低(每次事件都创建进程,开销巨大) |
| 扩展性 | 好,支持多播,可供多个程序同时监听 | 差,一次只能指定一个助手程序 |
| 当前状态 | 主流、推荐 | 已废弃,现代系统默认不启用 |
| 典型使用者 | udev、systemd-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- [linux目录下xxx.c + Kconfig + Makefile + DTS](#linux目录下xxx.c + Kconfig + Makefile + DTS)
- 内核直接加载模块
- uboot中初始化
.ko文件insmod
- 直接在运行中的系统执行insmod安装模块驱动
- 放在文件系统,开机自启脚本执行insmod
- 标准位置自动加载:/lib/modules/$(uname -r)/
- 比如001-字符设备驱动 以及 002-平台总线驱动 下的驱动
linux目录下xxx.c + Kconfig + Makefile + DTS
内核直接加载模块
- 编译一堆自己的.ko文件,打包进文件系统里面。
- 由系统启动脚本insmod
- 匹配设备树/总线设备!
- 匹配成功才会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 |
| `dmesg | grep -i of | dt |
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, ®_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
-
不同功能通道
video0:主视频流(如MJPG、YUYV格式的实时预览)video1:可能是元数据通道(如时间戳、IMU数据)、静态抓拍通道,或者同一个物理摄像头的另一个实例(例如支持同时输出高低两种分辨率)。
-
UVC扩展单元 某些UVC摄像头内部有多个逻辑子设备(如视频处理单元、静态图像捕获单元),内核驱动可能为每个子设备单独创建一个
/dev/video节点。 -
/dev/media0
Media Controller设备节点,用于管理摄像头内部的复杂管线。
- 作用:当V4L2设备内部有多个模块(如传感器、ISP、缩放器)时,
/dev/media0允许用户空间程序查看和配置这些模块之间的连接关系(称为“pipeline”)。 - 典型场景:UVC摄像头如果实现了媒体控制器API,你会看到这个节点。它本身不直接传输视频,而是通过
media-ctl工具来配置内部路由,然后再通过/dev/video0采集数据。
- 作用:当V4L2设备内部有多个模块(如传感器、ISP、缩放器)时,
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格式的区别如下:
| 格式名称 | 采样方式 | 存储类型 | 特点 |
|---|---|---|---|
| YUYV | 4:2:2 | 打包 (Packed) | 每两个像素共享一对UV,顺序交错排列。 |
| UYVY | 4:2:2 | 打包 (Packed) | 与YUYV类似,但顺序变为U、Y、V、Y。 |
| NV12 | 4:2:0 | 半平面 (Semi-Planar) | 一个Y平面,一个UV交错存储的平面,在Windows、Android和硬件编解码中很常见。 |
| I420 | 4:2:0 | 平面 (Planar) | 三个平面顺序存储:全部Y,然后全部U,最后全部V。在FFmpeg等软件中广泛使用。 |
| YV12 | 4: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 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 数据内容 | Y00 | U00 | Y01 | V00 | Y02 | U01 | Y03 | V01 |
| 所属像素 | 像素(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:2 | YUV 4:2:0 | N/A (每个像素包含完整R,G,B) | 基于视频编码标准 |
| 存储类型 | 打包 (Packed) | 半平面 (Semi-Planar) | 打包 (Packed) | 码流格式 (Bitstream) |
| 数据排列 | 交错排列:Y U Y V | 先存全部Y,再存交错UV | 顺序排列:R G B 或 B G R | 包含NAL单元等复杂结构 |
| 每像素占用 | 16 bits = 2 bytes | 12 bits = 1.5 bytes | 24 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 |
| BOOLEAN | 0/1 | 自动白平衡开关 |
| MENU | 索引 → 字符串 | 电源频率 0=50Hz 1=60Hz |
| INTEGER_MENU | 索引 → 整数 | 某驱动自定义模式 ID |
| BUTTON | 写入即触发一个动作 | 执行一次白平衡 |
| INTEGER64 | 64位整数 | 大范围计数器 |
| 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 | 改变此控件会影响图像布局(比如改变裁剪窗口) |
枚举设备支持的控件
流程
- 配置
- 获取一帧
/* 初始化 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/fb0800x600 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 级别控制- 通过
pad和pad-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) 检查条件

#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,主循环退出。
SIGUSR1和SIGUSR2这是两个预留的用户自定义信号,编号通常为 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)
范围从
SIGRTMIN到SIGRTMAX(通常是 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文件
-
路径:
/kernel/drivers/gpu/drm/panel
配置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 的值 | 必须 join 或 detach |
pthread_exit() | 线程自己 | 主动退出,可指定返回值 | 必须 join 或 detach |
pthread_cancel() | 其他线程 | 请求取消(异步/延迟) | 必须 join 或 detach |
⚠️ 无论哪种方式,线程结束后其资源(栈、TLS、内核结构)不会自动完全释放,除非被
join或detach。
| 场景 | 推荐做法 |
|---|---|
| 需要线程结果 | pthread_join() |
| 不需要结果, fire-and-forget | pthread_detach()(创建后立即 detach) |
| 需要中途终止线程 | pthread_cancel() + pthread_join(),但尽量用条件变量/标志位优雅退出 |
| 避免僵尸线程 | 确保每个线程都被 join 或 detach |
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)
- D-PHY:2 线差分(P/N),最高~2.5Gbps/lane,低成本,绝大多数嵌入式摄像头(GC2145/OV2640/IMX 系列)都用,RV1106 全是 DPHY。
- 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. 摄像头硬件端(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交互。poll或epoll用于等待帧就绪事件,避免CPU空转。 - 上层应用框架:实际开发中很少直接写ioctl,而是调用
V4L2 Userpace API封装库,或者使用 GStreamer 的v4l2src插件、FFmpeg 的avdevice模块。在Android系统中则对接Camera HAL。 - 显示/编码/推流:拿到内存中的图像数据后,应用将其送去显示(DRM)、硬件编码(如H.264/H.265)或通过网络协议(RTSP/WebRTC)推流。

第二部分:图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 rate 和 hsync 成功信息 | 驱动报错 Timeout waiting for PLL lock,无图像 |
| 协议解包成功 | CSI-2 Host | /proc/interrupts 中 cif 中断计数增加 | 硬件中断不增加,说明MIPI线序或电压不匹配 |
| DMA搬运正确 | VICAP -> DDR | cat /proc/kmsg 无 DMA FIFO overflow 错误 | 画面出现横向撕裂或绿色噪点 |
| 缓冲区队列流转 | videobuf2 -> /dev/videoX | 应用层poll返回POLLIN,DQBUFF不阻塞 | DQBUFF一直阻塞,检查缓冲区是否全部出队(STREAMON未调用) |
| 图像画面显示 | 用户层 GStreamer | gst-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
mmap 是 memory 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
- 000_lvgl移植-linux-mcu.md
- 001_lvgl-介绍.md
- 002_lvgl_conf配置文件.md
- 099_lvgl初始化基于esp8266.md
- 099_lvgl移植linux-通用.md
- 099_lvgl移植到imx6ull.md
- 099_lvgl移植到stm32f103c8t6.md
- images
- LVGL_GENERAL_PORTING_GUIDE.md
- lvgl控件学习
- lvgl移植到imx6ull源码
- lvgl调用摄像头.md
- vscode模拟lvgl环境
Lvgl控件学习
- 001_概念_display注册.md
- 002_概念_screen.md
- 003_概念_widgets.md
- 004_widgets_position.md
- 005_widgets_size.md
- 006_widgets_style.md
- 007_widgets_parts_states.md
- 008_widgets_flags.md
- 090_widgets.md
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对象,插入链表后统一管理。
- 分配并初始化
lv_disp_t对象- 存储显示驱动参数(分辨率、flush 回调、绘图缓冲区等)。
- 将该对象挂入全局显示链表,若为第一个显示设备则设为默认显示。
- 创建两个屏幕对象(
lv_obj_t)- 活动屏幕(
act_scr):当前可见的主屏幕。 - 上一个屏幕(
prev_scr):用于屏幕切换动画的备用屏幕(初始与act_scr相同或为NULL)。
- 活动屏幕(
- 配置屏幕对象的基本属性
- 设置屏幕尺寸为显示设备的
hor_res × ver_res。 - 清除可滚动标志(屏幕本身不可滚动)。
- 标记对象类型为
LV_OBJ_CLASS_SCREEN,使其成为所有其他控件的根容器。 - 将屏幕的
parent设为NULL(顶级对象)。
- 设置屏幕尺寸为显示设备的
- 将屏幕与显示设备关联
disp->act_scr = new_scr,disp->prev_scr = NULL(或new_scr)。- 设置屏幕的
disp指针指向当前显示设备。
- 触发默认主题/样式的加载
- 如果 LVGL 启用了主题功能,会自动为屏幕应用默认主题(背景色、默认样式等)。
- 主题可能进一步初始化默认字体、颜色方案等。
- 强制刷新整个屏幕(首次重绘)
- 调用
lv_obj_invalidate(act_scr)标记整个屏幕为“脏”。 - 随后 LVGL 的任务调度器会触发一次全屏重绘:在绘图缓冲区中绘制屏幕背景,并通过
flush_cb发送到物理显示屏。
- 调用
- 使
lv_scr_act()可用lv_scr_act()定义为lv_disp_get_scr_act(lv_disp_get_default()),此时默认显示设备已存在,act_scr已有效,因此可以返回屏幕根控件指针。
Screen
display对象创建之后会创建4个screen。
- 底层(位于活动屏幕下方,透明、不可滚动,但可以点击)
- Active Screen
- 顶层(位于活动屏幕上方,透明且不可滚动或点击)
- 系统层(位于顶层上方,透明且不可滚动或点击)
- 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: 在对齐基础上的额外像素偏移。
- align: 对齐方式枚举(如
-
-
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_MAIN、LV_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_...) 来启用或禁用。
LV_OBJ_FLAG_HIDDEN使 Widget 隐藏。(就像它完全不存在一样)LV_OBJ_FLAG_CLICKABLE使 Widget 可以通过输入设备点击LV_OBJ_FLAG_CLICK_FOCUSABLE点击时为 Widget 添加聚焦状态LV_OBJ_FLAG_CHECKABLE当 Widget 被点击时切换选中状态LV_OBJ_FLAG_SCROLLABLE使 Widget 可滚动LV_OBJ_FLAG_SCROLL_ELASTIC允许内部滚动但速度较慢LV_OBJ_FLAG_SCROLL_MOMENTUM当“抛出”Widget 时使其滚动更远LV_OBJ_FLAG_SCROLL_ONE仅允许滚动一个可停靠的子元素LV_OBJ_FLAG_SCROLL_CHAIN_HOR允许将水平滚动传播给父级LV_OBJ_FLAG_SCROLL_CHAIN_VER允许将垂直滚动传播给父级LV_OBJ_FLAG_SCROLL_CHAIN简单包装 (LV_OBJ_FLAG_SCROLL_CHAIN_HOR | LV_OBJ_FLAG_SCROLL_CHAIN_VER)LV_OBJ_FLAG_SCROLL_ON_FOCUS聚焦时自动滚动 Widget 以使其可见LV_OBJ_FLAG_SCROLL_WITH_ARROW允许使用箭头键滚动聚焦的 WidgetLV_OBJ_FLAG_SNAPPABLE如果父级启用了滚动停靠,则可以停靠到此 WidgetLV_OBJ_FLAG_PRESS_LOCK即使按压滑出 Widget 也保持按压状态LV_OBJ_FLAG_EVENT_BUBBLE向父级传播事件LV_OBJ_FLAG_GESTURE_BUBBLE向父级传播手势LV_OBJ_FLAG_ADV_HITTEST允许执行更准确的点击测试。例如,考虑圆角LV_OBJ_FLAG_IGNORE_LAYOUT使 Widget 不受布局定位影响LV_OBJ_FLAG_FLOATING当父级滚动时不滚动该 Widget,并忽略布局LV_OBJ_FLAG_SEND_DRAW_TASK_EVENTS启用发送LV_EVENT_DRAW_TASK_ADDED事件LV_OBJ_FLAG_OVERFLOW_VISIBLE不裁剪子元素的内容至父级边界LV_OBJ_FLAG_FLEX_IN_NEW_TRACK在此项目上开始一个新的 flex 轨道LV_OBJ_FLAG_LAYOUT_1自定义标志,由布局自由使用LV_OBJ_FLAG_LAYOUT_2自定义标志,由布局自由使用LV_OBJ_FLAG_WIDGET_1自定义标志,由 widget 自由使用LV_OBJ_FLAG_WIDGET_2自定义标志,由 widget 自由使用LV_OBJ_FLAG_USER_1自定义标志,由用户自由使用LV_OBJ_FLAG_USER_2自定义标志,由用户自由使用LV_OBJ_FLAG_USER_3自定义标志,由用户自由使用LV_OBJ_FLAG_USER_4自定义标志,由用户自由使用
一些例子:
/* 隐藏 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());
-
大小
//设置宽度为100 lv_obj_set_width(w1,100); //设置高度为50 lv_obj_set_height(w1,50); //同时设置宽度和高度 100x50 lv_obj_set_size(w1,100,50); //如果部件没有设置,会有默认的宽高。 -
位置
//坐标系跟电脑屏幕的是一样的,左上角为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 -
对齐*
//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) -
样式
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"
}
-
下载SDL(https://github.com/libsdl-org/SDL/releases/)
-
将SDL中的cmake和x86_64-w64mingw32复制到MinGW安装目录,如果是i686-w64-mingw32要对应

-
vscode打开示例工程(lvgl_template_v8.3_vscode.7z)。编译运行。

-
VSCode配置MinGW

-
构建运行

如果运行后没有反应,将SDL2-2.30.1\x86_64-w64-mingw32\bin\SDL2.dll复制到示例中的bin文件夹(编译后生成)
红外项目
项目主要是通过
回调函数
多线程
在lvgl主线程下开启一个btns线程去poll()循环回调btn_event_cb()。
有事件告诉lvgl去消费按钮输入。
事件驱动
非阻塞
只需要在lvgl的循环中读去一个函数btns_read的返回值,然后判断返回值就能知道是否有输入
移植通用流程
- 下载lvgl源码
https://github.com/lvgl/lvgl/
- 删除没有用的文件和文件夹

-
将lv_conf_template.h复制并改名为lv_conf.h
-
修改lv_conf.h中的#if 0为#if 1
-
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]; // 第二缓冲区(双缓冲) lv_disp_draw_buf_init(&draw_buf, buf1, buf2, LV_BUF_SIZE);- 给lvgl提供心跳时钟
lv_tick_inc(1); // 可以在定时中断或者linux定时函数中调用- 注册显示驱动,为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);- 提供绘图函数给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); }- 输入设备(伪代码)
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);- 在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 开发界面的大致步骤:
- 初始化:调用
lv_init()并初始化显示驱动和输入设备驱动(如触摸屏)。 - 创建对象:在屏幕上创建各种控件(如
lv_btn_create创建按钮,lv_label_create创建文字标签)。 - 设置属性:调整控件的位置、大小、颜色、文字内容、样式等。
- 添加事件:为控件绑定回调函数,例如“点击按钮时 LED 闪烁”。
- 循环运行:在主循环中周期性调用
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 |
示例代码
#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);
}
- 接收LVGL传递的矩形区域(
area)和像素数据(color_p) - 通过
draw16bitRGBBitmap将RGB565数据写入显存 - 必须调用
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
流程
- 下载lvgl源码
https://github.com/lvgl/lvgl/
- 删除没有用的文件和文件夹

-
将lv_conf_template.h复制并改名为lv_conf.h
-
修改lv_conf.h:
- lv_conf.h中的**#if 0**为#if 1,
- 修改lv_conf.h中LV_COLOR_DEPTH 色深
- 修改lv_conf.h中LV_MEM_CUSTOM开启自动分配内存
-
在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 构建文件

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. 常见问题排查
- 编译报错
error: ‘for’ loop initial declarations...:- 确保 Makefile 中
CFLAGS包含-std=gnu99。
- 确保 Makefile 中
- 头文件找不到
lvgl/lvgl.h:- 由于项目瘦身,直接引用
#include "lvgl.h"即可。
- 由于项目瘦身,直接引用
- 运行显示
cannot open framebuffer device:- 检查设备节点是否存在,或使用
sudo运行程序。
- 检查设备节点是否存在,或使用
- 触摸无响应:
- 运行
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

裁剪


STM32Cube IDE配置
项目配置






生成的目录结构

配置LVGL目录



移植屏幕厂商驱动测试
-
移植屏幕驱动最重要的一点是:给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)做如下修改:
-
#if 0改成#if 1:这个是开启lvgl,如果为0为不使用lvgl,为0整个.h文件失效
-
屏幕大小:LV_HOR_RES_MAX,LV_VER_RES_MAX设置成自己屏幕大小
-
颜色翻转:如果是SPI屏要设置LV_COLOR_16_SWAP为1
-
屏幕旋转(0,90°,180°,270°)缓冲区:LV_DISP_ROT_MAX_BUF调小一点,10u变成1u,测试没有用到屏幕旋转功能,LV_DISP_ROT_MAX_BUF很小都没事。
-
LV_MEM_SIZE:内存池,32 * 1024(32KB)改成2 * 1024(2KB),RAM占用2KB,C8T6只有20KB,所以我们这里调小一点。这里就不得不说LVGL内存的两种分配方式了:
- 手动分配LV_MEM_CUSTOM为0时,一个大小固定的数组(2KB)
- 自动分配LV_MEM_CUSTOM为1时,由malloc分配,单片机不怎么使用。移植LVGL到linux这种带操作系统才会用。
-
功能裁剪:
| 功能模块 | 修改后 | 说明 |
|---|---|---|
LV_USE_ANIMATION | 0 | 禁用动画 |
LV_USE_SHADOW | 0 | 禁用阴影绘制 |
LV_USE_PATTERN | 0 | 禁用矩形图案填充 |
LV_USE_VALUE_STR | 0 | 禁用矩形上的数值字符串绘制 |
LV_USE_BLEND_MODES | 0 | 禁用混合模式(仅正常混合) |
LV_USE_OPA_SCALE | 0 | 禁用整体透明度缩放 |
LV_USE_IMG_TRANSFORM | 0 | 禁用图像旋转和缩放 |
LV_USE_API_EXTENSION_V6 | 0 | 禁用 v6 API 兼容 |
LV_USE_API_EXTENSION_V7 | 0 | 禁用 v7 API 兼容 |
LV_USE_DEBUG | 0 | 完全禁用调试断言 |
-
对象裁剪:
-
保留对象:
LV_USE_BTN→ 1LV_USE_CONT→ 1LV_USE_LABEL→ 1
-
裁剪掉的对象:
对象 修改后 控件名称 LV_USE_ARC0 Arc(圆弧) LV_USE_BAR0 Bar(进度条) LV_USE_BTNMATRIX0 Button Matrix(按钮矩阵) LV_USE_CALENDAR0 Calendar(日历) LV_USE_CANVAS0 Canvas(画布) LV_USE_CHECKBOX0 Checkbox(复选框) LV_USE_CHART0 Chart(图表) LV_USE_CPICKER0 Color Picker(颜色选择器) LV_USE_DROPDOWN0 Dropdown List(下拉列表) LV_USE_GAUGE0 Gauge(仪表盘) LV_USE_IMG0 Image(图像) LV_USE_IMGBTN0 Image Button(图像按钮) LV_USE_KEYBOARD0 Keyboard(虚拟键盘) LV_USE_LED0 LED(指示灯) LV_USE_LINE0 Line(线条) LV_USE_LIST0 List(列表) LV_USE_LINEMETER0 Line Meter(线性仪表) LV_USE_OBJMASK0 Object Mask(对象遮罩) LV_USE_MSGBOX0 Message Box(消息框) LV_USE_PAGE0 Page(页面) LV_USE_SPINNER0 Spinner(加载动画) LV_USE_ROLLER0 Roller(滚轮选择器) LV_USE_SLIDER0 Slider(滑动条) LV_USE_SPINBOX0 Spinbox(数值调节框) LV_USE_SWITCH0 Switch(开关) LV_USE_TEXTAREA0 Textarea(文本区域) LV_USE_TABLE0 Table(表格) LV_USE_TABVIEW0 Tabview(选项卡视图) LV_USE_TILEVIEW0 Tileview(磁贴视图) LV_USE_WIN0 Window(窗口)
-
配置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**

编译烧录

LVGL 通用移植指南 (MCU & Linux)
本指南总结了将 LVGL 移植到任何嵌入式系统(无论是裸机 MCU 还是运行 Linux 的 SoC)的通用流程。
1. 核心源码集成
无论是什么平台,第一步都是将 LVGL 源码加入工程。
- 获取源码: 从 GitHub 克隆或下载特定版本的 LVGL。
- 添加路径: 确保编译器能够搜索到
lvgl.h所在的根目录。 - 编译配置:
- Linux/Makefile: 包含
lvgl.mk。 - MCU/IDE: 手动将
src目录下所有源文件加入编译列表,并添加头文件包含路径。
- Linux/Makefile: 包含
2. 配置文件 (lv_conf.h)
这是 LVGL 的“大脑”,决定了库的功能和行为。
- 创建文件: 复制
lv_conf_template.h并更名为lv_conf.h。 - 基本配置:
- 将
#if 0修改为#if 1。 - 颜色深度: 设置
LV_COLOR_DEPTH(通常 MCU 为 16,Linux 为 32)。 - 内存管理:
- MCU: 设置
LV_MEM_SIZE分配静态堆。 - Linux/OS: 建议设置
LV_MEM_CUSTOM 1直接使用系统的malloc/free。
- MCU: 设置
- 将
- Tick 接口:
- LVGL 需要一个毫秒级的计时器来处理动画和刷新。
- 方式 A (推荐): 开启
LV_TICK_CUSTOM 1并提供一个获取系统时间的函数。 - 方式 B: 在硬件中断(如 SysTick)中手动调用
lv_tick_inc(1)。
3. 显示驱动移植 (Display Interface)
LVGL 不直接操作硬件,它通过你提供的回调函数进行绘图。
- 初始化硬件: 初始化 LCD 控制器、SPI/I2C 总线或映射 Framebuffer。
- 注册驱动:
- 定义
lv_disp_draw_buf_t并分配显存缓冲区(可以是单缓冲或双缓冲)。 - 定义
lv_disp_drv_t结构体。 - 实现
flush_cb回调函数: 这是核心,LVGL 会传入坐标和像素数据,你需要将其拷贝到屏幕上。 - 调用
lv_disp_drv_register()。
- 定义
4. 输入设备移植 (Input Interface)
- 初始化硬件: 初始化触摸芯片、按键 GPIO 或打开 evdev 设备。
- 注册驱动:
- 定义
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_inc或LV_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()) #endiflinux
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
- arm-none-eabi工具文档.md
- arm-编程-编译-汇编-链接-bin.md
- bin文件解析.md
- sdcc
- sdcc-c编程-bin.md
- stc8g1k08a-mini
- stm32-mini
- stm32-notes
- 不同架构使用的编译器.md
- 事件驱动代替while.md
- 单片机学习总结.md
- 汇编关于出栈入栈.md
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-gnu或gcc-riscv64-unknown-elf安装。
- 源码:托管在 GitHub 的
- 厂商定制版本:
和 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是编程自己的应用。

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 0xAF。
IE=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_SW1、AFIO),你才能“操作 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内核来源
- www.kernel.org 内核官网,有linux所有版本。
- SOC厂商在linux内核官网找一个版本,在这个版本下操作。
- 板子厂商会从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
在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.bin和u-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中的系统一直都是没有动过的
-
查看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文件需要添加头部
- 要想使用cpu的一些功能。就要使用特权模式SVC模式。
- 汇编初始化SP指针。
- 设置跳转到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_defconfig 和 make -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
-
功能:
- 初始化 DDR 内存
- 初始化时钟
- 从存储设备加载主 U-Boot (
u-boot.bin) - 跳转到主 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 启动:
你需要的是:
- spl/u-boot-spl.bin → 打包到
rv1106_idblock_*.img - 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. 设备树的来源
设备树不是"从某处复制"的,而是:
- Rockchip 官方编写 → 放在 U-Boot 源码的
arch/arm/dts/目录 - 编译时自动生成 → 通过
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 的来源:
-
源文件位置:
/root/rk-tools/u-boot/arch/arm/dts/ ├── rv1106.dtsi # Rockchip 官方编写的 SoC 定义 ├── rv1106-evb.dts # Rockchip 官方编写的开发板配置 └── rv1106b-evb2-spi-nand.dts # 你的板子配置 -
编译过程:
# 步骤 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 -
由谁编写:
- ✅ Rockchip 官方工程师编写和维护
- ✅ 放在 U-Boot 源码的
arch/arm/dts/目录 - ✅ 随 U-Boot 源码一起发布
- ✅ 你可以根据需要修改或创建新的 .dts 文件
-
如何查看内容:
# 反编译为可读格式 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 无需跳过坏块。
- 优先检查用户分区 LBA 0(第一个扇区),寻找
- 注意:某些 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.bin | mkimage -T rksd | uboot.img | NAND: uboot分区 |
| DDR (闭源二进制) | boot_merger | idblock.img | NAND: idblock分区 |
| USB (闭源二进制) | - | download.bin | 烧录工具用 |
| TEE (闭源二进制) | trust_merger | trust.img | 可选 |
| zImage + dtb + resource.img | mkimage -f boot.its | boot.img | NAND: boot分区 |
| env.txt | mkenvimage | env.img | NAND: env分区 |
| rootfs目录 | mkfs.ubifs + ubinize | rootfs_*.ubi rootfs.img (软链接) | NAND: rootfs分区 |
| oem目录 | 同上 | oem_*.ubi | NAND: oem分区 |
| userdata目录 | 同上 | userdata_*.ubi | NAND: userdata分区 |
| 以上全部 | afptool + rkImageMaker | update.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

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有两种编译方式。
- rk提供的build.sh编译
- 原生编译(Makefile),不推荐。
2. 手动编译
2.1 rkbin生成
cd rkbin
./tools/boot_merge RKBOOT/RV1106MINIALL.ini
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配置项
- 设置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生成的spl和tpl
-
将tpl和spl放到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配置项
1.3 使用make.sh选择xxx_defconfig文件
这一步相当于make xxx_defconfig,make.sh帮我们做了这个步骤。
#注意make.sh会自己拼接_defconfig
./make.sh rv1106-spi-nand-nomcu
等待执行完
1.4 烧录
rk官方提供的RV1106MINIALL.ini这个SPL会去nand flash的0x00800000找uboot。
如果要指定SPL找uboot的地址。需要在配置文件中配置
CONFIG_SYS_LOAD_ADDR =

命令
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)
-
生成env.img(设置了偏移量和大小就不需要在xxx_defconfig配置)
-s 0x400000: ENV大小4M生成的
env.img是一个二进制格式的ENV数据
../uboot/tools/mkenvimage -s 0x400000 -o env.img .env.txt
-
env注意点
这3者必须完全对应。Config,mkenvimage,烧录地址。
| U-Boot Config | CONFIG_ENV_OFFSET=0x400000 CONFIG_ENV_SIZE=0x400000 | 告诉 U-Boot 去 0x400000 读,读 4M |
|---|---|---|
| mkenvimage | -s 0x400000 | 生成4M 大小的镜像 |
| 烧录地址 | 0x400000 | 将 env.img 烧录到 Flash 的 0x400000 处 |
Rockchip 平台的特殊性
再次强调,Luckfox RV1106 默认使用 CONFIG_ENVF (Environment Framework)。
-
在这种模式下,U-Boot 可能忽略
CONFIG_ENV_OFFSET,而是通过 MTD 分区表 来查找名为env的分区。 -
如果是这种情况,只要你:
.env.txt中的mtdparts定义了env在正确位置。- 烧录工具根据
mtdparts将env.img烧录到正确位置。 - 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信息
-
配置
[*] 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
关闭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信息
-
配置
[*] 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 compression1.
[*] 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 性能。可选
none、lzo、zlib等。
Compression method = no compression- 含义:Buildroot 生成 UBIFS 镜像时(即制作根文件系统镜像阶段)采用的压缩方法。这里为
no compression(不压缩)。 - 注意:这与“运行时压缩”是两个不同概念:
- 镜像生成压缩:制作
ubifs.img时是否压缩整个文件系统。如果选lzo,生成的镜像体积小,但挂载时需要解压(只读,如 squashfs 方式)。UBIFS 通常不启用此选项,因为它本身支持运行时压缩。 - 运行时压缩:文件系统运行时对写入文件实时压缩。
- 镜像生成压缩:制作
- 为什么选 no compression:防止 Buildroot 在生成镜像时对内容做额外压缩,因为运行时压缩已经足够,且双重压缩无意义。

- 作用:告诉 Buildroot 生成一个 UBI 镜像(
关闭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
-
创建oem文件夹
-
执行命令生成ubifs文件
mkfs.ubifs -r oem -m 2048 -e 126976 -c 247 -o oem_ubifs.imgr <目录>:指定要打包的根目录(如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 镜像文件名。
-
封装 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相同即可。
-
-
生成oem.img
直接修改oem.ubi名称就好了
cp oem.ubi oem.img
生成可烧写的userdata.img
-
创建userdata文件夹
-
执行命令生成ubifs文件
mkfs.ubifs -r userdata -m 2048 -e 126976 -c 247 -o userdata.img -
封装 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
-
生成oem.img
cp userdata.ubi userdata.img
-
只需要将rootfs.ubi重命名就好
cp rootfs.ubi rootfs.img
使用 Rockchip 工具打包
afptool: 将各个分区镜像打包成一个中间文件(通常叫package-file或firmware.img)。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 里使能对应控制器,配引脚 |
| 换摄像头 sensor | 改 rockchip-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-shadow 从 RK_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=ubifs | RK_PARTITION_FS_TYPE_CFG 中 rootfs 为 ubifs 类型 | [build.sh:2120] |
rk_dma_heap_cma=66M | BoardConfig 的 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, perl | BR2_PACKAGE_PYTHON3=y |
| 网络服务 | openssh, dhcpcd, nginx, vsftpd | BR2_PACKAGE_OPENSSH=y |
| 调试工具 | strace, gdb, valgrind, tcpdump | 需要自己加 |
| 系统工具 | htop, nano, iperf3, iptables | BR2_PACKAGE_HTOP=y |
| USB gadget | 串口/网卡/摄像头/U盘 模拟 | 内核配置,rootfs 配启动脚本 |
| 数据库 | sqlite, influxdb | 需要自己加 |
| 媒体库 | ffmpeg, gstreamer | Rockchip 有自己的 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_IQFILES 和 RK_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 分区存 app:
RK_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 ← 烧录工具最终认的文件
-
buildroot从网上下载需要的包。
这个步骤Buildroot 从 defconfig 读取包列表,下载源码
-
交叉编译以适配目标开发板。
配置了交叉编译器、编译生成需要的文件、安装到 staging 目录
-
生成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.cfgubinize.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 xzjffs2mkfs.jffs2 -d 目录 -o 镜像erofsmkfs.erofs 镜像 目录本质上都是把目录 + 元数据 + 压缩选项丢给一个 mkfs 工具。
-
使用afptool打包出update.img
核心是 2 个工具
tools/linux/Linux_Pack_Firmware/:①
afptool— 把多个子镜像打包成一个mk-update_pack.sh:223:afptool -pack 镜像目录 update_tmp.imgafptool读取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-fileafptool把所有子镜像按分区顺序拼接成一个update_tmp.img,并在头部写入每个分区的偏移和大小信息。②
rkImageMaker— 加上 Rockchip 头部mk-update_pack.sh:225:rkImageMaker -RK1106 download.bin update_tmp.img update.img -os_type:androidosrkImageMaker在update_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.mk 的 RK_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核心层
- _main 函数
- board_init_f_alloc_reserve,board_init_f_init_reserve (两步)
- board_init_f 比较重要
- 省略一堆
- relocate 比较重要
- 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拼接
添加自己的板子
- 在
board/rockchip/下新建目录(如board/rockchip/myboard/) - 新建 Kconfig,定义
TARGET_MYBOARD,设置SYS_BOARD/SYS_VENDOR - 新建
include/configs/myboard.h - 在
arch/arm/mach-rockchip/rv1106/Kconfig中添加source "board/rockchip/myboard/Kconfig" - 新建
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 显示 logo | U-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";
| 顺序 | 尝试匹配 | 结果 |
|---|---|---|
| 1st | U-Boot 搜 lh24030c50 | ✅ 命中我们的驱动 |
| 2nd | simple-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 显示 logo | U-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文件记录了原来可以编译成模块的配置,现在编译进了内核。专门给外部用户态工具(如modprobe、depmod)看的一份“已内置模块花名册”。它保证了即使你把驱动编进内核,用户态的热插拔和加载机制依然能“假装”找到了这个模块,从而顺畅执行后续流程。 -
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/) | 一些板级小工具 |
drv | make modules_install → 提取所有 .ko 到 kernel_drv_ko/ | 内核模块拷贝 |
strip | 如果是 RELEASE 模式,strip 掉调试符号 | 缩小根文件系统体积 |
最后根据存储介质打包根文件系统镜像:
- spi_nand →
rootfs_ubi→mkfs_ubi.sh→rootfs_base.img(UBIFS 格式) - emmc →
rootfs_ext4→mkfs_ext4.sh→rootfs_base.img(ext4 格式) - spi_nor →
rootfs_jffs2→mkfs_jffs2.sh→rootfs_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_normalCONFIG_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模式切换
| 写的内容 | 实际模式 |
|---|---|
host | Host(主机) |
device | Device/Peripheral(设备) |
otg | OTG(自动识别) |
# 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@2的reg = <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 引脚为功能 2rkcif_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=m | ✅ | GC2145 传感器 |
CONFIG_VIDEO_ROCKCHIP_CIF=m | ✅ | CIF 视频采集 |
CONFIG_VIDEO_ROCKCHIP_ISP=m | ✅ | ISP 图像处理 |
CONFIG_ROCKCHIP_DVBM=m | ✅ | ISP 依赖的缓冲管理 |
CONFIG_PHY_ROCKCHIP_CSI2_DPHY=m | ✅ | MIPI 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
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 调用) |
| 其他 | 不会被自动执行 |
数字越小越早执行,建议:
| 脚本 | 用途 | 理由 |
|---|---|---|
S10udev | udev 设备管理 | 必须先启动,否则没有 /dev/video0 |
S20urandom | 随机数种子 | 系统基础 |
S50usbdevice | USB 配置 | 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
| 引脚序号 | 复用 | 备注说明 |
|---|---|---|
| 23 | SARADC_IN0/GPIO4_C0_z | ADC判断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_hold | 60 | PWM3_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文件
-
路径:
/kernel/drivers/gpu/drm/panel
配置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
省略
加载镜像
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
步骤:
-
选择开发板,作用是生成一下步的**.BoardConfig.mk**文件
详细步骤:002_幸狐sdk编译-选择开发板
-
修改.BoardConfig.mk文件,主要是注释/删除摄像头配置、修改env适配flash大小等操作
-
适配屏幕,这一步需要修改设备树和驱动文件用来点亮屏幕。
详细步骤:004_幸狐sdk编译-适配显示屏
-
适配摄像头,需要修改设备树和驱动文件支持摄像头
作用:
生成.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"
流程(在内核设备树)
-
首先确认使用的屏幕
这里使用的屏幕:ST7789V + CTC2.8 3SPI_RGB18BIT 需要spi去做初始化。
有些通用并口LCD屏编写设备树的方式又不一样。
-
查看PCB原理图,确认接线。
-
找到背光引脚,先点亮背光。

-
修改背光设备树(GPIO控制背光)
新建
aa_rv1106-lcd.dtsi测试文件。在使用的dts中#include引入,注意不能有panel节点,panel节点会控制背光,如果panel初始化失败,背光就没有效果。-
新增背光节点
#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>; }; }; -
开启内核支持
CONFIG_BACKLIGHT_GPIO=y -
查看是否有配置
ls /sys/class/backlight #下有 backlight -
控制背光
echo 0 > /sys/class/backlight/backlight/brightness echo 1 > /sys/class/backlight/backlight/brightness
-
-
修改背光设备树(PWM控制背光)
-
新增节点
#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>; }; -
内核支持
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
-
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/) | 一些板级小工具 |
drv | make modules_install → 提取所有 .ko 到 kernel_drv_ko/ | 内核模块拷贝 |
strip | 如果是 RELEASE 模式,strip 掉调试符号 | 缩小根文件系统体积 |
最后根据存储介质打包根文件系统镜像:
- spi_nand →
rootfs_ubi→mkfs_ubi.sh→rootfs_base.img(UBIFS 格式) - emmc →
rootfs_ext4→mkfs_ext4.sh→rootfs_base.img(ext4 格式) - spi_nor →
rootfs_jffs2→mkfs_jffs2.sh→rootfs_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
- 编译驱动模块
数据引脚
| 引脚序号 | 信号名称 | 备注说明 |
|---|---|---|
| 117 | MIPI_CSI_RX_D3N | MIPI CSI 差分数据 3 负端 |
| 118 | MIPI_CSI_RX_D3P | MIPI CSI 差分数据 3 正端 |
| 119 | MIPI_CSI_RX_CK1N | MIPI CSI 差分时钟 1 负端 |
| 120 | MIPI_CSI_RX_CK1P | MIPI CSI 差分时钟 1 正端 |
| 121 | MIPI_CSI_RX_D2N | MIPI CSI 差分数据 2 负端 |
| 122 | MIPI_CSI_RX_D2P | MIPI CSI 差分数据 2 正端 |
| PHY 组 | 配套高速时钟 | 可用数据 Lane | 用途 |
|---|---|---|---|
| PHY0 | CK0P/CK0N | D0P/D0N、D1P/D1N | 摄像头 1(2Lane MIPI) |
| PHY1 | CK1P/CK1N | D2P/D2N、D3P/D3N | 摄像头 2(2Lane MIPI,当前接线) |
其他引脚
| 引脚序号 | 信号名称 | 备注说明 | 复用关系 |
|---|---|---|---|
| 5 | I2C4_SCL_M2_1V8 | I2C时钟 | VI_CIF_D11/UART5_TX_M2/I2C4_SCL_M2/GPIO3_C7_d |
| 7 | I2C4_SDA_M2_1V8 | I2C数据引脚 | VI_CIF_D12/UART5_RX_M2**/I2C4_SDA_M2**/GPIO3_D0_d |
| 2 | MIPI_CLK0_OUT | 第0路摄像头时钟线24MHz | VI_CIF_CLKO_M0/MIPI_CLK0_OUT/GPIO3_C4_d |
| 3 | MIPI_RST | VI_CIF_VSYNC_M0/GPIO3_C5_d | |
| 4 | MIPI_PWDN | Sensor 电源使能 / 待机开关,唤醒芯片输出图像 | VI_CIF_D10/PWM7_IR_M2/MIPI_CLK1_OUT/GPIO3_C6_d |
RV1106 引脚复用功能表
| 引脚号 | 引脚复用功能 |
|---|---|
| 1 | MIPI_AVDD1V8/GPIO7_VCC1V8 |
| 2 | VI_CIF_CLKO_M0/MIPI_CLK0_OUT/GPIO3_C4_d |
| 3 | VI_CIF_VSYNC_M0/GPIO3_C5_d |
| 4 | VI_CIF_D10/PWM7_IR_M2/MIPI_CLK1_OUT/GPIO3_C6_d |
| 5 | VI_CIF_D11/UART5_TX_M2/I2C4_SCL_M2/GPIO3_C7_d |
| 6 | VI_CIF_D13/UART5_RTS_M2/I2C3_SCL_M2/GPIO3_D1_d |
| 7 | VI_CIF_D12/UART5_RX_M2/I2C4_SDA_M2/GPIO3_D0_d |
| 8 | VI_CIF_D14/UART5_CTS_M2/I2C3_SDA_M2/GPIO3_D2_d |
| 9 | VI_CIF_D15/PWM1_M2/GPIO3_D3_d |
| 10 | DVDD_1 |
| 11 | SDMMC0_DET/GPIO3_A1_u |
| 12 | SDMMC0_D1/UART2_TX_M0/PWM9_M0/GPIO3_A2_u |
| 13 | GPIO4_VCC |
| 14 | SDMMC0_D0/UART2_RX_M0/PWM8_M0/GPIO3_A3_u |
| 15 | SDMMC0_CLK/UART5_RTS_M0/I2C0_SCL_M2/JTAG_LPMCU_TCK_M1/PWM10_M0/GPIO3_A4_d |
| 16 | SDMMC0_CMD/UART5_CTS_M0/I2C0_SDA_M2/JTAG_LPMCU_TMS_M1/PWM11_IR_M0/GPIO3_A5_u |
| 17 | SDMMC0_D3/UART5_TX_M0/JTAG_CPU_TMS_M0/JTAG_HPMCU_TMS_M1/GPIO3_A6_u |
| 18 | SDMMC0_D2/UART5_RX_M0/JTAG_CPU_TCK_M0/JTAG_HPMCU_TCK_M1/GPIO3_A7_u |
| 19 | RTC_AVDD3V3 |
| 20 | RTC_XOUT |
| 21 | RTC_XIN |
| 22 | SARADC_IN1/PWM1_M1/GPIO4_C1_z |
| 23 | SARADC_IN0/GPIO4_C0_z |
| 24 | SARADC_USB_AVDD1V8 |
| 25 | USB_VBUSDET |
| 26 | USB_DM |
| 27 | USB_DP |
| 28 | USB_AVDD3V3 |
| 29 | CODEC_LINEOUT |
| 30 | CODEC_VCM |
| 31 | CODEC_AVDD1V8 |
| 32 | CODEC_MICBIAS |
| 33 | CODEC_MIC0N |
| 34 | CODEC_MIC0P |
| 35 | CODEC_MICIN |
| 36 | CODEC_MIC1P |
| 37 | CODEC_AVSS |
| 38 | EMMC_D5/SPI1_CLK_M0/UART1_RX_M2/I2C2_SCL_M1/GPIO4_A7_u |
| 39 | EMMC_D3/FSPI_D3/GPIO4_A6_u |
| 40 | EMMC_D4/SPI1_CS0_M0/UART1_TX_M2/I2C2_SDA_M1/GPIO4_A5_u |
| 41 | EMMC_D0/FSPI_D0/GPIO4_A4_u |
| 42 | EMMC_D1/FSPI_D1/GPIO4_A3_u |
| 43 | GPIO3_VCC |
| 44 | EMMC_D2/FSPI_D2/GPIO4_A2_u |
| 45 | EMMC_D6/SPI1_MOSI_M0/UART0_TX_M2/I2C0_SCL_M1/GPIO4_A1_u |
| 46 | EMMC_D7/SPI1_MISO_M0/UART0_RX_M2/I2C0_SDA_M1/GPIO4_A0_u |
| 47 | EMMC_CMD/FSPI_CS0/GPIO4_B0_u |
| 48 | EMMC_CLK/FSPI_CLK/GPIO4_B1_d |
| 49 | DDR_VDDQ_1 |
| 50 | DDR_VDDQ_2 |
| 51 | DVDD_2 |
| 52 | DRAM_ZQ |
| 53 | DDR_PLL_AVDD1V8 |
| 54 | DVDD_3 |
| 55 | DVDD_4 |
| 56 | DDR_VDDQ_3 |
| 57 | TVSS |
| 58 | UART0_RX_M0/CLK_32K/CLK_REFOUT/RTC_CLKO/GPIO0_A0_z |
| 59 | UART0_TX_M0/PWM2_M0/GPIO0_A1_d |
| 60 | PWM3_IR_M0/GPIO0_A2_d |
| 61 | PMU_VCC3V3 |
| 62 | PMIC_PWR_CTRL_M1/GPIO0_A3_u |
| 63 | PMIC_PWR_CTRL_M0/PWM1_M0/GPIO0_A4_d |
| 64 | I2C1_SCL_M0/UART1_RTS_M0/PWM5_M0/GPIO0_A5_d |
| 65 | I2C1_SDA_M0/UART1_CTS_M0/PWM6_M0/GPIO0_A6_d |
| 66 | nPOR |
| 67 | PMU_DVDD0V9 |
| 68 | OSC_XIN |
| 69 | OSC_XOUT |
| 70 | OSC_AVDD1V8/PLL_AVDD1V8 |
| 71 | OSC_PLL_DVDD |
| 72 | UART3_TX_M0/I2C2_SCL_M0/PWM7_IR_M0/GPIO1_A0_d |
| 73 | UART3_RX_M0/I2C2_SDA_M0/PWM4_M0/GPIO1_A1_d |
| 74 | PWM0_M0/CPU_AVS/VI_CIF_D0_M1/GPIO1_A2_d |
| 75 | UART1_TX_M0/I2C0_SCL_M0/GPIO1_A3_d |
| 76 | UART1_RX_M0/I2C0_SDA_M0/GPIO1_A4_d |
| 77 | UART4_RX_M0/PWM3_IR_M1/GPIO1_B0_d |
| 78 | UART4_TX_M0/PWM7_IR_M1/SPI1_CS1_M0/VI_CIF_D1_M1/GPIO1_B1_d |
| 79 | JTAG_CPU_TCK_M1/UART2_TX_M1/JTAG_HPMCU_TCK_M0/JTAG_LPMCU_TCK_M0/GPIO1_B2_d |
| 80 | JTAG_CPU_TMS_M1/UART2_RX_M1/JTAG_HPMCU_TMS_M0/JTAG_LPMCU_TMS_M0/GPIO1_B3_u |
| 81 | GPIO1_VCC3V3 |
| 82 | DVDD_5 |
| 83 | VO_LCDC_D1/VI_CIF_D8_M1/PWM10_M1/UART4_RTS_M1/GPIO1_C6_d |
| 84 | VO_LCDC_D0/VI_CIF_D9_M1/PWM11_IR_M1/UART4_CTS_M1/GPIO1_C7_d |
| 85 | VO_LCDC_CLK/VI_CIF_CLKO_M1/I2C3_SCL_M1/UART5_TX_M1/PWM11_IR_M2/AUD_DSM_N/GPIO1_D3_d |
| 86 | VO_LCDC_VSYNC/VI_CIF_VSYNC_M1/I2C3_SDA_M1/UART5_RX_M1/SPI0_CS1_M0/PWM0_M1/AUD_DSM_P/GPIO1_D2_d |
| 87 | VO_LCDC_HSYNC/VI_CIF_HREF_M1/PWM10_M2/UART5_CTS_M1/UART3_RX_M1/GPIO1_D1_d |
| 88 | GPIO6_VCC |
| 89 | VO_LCDC_DEN/VI_CIF_CLKI_M1/PWM3_IR_M2/UART5_RTS_M1/UART3_TX_M1/GPIO1_D0_d |
| 90 | VO_LCDC_D2/VI_CIF_D7_M1/PWM9_M1/UART4_TX_M1/SDMMC1_D2_M1/GPIO1_C5_d |
| 91 | VO_LCDC_D3/VI_CIF_D6_M1/PWM8_M1/UART4_RX_M1/SDMMC1_D3_M1/GPIO1_C4_d |
| 92 | VO_LCDC_D4/VI_CIF_D5_M1/PWM6_M2/I2C4_SDA_M1/SDMMC1_CMD_M1/SPI0_MISO_M0/GPIO1_C3_d |
| 93 | VO_LCDC_D5/VI_CIF_D4_M1/PWM5_M2/I2C4_SCL_M1/SDMMC1_CLK_M1/SPI0_MOSI_M0/GPIO1_C2_d |
| 94 | VO_LCDC_D6/VI_CIF_D3_M1/PWM4_M2/SPI0_CLK_M0/SDMMC1_D0_M1/GPIO1_C1_d |
| 95 | VO_LCDC_D7/VI_CIF_D2_M1/PWM2_M2/SPI0_CS0_M0/SDMMC1_D1_M1/GPIO1_C0_d |
| 96 | OTP_AVDD1V8/ETH_AVDD1V8/TSADC_AVDD1V8 |
| 97 | ETH_PHY_RXN |
| 98 | ETH_PHY_RXP |
| 99 | ETH_PHY_TXN |
| 100 | ETH_PHY_TXP |
| 101 | ETH_AVDD3V3 |
| 102 | ETH_EXTR |
| 103 | DVDD_6 |
| 104 | UART0_TX_M1/I2C1_SDA_M1/VO_LCDC_D17/PWM6_M1/GPIO2_B1_d |
| 105 | UART0_RX_M1/I2C1_SCL_M1/VO_LCDC_D16/PWM5_M1/GPIO2_B0 |
| 106 | UART0_CTS_M1/I2S0_SDO1_SDI3/VO_LCDC_D15/PWM4_M1/I2C3_SDA_M0/PRELIGHT_TRIG_OUT/GPIO2_A7_d |
| 107 | UART0_RTS_M1/I2S0_SDO2_SDI2/VO_LCDC_D14/PWM2_M1/I2C3_SCL_M0/FLASH_TRIG_OUT/GPIO2_A6_d |
| 108 | GPIO5_VCC |
| 109 | SDMMC1_D1_M0/I2S0_SCLK/VO_LCDC_D8/UART1_CTS_M1/I2C4_SDA_M0/GPIO2_A0_d |
| 110 | SDMMC1_D0/I2S0_LRCK/VO_LCDC_D9/UART1_RTS_M1/I2C4_SCL_M0/GPIO2_A1 |
| 111 | SDMMC1_CLK_M0/I2S0_MCLK/VO_LCDC_D10/GPIO2_A2_d |
| 112 | SDMMC1_CMD_M0/I2S0_SDO3_SDI1/VO_LCDC_D11/GPIO2_A3_d |
| 113 | SDMMC1_D3_M0/I2S0_SDO0/VO_LCDC_D12/UART1_TX_M1/GPIO2_A4_d |
| 114 | SDMMC1_D2_M0/I2S0_SDI0/VO_LCDC_D13/UART1_RX_M1/GPIO2_A5_d |
| 115 | CPU_DVDD |
| 116 | DVDD_7 |
| 117 | VI_CIF_D0_M0/MIPI_CSI_RX_D3N/LVDS_RX_D3N/GPIO3_B0_d |
| 118 | VI_CIF_D1_M0/MIPI_CSI_RX_D3P/LVDS_RX_D3P/GPIO3_B1_d |
| 119 | VI_CIF_D2_M0/MIPI_CSI_RX_CK1N/LVDS_RX_CK1N/GPIO3_B2_d |
| 120 | VI_CIF_D3_M0/MIPI_CSI_RX_CK1P/LVDS_RX_CK1P/GPIO3_B3_d |
| 121 | VI_CIF_D4_M0/MIPI_CSI_RX_D2N/LVDS_RX_D2N/GPIO3_B4_d |
| 122 | VI_CIF_D5_M0/MIPI_CSI_RX_D2P/LVDS_RX_D2P/GPIO3_B5_d |
| 123 | VI_CIF_D6_M0/MIPI_CSI_RX_D1N/LVDS_RX_D1N/GPIO3_B6_d |
| 124 | VI_CIF_D7_M0/MIPI_CSI_RX_D1P/LVDS_RX_D1P/GPIO3_B7_d |
| 125 | VI_CIF_D8_M0/MIPI_CSI_RX_CK0N/LVDS_RX_CK0N/GPIO3_C0_d |
| 126 | VI_CIF_D9_M0/MIPI_CSI_RX_CK0P/LVDS_RX_CK0P/GPIO3_C1_d |
| 127 | VI_CIF_CLKI_M0/MIPI_CSI_RX_D0N/LVDS_RX_D0N/GPIO3_C2_d |
| 128 | VI_CIF_HREF_M0/MIPI_CSI_RX_D0P/LVDS_RX_D0P/GPIO3_C3_d |
| E-PAD | VSS |
| 引脚序号 | 信号名称 |
|---|---|
| 11 | SDMMSC0_DET |
| 14 | SDMMSC0_D0 |
| 12 | SDMMSC0_D1 |
| 18 | SDMMSC0_D2 |
| 17 | SDMMSC0_D3 |
| 15 | SDMMSC0_CLK |
| 16 | SDMMSC0_CMD0p |
相关芯片
| 功能 | IC型号 | 特点 |
|---|---|---|
| 充电管理 | SLM6300 | |
| 电量计 | CW2015CHBD | |
| 电源管理 | EA3036CQBR |
充电检测
- 引脚
| 名称 | 引脚号 | 复用关系 |
|---|---|---|
| NCHRG | 123 | VI_CIF_D6_M0/MIPI_CSI_RX_D1N/LVDS_RX_D1N/GPIO3_B6_d |
| NSTDBY | 124 | VI_CIF_D7_M0/MIPI_CSI_RX_D1P/LVDS_RX_D1P/GPIO3_B7_d |
- 电平情况
| 引脚/状态 | 充电 | 充满 | 移除 |
|---|---|---|---|
| NCHRG | L | H | H |
| NSTDBY | H | L | H |
充电检测2
说明:通过判断usb插入,来去确定是否在充电
-
引脚
名称 引脚号 复用关系 USB_VBUSDET/USB_DET 25 USB_VBUSDET/GPIO1_D0 -
默认0,插入之后1.8v
电量查看
| 名称 | 引脚号 | 复用关系 |
|---|---|---|
| I2C数据线 SCL | 75 | UART1_TX_M0/I2C0_SCL_M0/GPIO1_A3_d |
| I2C时钟线 SDA | 76 | UART1_RX_M0/I2C0_SDA_M0/GPIO1_A4_d |
| 模块名称 | 引脚序号 | 引脚名 |
|---|---|---|
| USB切换 | 58 | GPIO0_A0 |
| SD卡使能 | 59 | GPIO0_A1 |
| 电源控制 | 60 | POW_HOLD/GPIO0_A2 |
| 电源开关检测按钮 | 62 | SOC_GPIO_BELL/GPIO0_A3 |
| 红外摄像头电源开关 | 63 | GPIO0_A4 |
| 照明灯光 | 74 | PWM0_M0 |
| LCD背光 | 77 | LCD_BL_EN |
电源控制:SOC手动拉高,外围电路就能持续供电,详细看下图
电源开关检测按钮:kernel启动后就会拉高电源控制引脚持续给soc供电。

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 TOPS | 1.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

引脚使用说明:
- 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

- **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元。

淘宝价格千份单价0.62065元。

PCA9555
**说明:**2.3V~5.5V、温度范围:-40°C ~ +85°C。
**引脚:**24引脚。P00-P7、P10-P17、INT、SCL、SDA、A0、A1、A2。可检测16路
价格:
使用国产芯片立创商城价格千份1.27元。

通信模块
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; } -
注意点
-
编写的应用程序必须在.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。
- 下载对应的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
- 打开Arduino库文件夹中的
TFT_eSPI目录 - 编辑
User_Setup.h文件 - 确保有以下配置:
#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-mode | SMBus 的 警报信号模式。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 读写操作(以写操作为例)包含以下序列:
-
起始条件(S):主机发出起始信号,开始一次通信。
-
设备地址与读写位(第 1 个字节): 前 7 位(A6~A0)为从机地址,用于寻址总线上最多 127 个设备。 最低位(R/W)表示本次操作方向:
0表示主机向从机写入数据(写操作)1表示主机从从机读取数据(读操作) 该字节传输完成后,从机会在第 9 个时钟周期回复一个应答位(A)。
-
寄存器地址(第 2 个字节): 该字节(B7~B0)指向从机内部的寄存器地址。后续的读写操作将从此地址开始。 该字节传输完成后,从机再次回复一个应答位(A)。
注意:若本次操作为连续读写,每次访问该寄存器后,地址指针会自动加 1,指向下一个寄存器。
-
数据字节(第 3 个及后续字节):
- 写操作:主机发送数据字节(D7~D0),写入第 2 步指定的寄存器。从机每收到一个字节回复一个应答位(A)。之后可继续发送多个数据字节,寄存器地址自动递增。
- 读操作:主机转为接收模式,从机发送数据字节(D7~D0),内容为当前寄存器地址中的值。主机每收到一个字节回复一个应答位(最后一个字节后可回复非应答 NAK 表示结束)。之后可继续读取多个字节,寄存器地址自动递增。
-
停止条件(S):主机发出停止信号,释放总线,结束本次通信。

硬件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极为重要,所以单独拿出来作为一个注意点。
mmap 是 memory map 的缩写,它将内核中的一块内存(这里是 Framebuffer)映射到用户进程的地址空间,使得可以直接像访问普通数组一样读写这块内存,而不需要通过 read/write 系统调用。
说到mmap就不得不说零拷贝(zero-copy),举一些例子,但我们重点将mmap功能。
| 实现方式 | CPU拷贝次数 | 系统调用 | 适用场景 | 硬件要求 |
|---|---|---|---|---|
mmap+write | 1 | 2 | 小文件、需修改数据 | 无 |
sendfile | 0~1 | 1 | 静态文件传输(HTTP服务器) | 可选DMA |
splice | 0 | 1~2 | 通用内核态数据传输 | 无 |
MSG_ZEROCOPY | 0 | 1 | 大消息网络发送 | 网卡支持 |
io_uring | 0 | 0 (共享内存) | 现代高性能异步I/O | 内核5.1+ |
RDMA | 0 | 0 (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 显存:图形程序中上传纹理或顶点数据。
- Framebuffer (
4. 动态链接库的加载
当你运行一个程序时,操作系统加载 .so (共享库) 或 .dll 文件时,底层通常也使用 mmap。
- 原理:将库文件映射到内存,多个进程可以共享同一份库文件的物理页面(只读部分),节省内存。
5. 大内存分配(匿名映射)
当程序请求大块内存(例如 malloc 超过一定阈值,通常是 128KB 或 1MB)时,glibc 的 malloc 内部会使用 mmap 而不是 brk/sbrk。
- 参数:
fd设为-1,flags设为MAP_ANONYMOUS | MAP_PRIVATE。 - 优势:释放方便(直接
munmap),不会造成堆内存碎片。
总结:什么时候该用 mmap?
| 场景 | 推荐指数 | 原因 |
|---|---|---|
| 大文件随机读写 | ⭐⭐⭐⭐⭐ | 避免多次 seek 和拷贝,利用 OS 页缓存机制。 |
| 进程间共享大数据 | ⭐⭐⭐⭐⭐ | 最快的 IPC 方式,零拷贝。 |
| 操作硬件设备/显存 | ⭐⭐⭐⭐⭐ | 用户态直接访问硬件内存的唯一标准方式。 |
| 小文件顺序读写 | ⭐⭐ | read/write 更简单,且 mmap 有缺页中断开销。 |
| 网络传输 | ⭐⭐⭐ | 结合 sendfile 或 write 可实现零拷贝发送。 |
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=y | vexpress 平台 |
CONFIG_SMP=y | 多核支持 |
CONFIG_ARM_GIC=y | 中断控制器 |
CONFIG_SERIAL_AMBA_PL011=y | 串口驱动 |
CONFIG_MMC_PL180=y | SD 卡 |
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=y | SMP |
CONFIG_PREEMPT=y | preempt |
CONFIG_THUMB2_KERNEL=y | thumb2 |
# 查看内核 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 format | vermagic 不匹配 | 用同一份 .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 512M | 512MB 内存 |
-no-reboot | 关机即退出 |
6.4 vexpress-a9 外设列表
| 地址 | 设备 | 内核驱动 |
|---|---|---|
0x10009000 | PL011 UART (控制台) | SERIAL_AMBA_PL011 |
0x1000a000 | PL011 UART #1 | 同上 |
0x10011000 | SP805 定时器 | ARM_TIMER_SP805 |
0x10017000 | PL031 RTC | RTC_DRV_PL031 |
0x10020000 | PL181 MMC (SD卡) | MMC_ARMMMCI |
0x100e0000 | SMSC LAN9118 网卡 | SMC_LAN9118 |
0x1e000000 | GIC 中断控制器 | 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 仓库
# 影响:无,只是警告信息
# 解决:忽略
附:全部脚本索引
| # | 文件名 | 用途 |
|---|---|---|
| 1 | boot.sh | 一键启动 QEMU |
| 2 | kernel-build.sh | 编译/清理内核 |
| 3 | rootfs/build-initramfs.sh | 构建 initramfs |
| 4 | rootfs/init.c | PID 1 进程源码 |
| 5 | modules/002_of/Makefile | 模块 Makefile |
| 6 | modules/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 校验流程
- 主设备初始化 CRC 计算器,写入待传输数据,生成 CRC 码;
- 主设备发送数据帧(含有效数据 + CRC 码);
- 从设备接收数据后,用相同多项式计算 CRC,对比接收到的 CRC 码;
- 若一致,确认数据正确;若不一致,判定传输错误,可触发重传或报错。
SPI 额外注意事项
- 总线拓扑:SPI 支持一主多从的星形拓扑,需注意片选信号的隔离和匹配,避免信号串扰;
- 信号电平:SPI 通常为 3.3V 电平,部分外设支持 5V,需通过电平转换芯片匹配,避免引脚烧毁;
- 传输同步:主设备的 SCK 时钟需与数据传输严格同步,高频传输时需考虑 PCB 走线长度,减少信号延迟;
- 中断 / DMA:高速 SPI 传输建议使用 DMA(直接存储器访问),减少 CPU 占用;低速传输可使用中断或轮询方式。
片选和数据/命令控制
涉及到数据传输。后面再补充
1. GRAM 写操作模式
LCD 驱动 IC 具有 GRAM(图形显存)写模式。当主机发送写显存命令(通常为 0x2C)后,控制器进入该模式,后续接收的所有数据均被写入 GRAM,并直接反映在屏幕上。
重要行为:
- 在 GRAM 写模式下,如果 DC(数据/命令)引脚被意外拉低(即发送了命令),控制器会立即退出 GRAM 模式,转而执行命令解析。
- 因此,连续写入像素数据期间必须保持 DC 为高电平(数据模式)。
2. 命令分类
根据是否需要附带数据,LCD 命令可分为三类:
| 类型 | 说明 |
|---|---|
| 无数据操作命令 | 仅发送命令字节,无后续数据。例如:软件复位、进入睡眠模式等。 |
| 有数据操作命令 | 发送命令后,必须发送固定数量的数据字节。例如:设置对比度、伽马校正。 |
| 不定长数据操作 | 发送命令后,可连续发送任意长度的数据,直到被新命令打断。典型例子:写显存命令(0x2C)。 |
对于写显存操作,通常需要先通过 0x2A 和 0x2B 命令设置窗口(地址范围),包括起始坐标 (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 位传输。
- 关键限制:发送命令时(如
0x2A、0x2B、0x2C),必须使用 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显示,比如翻转,改变分辨率之类操作。
流程
- SPI初始化
- 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
- 001_vue3从浏览器加载流程.md
- 002_vue3基本介绍.md
- 003_vue2-3知识.md
- 004_setup基本使用以及结合vue.md
- 005_setup语法糖.md
- 006_ref和reactiv响应式.md
- 007_toRefs和toRef.md
- 008_计算属性.md
- 009_watch.md
- 010_watchEffect.md
- 011_标签中的ref.md
- 012_props.md
- 013_生命周期.md
- 014_hooks.md
- 015_路由概念.md
- 016_路由工作模式.md
- 017_路由其他.md
- 018_嵌套路由(传参).md
- 019_路由props.md
- 020_路由历史栈.md
- 021_编程式导航.md
- 022_pinia.md
- 023_组件通信.md
- 024_插槽.md
- 025_其他api.md
- 026_分别暴露&默认暴露.md
index.html
浏览器加载index.html
<!-- 浏览器看到的是: -->
<body>
<div id="app"></div> <!-- 空的,没有内容 -->
<script type="module" src="/src/main.ts"></script>
</body>
- 解析 HTML:遇到
<div id="app">,创建空的 DOM 元素 - 注册元素:在 DOM 树中记录这个 div,设置 id="app"
- 样式计算:计算 CSS(如果有样式的话),但现在是空的
- 布局和绘制:绘制一个空矩形(0高度或默认高度)
- 继续解析:继续解析后面的
<script>标签
进入到Vue的世界
- 解析main.ts,这一步开发环境和打包后是不同的。打包后就没有ts文件了,全部变成js文件。
- 在main.ts中,vue的语法,创建一个组件。再将这个组件绑定到id为app的div。
- 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语法混用的一些注意点。
- 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函数)
- 一个包含上述内容的数组
场景
- 监视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
路由使用
- 根文件夹下创建router文件夹。
- 创建index.ts文件。
- 在index.ts文件编写路由规则。
- 在main.ts中引入路由使用路由。
- 在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
- vue2: mode:'history'
- vue3: history:createWebHistory()
- React: BrowserRouter
url更美观没有#。缺点:后期项目上线,需要配合服务端处理路径问题
hash
- vue2: mode:'hash'
- vue3: history:createWebHashHistory()
- 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
- 001_创建文件关系作用.md
- 001_项目创建.md
- 002_各种工具安装.md
- 003_安装测试element-plus.md
- 004_windicss框架使用.md
- 005_路由和404界面.md
- 006_使用icon.md
- 007_登录界面相关.md
- 008_axios库安装使用.md
- 009_axios请求拦截.md
- 010_登录前置守卫.md
- 011_回车事件监听.md
- 012_自定义指令.md
- 013_分页列表和图片上传.md
- 014_路由嵌套实现和使用.md
- 015_el-ui使用.md
- 016_环境变量配置.md
- 017_mock模拟数据.md
- 018_store-pinio.md
- 019_路由动态生成菜单.md
- 20_配置路由权限.md
- images
项目目录
新建的项目目录缺少很多必要的目录和文件
要在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
配置项目相关内容
创建一个空白的项目


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);
});
前置守卫
- 在src文件夹新建permission.ts文件
- 在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)
})
自定义指令
-
根目录下创建directives。
-
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) } } } }) } } -
在需要的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

第一层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秘钥
-
在 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-repo 或 pip 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.zip和bin/整个目录,则写为--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
- CI-CD-jenkins
- exe4j
- File文件操作.md
- Flux流式调用.md
- inet-rouyi
- [java mqtt.md](java mqtt)
- javaTCP编程-从入门到精通
- java定时器.md
- java日志框架
- JVM
- maven
- mp分页插件.md
- mybatis别名和包扫码.md
- sku-spu.md
- spring-security鉴权.md
- springboot自动装配注意项.md
- springcloud相关
- sqlite_mybatis-plus_druid_多数据源.md
- SSE-WS.md
- SSM相关
- 互联管理平台搭建
- 代理-(反射注解鉴权).md
- 单体项目pom示例.md
- 反射+注解+鉴权.md
- 微服务项目pom解析.md
- 模板引擎
- 物联网项目文档
- 项目
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









接下来就是一直下一步
JavaTCP编程 从入门到精通
- Buffer概念.md
- Channel&Selector.md
- io模型.md
- JavaTcp之Nio.md
- javaTcp底层协议与redis通信(各种中间件协议).md
- javaTcp编程BIO.md
- java从TCP到http.md
- netty创建一些概念.md
- netty高性能tcp服务器实战.md
- static
- 零拷贝.md
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
上下文切换过程:
- 用户态 → 内核态:调用
read()系统调用 - 内核态:等待数据(进程挂起,不占用 CPU)
- 内核态:数据从网卡复制到内核缓冲区(DMA 直接内存访问)
- 内核态:数据从内核缓冲区复制到用户缓冲区
- 内核态 → 用户态:返回调用结果
特点:2 次上下文切换 + 2 次数据复制
用户进程 内核
| |
| 调用read() |
|----------------->|
| |
| 等待数据准备 |
| (进程阻塞) |--- 等待数据包到达 ---
| | ↓
| | 数据到达网卡
| | ↓
| | 数据复制到内核缓冲区
| | ↓
| 数据复制完成 |
|<-----------------|
| |
| 复制数据到用户空间|
| (内核到用户复制) |
| |
| 返回结果 |
|<-----------------|
同步非阻塞IO
上下文切换过程:
- 用户态 → 内核态:发起系统调用
- 内核态 → 用户态:立即返回(无数据)
- 重复步骤 1-2 进行轮询(多次上下文切换)
- 用户态 → 内核态:数据就绪时调用
- 内核态:数据从内核复制到用户空间
- 内核态 → 用户态:返回结果
特点:多次上下文切换 + 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次(提交) | 内核自动复制 | 回调/信号 |
数据复制过程:
- DMA 复制:网卡数据 → 内核缓冲区(由 DMA 控制器完成,不占用 CPU)
- 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服务器是极其简单的。
- new ServerSocket(8111)就能创建一个Tcp服务器。
- 执行到serverSocket.accept()方法。代码会卡(阻塞)在这个方法,不执行后面的代码。当客户端连接进来时,该方法就不卡(阻塞)。同时会返回客户端连接信息。
- 打印的信息,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);
}
}
-
当TcpServer和TcpClient连接完成之后,他们之间就会有通道。我们拿到输入流是针对Server而言。即输入到Server的流。
-
inputStream.read();方法也会阻塞,当客户端发送数据过来的时候才会取消阻塞。
-

-
客户端发送的是ascii的123,所以服务器要将ascii转成字符(char) readData。通过测试我们发现,客户端发送的是123,但服务只打印了1。read()方法读取流中的数据是挨个读的。读到一个数据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信息,但只能接收到一次,服务器还是会断开。而且我们是不知道客户端要传多少信息过来的。当,接收大小 < 发送大小。数据还是会发生丢失。
-
循环读取,代码:
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表示客户端发送完成。循环读取,每次只读一个字符,如果客户端发送的数据量大,那么性能就会下降。结合上面两种接收方式做以下改进。
-
结合优化
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) {
}
}
}
连接过来时,线程池管理这些连接。经过测试我们发现,当超出最大连接数量时,后面的连接就会断开。这样子就能防止过多连接影响性能。显然这种方式对超出连接数量后的连接不够友好。它们无论如何都不能连接到服务器,我们提供的服务器用户多了就会卡。明显我们要解决这个问题。
解决问题的方式就有:
-
大力飞砖:又称纵向扩展,即升级配置,增强服务器的硬件配置(如增加CPU、内存、存储等)来提升其性能。
-
负载均衡:又称横向扩展,即增加服务器数量,一个服务器不够用那就搞多几个服务器。
-
优化:充分利用电脑的性能,减少连接开销,减少处理开销。创建线程过多,线程上下文切换,以及每个线程都会占用一定的栈空间。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日志框架系统学习教程(完善版)
第一部分:日志基础与核心概念
- 日志的重要性与使用场景
- 为什么需要日志框架
- 日志的五个核心作用(调试、监控、审计、统计分析、故障排查)
- 良好日志实践的标准
- 日志级别详解
- TRACE、DEBUG、INFO、WARN、ERROR、FATAL
- 级别选择策略与应用场景
- 动态调整日志级别
第二部分:Java日志发展历史与架构演进
- 第一代:JDK自带日志
- java.util.logging (JUL) 的诞生与设计
- 优点与局限性
- 第二代:Log4j 1.x的统治时代
- Apache Log4j 1.x的设计理念
- 配置文件格式与基本使用
- 性能问题与停止维护
- 日志门面的出现
- 为什么需要门面模式
- Commons Logging (JCL) vs SLF4J
- 门面模式的优势与实现原理
- 第三代:现代日志框架
- Logback:SLF4J的官方实现
- Log4j 2.x:性能与功能的飞跃
- 异步日志的演进
第三部分:主流日志框架深度使用
- java.util.logging (JUL)
- 基础配置与Handler使用
- 自定义Formatter与Filter
- 在无依赖项目中应用
- 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与自定义组件
- 性能优化与监控
- Logback
- 架构与组件:Logger、Appender、Layout
- 配置文件详解:logback.xml、logback-spring.xml
- Appender:ConsoleAppender、FileAppender、RollingFileAppender
- 条件配置与变量使用
- MDC(Mapped Diagnostic Context)使用
- 性能特性与自动重载配置
第四部分:日志门面深入理解
- SLF4J深入
- 绑定机制与桥接器
- MDC与标记(Marker)高级使用
- 参数化日志与性能优化
- 桥接旧日志系统(jcl-over-slf4j, jul-to-slf4j, log4j-over-slf4j)
- 日志统一管理策略
- 多模块项目日志统一
- 第三方库日志接管
- 避免日志冲突与依赖地狱
第五部分:Spring生态集成
- Spring Framework日志集成
- Spring的日志抽象
- 与JCL、SLF4J的集成方式
- 在Spring MVC/WebFlux中的日志实践
- Spring Boot日志配置
- 默认Logback配置与自动配置原理
- 配置文件优先级:logback-spring.xml > application.properties
- Profile-specific配置
- 彩色输出与banner日志
- Spring Boot切换日志框架
- 切换到Log4j2:排除logback,添加log4j2依赖
- 切换到JUL:配置与注意事项
- 多日志框架并存策略
第六部分:高级特性与最佳实践
- 性能优化
- 异步日志的性能影响
- 合理的日志级别设置
- 避免日志中的性能陷阱
- 结构化日志
- JSON格式日志输出
- 与ELK/EFK栈集成
- 在微服务中的结构化日志实践
- 分布式追踪
- TraceId与SpanId的传递
- 集成Sleuth/Brave
- 日志与OpenTelemetry
- 日志监控与告警
- 关键错误告警配置
- 日志量监控
- 基于日志的Metrics收集
第七部分:项目实战与案例分析
- 单应用日志配置实战
- 开发/测试/生产环境不同配置
- 日志文件切割与归档策略
- 敏感信息过滤
- 微服务架构日志方案
- 集中式日志收集架构
- 日志规范与标准化
- 跨服务调用链追踪
- 常见问题排查
- 日志丢失问题定位
- 性能问题排查
- 配置不生效排查流程
第八部分:扩展与未来趋势
- 新兴日志框架了解
- tinylog
- Log4j 2.x最新特性
- 云原生环境日志
- 容器环境日志最佳实践
- Kubernetes日志方案
- Serverless函数日志
- 日志即数据理念
- 日志分析平台搭建
- 基于日志的机器学习应用
大纲
- 基本使用
- 日志级别
- 日志处理器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中没有使用
原因分析:
已经使用了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/24filters: 过滤,可以直接放行一些路径比如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.支持识图解析。
执行流程
从浏览器再回到浏览器。
- 前端访问一个路径
- 通过一系列的过滤器链(Filter),过滤器执行顺序与过滤器在servlet容器注册顺序决定。
- 来到DispatcherServlet,通过httpServletRequest拿到访问路径(uri),通过路径获取要对应的执行器(controller)。
- 拦截到达执行器(controller)前的拦截,preHandle拦截器开始作用。
- controller处理业务,DispatcherServlet调用 ViewResolver视图解析器拿到前端上报的数据,也可以通过HttpServletRequest拿到前端请求携带的数据。
- 拦截处理完的结果,postHandler执行。
- 渲染视图,结合thymeleat之类的视图框架将各种再ModelAndView或者respose中的数据,解析并且放到html文件。
- 后置拦截器afterCompletion负责清理各种这次请求产生的资源和日志。
- 再次通过,过滤器链(Filter),过滤器执行顺序与过滤器在servlet容器注册顺序决定。
- 回到浏览器。
模板引擎
前后端不分离,使用后端直接整合前端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。
关系与区别
| 维度 | SPU | SKU |
|---|---|---|
| 含义 | 商品系列/款式 | 具体单品 |
| 是否区分颜色/尺寸 | 不区分 | 区分 |
| 库存管理 | 不用于库存 | 用于精确库存 |
| 举例 | “华为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

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 解压与压缩专题(常见文件格式)
| 文件格式 | 解压命令 | 压缩命令 | 说明 |
|---|---|---|---|
| .tar | tar -xvf file.tar | tar -cvf archive.tar file/ | 仅打包,不压缩 |
| .tar.gz / .tgz | tar -xzvf file.tar.gz 或 tar -xzvf file.tgz | tar -czvf archive.tar.gz file/ | gzip 压缩 |
| .tar.bz2 / .tbz2 | tar -xjvf file.tar.bz2 或 tar -xjvf file.tbz2 | tar -cjvf archive.tar.bz2 file/ | bzip2 压缩(压缩率更高) |
| .tar.xz | tar -xJvf file.tar.xz | tar -cJvf archive.tar.xz file/ | xz 压缩(压缩率高,较慢) |
| .tar.Z | tar -xZvf file.tar.Z | tar -cZvf archive.tar.Z file/ | 古老的 compress 压缩 |
| .gz | gunzip file.gz 或 gzip -d file.gz | gzip file (原文件会被替换) 保留原文件:gzip -c file > file.gz | 单文件压缩 |
| .bz2 | bunzip2 file.bz2 或 bzip2 -d file.bz2 | bzip2 file (原文件会被替换) 保留原文件:bzip2 -c file > file.bz2 | 单文件压缩 |
| .xz | unxz file.xz 或 xz -d file.xz | xz file (原文件会被替换) 保留原文件:xz -c file > file.xz | 单文件压缩 |
| .zip | unzip file.zip | zip -r archive.zip file/ | 常见跨平台格式,可保留目录结构 |
| .rar | unrar x file.rar (需安装 unrar) rar x file.rar (需安装 rar) | rar a archive.rar file/ (需安装 rar) | 商业压缩格式,Linux 需额外软件 |
| .7z | 7z x file.7z (需安装 p7zip) | 7z a archive.7z file/ | 高压缩率,开源 |
| .lz | lzip -d file.lz | lzip -c file > file.lz | LZMA 算法,类似 xz |
| .lz4 | lz4 -d file.lz4 | lz4 file (原文件保留在 file.lz4) | 极快速压缩 |
| .zst | unzstd file.zst 或 zstd -d file.zst | zstd file (原文件保留) 压缩目录:先 tar 再 zstd | Zstandard,高压缩比+高速 |
使用 Netplan (适用于Ubuntu 18.04及以后版本)
这是Ubuntu官方推荐的配置方式。
-
编辑配置文件:
/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]
创建
- 检查现状 确认当前无Swap,并确保有足够磁盘空间。
swapon --show
df -h /
- 创建Swap文件 创建大小为2GB的Swap文件。
fallocate -l 2G /swap
- 第2步,有些文件系统不支持fallocate指令,使用dd以下命令 2048为2g
dd if=/dev/zero of=/swap bs=1M count=2048 status=progress
- 设置权限 锁定文件权限,防止被随意读取。
chmod 600 /swap
- 格式化 将文件标记为交换空间格式。
mkswap /swap
- 立即启用 激活Swap。
swapon /swap
- 验证启用 检查Swap是否成功启用并查看大小。
swapon --show
free -h
- 永久生效 配置系统启动时自动挂载此Swap。
echo '/swap none swap sw 0 0' | sudo tee -a /etc/fstab
- 设置交换积极性
echo 'vm.swappiness=60' | sudo tee -a /etc/sysctl.conf
- 确定交互积极性
sysctl -p
移除Swap
- 查看当前状态 确认正在使用的Swap文件路径。
sudo swapon --show
- 停用Swap 关闭所有Swap交换空间。
sudo swapoff /swap
- 验证已停用 确认Swap已完全关闭。
sudo swapon --show
free -h
- 删除自动挂载 从系统配置中移除Swap,使其开机不启动。
打开文件 vim /etc/fstab
删除这行 /swap none swap sw 0 0
- 删除Swap文件 永久删除磁盘上的文件以释放空间。
rm /swap
- (可选)清理内核参数 移除之前调整的swappiness优化设置。
打开文件 vim /etc/sysctl.conf
修改vm.swappiness=0
- 重启生效
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相关
- [emqx ssl证书配置.md](emqx ssl证书配置)
- images
- java-emqx共享订阅问题.md
- mqtt介绍,发布者,broker,订阅者.md
- 关于qos.md
- 设备上报主题设置.md
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向订阅者发送信息,订阅者是否收到都不关心。

Qos 1

QOS 2

单主题方式
- 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++目录—>包含目录—>编辑


项目—>属性—>链接器—>输入—>附加依赖项—>编辑
opencv_world4120.lib:Release 版本,用于最终发布的程序。opencv_world4120d.lib:Debug 版本,用于开发调试阶段。(只需要填这个就好)

测试使用
#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
背景差分
目标跟踪
移动路径
二维码识别
人脸识别

特征工程
特征提取
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=cpu 或 device=0 |
format | 导出模型格式 | format=onnx |
模型微调 数据微调
-
安装环境
pip install ultralytics # YOLOv8/v11 # 或 git clone https://github.com/ultralytics/yolov5 pip install -r requirements.txt -
准备数据集
-
格式:每张图片对应一个
.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) |
device | GPU编号 | 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

2.*切换分支
git checkout 5.12.9
3.qt creator 打开这个项目

4.构建
5.构建完成后
**注意区分是debug构建还是release构建。
**如果构建和引用错了,会连接不上mqtt服务器

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终端界面;
第二步:
找到项目release的地址信息,注意必须是release,选debug打包会很大
第三步:
执行打包命令

Rbac和Saas
rbac权限管理框架
最基本的五个表格
-
用户
-
角色
-
权限(菜单)
-
用户角色
-
角色权限
设计
用户表(sys_user)
| 字段名 | 数据类型 | 注释 | 关系&默认 |
|---|---|---|---|
| id | int | 主键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)
| 字段名 | 数据类型 | 注释 | 约束 |
|---|---|---|---|
| id | bigint | 主键id | 不为空 |
| name | varchar(50) | 租户名称 | |
| package_id | bigint | 套餐id | 外键,关联租户套餐表 |
租户套餐表(tenant_package)
| 字段名 | 数据类型 | 注释 | 约束 |
|---|---|---|---|
| id | bigint | 主键id | 不为空 |
| menu_ids | text | 菜单id集合 | 存储格式:逗号分隔或JSON数组 |
菜单表(menu)
| 字段名 | 数据类型 | 注释 | 约束 |
|---|---|---|---|
| id | bigint | 主键id | 不为空 |
| name | varchar | 菜单名称 | |
| perm | varchar | 权限标识 | 如:sys:user:list |
角色表(role)
| 字段名 | 数据类型 | 注释 | 约束 |
|---|---|---|---|
| id | bigint | 主键id | 不为空 |
| name | varchar | 角色名称 | |
| tenant_id | bigint | 租户id | 外键,关联租户表 |
角色菜单关联表(role_menu)
| 字段名 | 数据类型 | 注释 | 约束 |
|---|---|---|---|
| id | bigint | 主键id | 不为空 |
| menu_id | bigint | 菜单id | |
| role_id | bigint | 角色id |
业务流程说明
一、创建租户
-
选择套餐 运营人员在前端为待创建租户选择一个已配置好的套餐(即
tenant_package中的一条记录)。 -
创建租户管理员账号 系统自动创建一个用户账号,该账号默认为当前租户的管理员。
-
创建租户管理员角色 系统为当前租户新增一条角色记录,角色名称建议为“租户管理员”或“{租户名称}_admin”,并关联
tenant_id。 -
初始化角色菜单权限 根据所选套餐中的
menu_ids(菜单ID集合),系统批量将菜单与角色的关联关系插入role_menu表,完成管理员角色的权限初始化。说明:租户管理员角色的权限来源于租户套餐,通过角色与菜单的关联实现,为后续权限扩展(如新增子角色)奠定基础。
二、修改套餐配置
- 更新套餐定义
运营人员修改
tenant_package表中的menu_ids字段内容(例如新增或移除某些菜单ID)。 - 查询受影响的租户
系统查询所有
tenant表中package_id指向该套餐的租户记录。 - 遍历租户并同步权限
对每个受影响的租户执行以下操作:
- 获取该租户的“租户管理员”角色(或所有需要同步的角色)。
- 获取当前角色的已有菜单权限(从
role_menu查询)。 - 将原
menu_ids与新menu_ids进行比对:- 新增菜单:将新出现的菜单ID插入
role_menu。 - 删除菜单:将不再包含的菜单ID从
role_menu中移除。
- 新增菜单:将新出现的菜单ID插入
- 提交变更,确保租户管理员权限与套餐定义一致。
三、用户登录与权限加载
-
用户登录认证 用户输入账号密码,系统校验身份合法性。
-
查询用户角色 根据用户ID关联查询其所属的角色(支持多角色)。
-
查询角色菜单权限 根据角色ID集合,关联查询
role_menu和menu表,获取所有授权菜单及权限标识(perm)。关键点:权限来源于角色与菜单的关联关系,而非直接关联租户套餐。租户管理员角色的菜单权限已在创建租户时通过套餐初始化至
role_menu,因此登录时只需通过角色即可获取完整权限。 -
构建权限数据 系统去重后生成权限标识列表(如
["sys:user:list", "sys:role:add"])。 -
返回前端渲染 将权限列表返回给前端,前端根据权限控制菜单显隐及按钮级别的操作权限。
中间件
Kafka
Scala和java开发的消息中间件
线程之间数据交互
每个线程都有自己的栈内存,但是所有线程共用堆内存。只要把需要共享的数据放到堆内存就可以实现数据交互。在java并发工具包中java.util.concurrent,可以实现线程之间数据共享。
进程之间数据交互
一般通过socket
消息队列的两种模式
点对点模式
消费者主动拉数据,消息收到后清除消息。
producer ----生产---->message queue ----消费---->comsumer---->确认----mq---->mq删除已经消费的消息
发布订阅模式
- 可以有多个topic主题(浏览,点赞,收藏,评论等)
- 消费者消费数据之后,不删除数据
- 每个消费者互相独立,都可以消费到数据
producers----消息---->topic----推送---->comsumers
直接安装
-
直接按照下载的版本是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
2. 配置规则
这一步是规定什么主题要发送到kafka的什么主题
比如接收kafka/# 这一类以kafka开头的主题,#是通配符,表示所有然后推送到kafka的test主题
首先要创建动作。规定这个kafka/# 主题要做什么动作。比如说推送到kafka的test主题,或者推送到Redis中。
3. 测试

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指令:用于数据迁移,一下子将磁盘文件插入到数据库。

文件里面的数据主键顺序插入,也要比乱序高。
顺序插入高效原因
查询优化
索引优化
- 在 WHERE、JOIN、ORDER BY、GROUP BY 涉及的列上建立合适索引。
- 使用覆盖索引(索引包含查询的所有列),避免回表。
- 避免在索引列上使用函数或计算(如
WHERE DATE(col)=...→ 应改为范围条件)。 - 区分度低的列(如性别)不单独建索引,可联合索引放在后面。
- 联合索引遵循最左前缀法则。
- 定期分析并重建索引(
OPTIMIZE TABLE)。
SQL 语句优化
- 避免
SELECT *,只取需要的列。 - 用
EXISTS代替IN(子查询数据量大时),或者使用JOIN改写。 - 用
UNION ALL代替UNION(不需要去重时)。 - 大分页优化:
LIMIT 100000,10→ 改为WHERE id > 上次最大id LIMIT 10(游标分页)。- 或先通过覆盖索引查出主键,再回表取数据。
- 避免
OR导致索引失效,可改用UNION或优化为IN。 - 使用
LIKE时避免前置通配符'%abc',会索引失效。 - 合理使用
EXPLAIN分析执行计划,关注type(至少达到range或ref)、rows、Extra中是否出现Using filesort/Using temporary。
表结构优化
- 字段类型尽量小且固定长度(如用
INT不用BIGINT,用CHAR代替变长等),减少 I/O。 - 适当反范式化(冗余常用字段)减少 Join。
- 拆分大表(水平分表/垂直分表)。
- 对只读或很少变动的历史数据使用归档表。
其他
- 查询缓存(MySQL 5.7 及之前,8.0 已废弃),若使用则避免不适合缓存的查询(带
NOW()等)。 - 限制返回行数:
LIMIT子句尽早加。 - 避免在循环中执行 SQL,改成批量或 JOIN。
更新&删除优化
- 批量更新/删除
每次操作限定行数(如
LIMIT 1000),配合循环,避免长事务锁住大表。 - 利用索引 WHERE 条件必须命中索引,否则行锁升级为表锁(InnoDB 走索引才锁行)。
- 避免锁争用 更新高频热行时,考虑排队机制或减少并发直接写。
- 先查询后更新 如果更新逻辑复杂,可先查出主键集合,再按主键批量更新。
- 分区裁剪 对分区表进行 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, 图片等)能够被正常访问
当一个浏览器访问你的单页面应用时,会发生以下步骤:
- 浏览器请求
/YH-LX001B/-> Nginx 返回index.html。 index.html文件被浏览器解析,它里面包含了链接去请求各种静态资源,比如:<script src=“js/app.js”><link href=“css/style.css”>
- 浏览器会向服务器发起新的请求:
GET /YH-LX001B/js/app.jsGET /YH-LX001B/css/style.css
正确的流程是怎样的?
有了完整的 try_files $uri $uri/ /YH-LX001B/index.html;,流程是这样的:
- 请求
/YH-LX001B/js/app.js:$uri-> Nginx 在网站根目录下寻找/YH-LX001B/js/app.js这个文件。- 找到了! 于是 Nginx 停止尝试,直接将这个正确的 JavaScript 文件发送给浏览器。✅
- 请求
/YH-LX001B(一个目录):$uri-> 寻找名为YH-LX001B的文件,没找到。$uri/-> 寻找名为YH-LX001B/的目录,找到了。- Nginx 会默认寻找这个目录下的索引文件(如
index.html),并返回它。✅
- 请求
/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,表示宽度或者高度就是这么大。
项目规划
物联网项目文档
-
前言
本文档为物联网设备管理平台(软硬件一体化)项目的全套工程文档,涵盖项目从立项调研到测试验收的全生命周期,包含软件工程与硬件设备开发工程相关内容。结合项目定位(软硬件一体化提供商,提供多场景物联网服务),文档采用“核心内容整合、软硬件模块分离”的结构——整体项目框架、立项、可行性分析等核心内容统一整合,软件、硬件的需求分析、设计、测试等专项内容独立分节,既保证项目整体性,又兼顾软硬件开发的专业性和独立性,解决“整合与分离”的核心需求。软件技术选型、硬件设备选型可根据自身规划,直接填充至对应章节的指定位置。
项目核心定位:作为软硬件一体化物联网服务提供商,搭建支持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.1.4 项目目标
1.1.4.1 总体目标
搭建一套稳定、高效、可扩展的物联网设备管理平台,实现软硬件深度协同,支持多协议设备接入、多场景服务落地,成为专业的物联网软硬件一体化服务提供商,满足客户在设备管理、数据采集、远程控制等方面的核心需求,提升客户满意度与市场占有率。
1.1.4.2 阶段性目标
-
立项调研阶段(1-2周):完成市场调研、技术调研,确定软硬件选型方案,完善可行性分析;
-
需求分析与设计阶段(3-4周):完成软件、硬件的需求分析,完成概要设计、详细设计,输出设计文档;
-
开发实现阶段(8-10周):完成软件平台开发、硬件设备选型与集成,实现软硬件协同联调;
-
测试验收阶段(2-3周):完成软件、硬件及系统集成测试,修复问题,通过验收,输出测试报告;
-
上线部署阶段(1-2周):完成平台上线、硬件部署,提供客户培训与技术支持,进入运维阶段。
1.1.5 项目范围
-
软件范围:物联网设备管理平台开发,包括设备接入模块、数据采集与存储模块、远程控制模块、数据分析模块、用户管理模块、权限管理模块等,支持MQTT、TCP、HTTP等多协议接入;
-
硬件范围:硬件设备选型、集成与调试,包括温湿度传感器、控制终端、通信模块等,适配多场景部署,与软件平台实现无缝对接;
-
服务范围:为客户提供软硬件一体化部署、调试、培训、售后技术支持,覆盖智能家居、智慧农业、智能工厂等核心场景;
-
排除范围:不涉及硬件设备的核心芯片自主研发(仅做选型与集成),不涉及第三方平台的二次开发(除非客户特殊需求)。
1.1.6 项目资源需求
-
人力资源:软件开发工程师、硬件工程师、测试工程师、产品经理、项目管理人员、运维工程师;
-
硬件资源:测试用硬件设备(传感器、控制终端、通信模块等)、服务器、网络设备;
-
软件资源:开发工具、测试工具、数据库、操作系统、协议调试工具等;
-
资金资源:研发资金、硬件采购资金、测试资金、培训资金等。
1.1.7 立项审批意见
审批人:__________
审批意见:__________
审批日期:__________
-
第二部分:可行性分析报告
2.1 概述
本报告针对物联网设备管理平台(软硬件一体化)项目,从市场、技术、经济、操作、风险五个维度进行可行性分析,判断项目是否具备实施条件,为项目决策提供科学依据。本次分析基于当前市场环境、技术水平及自身资源,结合项目目标与范围,确保分析结果真实、可靠、具有指导性。
2.2 市场可行性分析
-
市场需求:随着物联网技术在各行业的渗透,智能家居、智慧农业、智能工厂等领域对设备管理平台的需求持续增长,客户对“软硬件一体化”服务的需求日益迫切,避免了单独采购软硬件的整合成本与技术壁垒,市场空间广阔;
-
市场竞争力:当前市场中,多数物联网服务提供商要么只做软件平台,要么只做硬件设备,软硬件一体化提供商较少,项目凭借“多协议接入”“多场景适配”“一站式服务”的优势,可形成差异化竞争,契合市场需求;
-
市场前景:物联网产业处于快速发展阶段,政策支持力度大,各行业数字化转型加速,未来对物联网设备管理平台及软硬件一体化服务的需求将持续提升,项目具有良好的市场前景和可持续性。
2.3 技术可行性分析
-
技术成熟度:MQTT、TCP、HTTP等设备接入协议已成为物联网领域的主流协议,技术成熟、应用广泛;软件平台开发(如设备管理、数据存储、远程控制)、硬件设备选型与集成技术均已成熟,不存在难以突破的技术壁垒;
-
技术储备:项目团队已明确软件技术选型与硬件设备选型方案,具备相关的开发、集成、测试技术能力,可支撑项目顺利实施;
-
软硬件协同:采用成熟的软硬件协同技术,通过标准化接口实现软件平台与硬件设备的无缝对接,可确保数据传输稳定、控制指令高效执行,解决软硬件脱节问题;
-
可扩展性:软件平台采用模块化设计,硬件设备支持灵活替换与扩展,可根据后续市场需求,快速适配新的场景、新的设备类型,技术扩展性强。
2.4 经济可行性分析
-
成本估算:项目成本主要包括硬件采购成本、研发成本、人力成本、测试成本、培训成本、运维成本等,结合项目规模与阶段性目标,成本可控,可通过合理规划优化成本;
-
收益预测:项目收益主要来自软硬件一体化服务收费、设备销售、售后运维服务等,随着客户群体的拓展,收益将逐步提升,预计在项目上线后1-2年内实现盈利;
-
投资回报:综合成本与收益分析,项目投资回报率合理,风险可控,具备良好的经济可行性,符合企业长期发展战略。
2.5 操作可行性分析
-
团队能力:项目团队具备软件开发、硬件集成、测试、项目管理等相关能力,可熟练完成项目各阶段工作,确保项目顺利推进;
-
操作难度:软件平台操作界面简洁、易用,客户可快速掌握设备接入、数据查看、远程控制等操作;硬件设备部署简单、调试便捷,可适应不同场景的部署需求;
-
运维保障:制定完善的运维方案,配备专业的运维工程师,可及时处理软件故障、硬件故障,保障平台与设备的稳定运行,降低操作与运维难度。
2.6 风险可行性分析
2.6.1 潜在风险
-
技术风险:软硬件协同过程中可能出现兼容性问题,设备接入过程中可能出现协议适配问题,影响平台稳定性;
-
市场风险:市场需求变化过快,或竞争对手推出同类产品,影响项目市场占有率;
-
成本风险:硬件采购价格波动、研发成本超支,导致项目成本增加;
-
进度风险:项目各阶段工作推进滞后,影响项目上线时间。
2.6.2 风险应对措施
-
技术风险:提前进行技术调研与测试,完善软硬件选型方案,加强软硬件协同联调,建立技术问题应急处理机制,及时解决兼容性、协议适配等问题;
-
市场风险:持续关注市场需求变化,加强市场调研,及时优化产品与服务,打造差异化优势,加强客户维护,提升客户粘性;
-
成本风险:合理规划采购计划,与供应商签订长期合作协议,控制硬件采购成本;优化研发流程,避免研发成本超支,建立成本监控机制;
-
进度风险:制定详细的项目进度计划,明确各阶段工作任务与时间节点,加强项目进度监控,及时调整工作安排,确保项目按时推进。
2.7 可行性结论
综合市场、技术、经济、操作、风险五个维度的分析,本项目市场需求明确、技术成熟、成本可控、风险可应对,具备完全的可行性,建议立项实施。
第三部分:需求分析文档
本章节采用“整体需求+软件需求+硬件需求”的结构,整体需求明确项目核心诉求,软件、硬件需求分别独立阐述,兼顾整合性与独立性,确保需求清晰、可落地、可验证。
3.1 整体需求
-
多协议接入需求:支持MQTT、TCP、HTTP等多种设备接入协议,实现不同类型、不同厂商硬件设备的快速接入,解决设备接入标准化问题;
-
软硬件协同需求:软件平台与硬件设备无缝对接,实现数据实时采集、远程控制指令高效下发,确保软硬件协同稳定运行;
-
多场景适配需求:适配智能家居、温湿度数据上报、智慧农业、智能工厂等多场景,提供针对性的软硬件解决方案;
-
核心功能需求:实现设备管理、数据采集与存储、远程控制、数据分析、用户管理、权限管理等核心功能;
-
性能需求:平台运行稳定,数据传输延迟低,设备接入容量可扩展,硬件设备运行可靠、功耗合理;
-
易用性需求:软件平台操作简洁,硬件设备部署、调试便捷,客户可快速上手,降低使用成本;
-
安全性需求:保障设备接入安全、数据传输安全、数据存储安全,防止非法接入、数据泄露、指令篡改。
3.2 软件需求(独立模块)
3.2.1 功能需求
3.2.1.1 设备接入模块
-
支持MQTT、TCP、HTTP三种核心协议接入,可扩展其他物联网协议;
-
支持设备批量接入与单个接入,提供设备接入指引与配置工具;
-
实现设备接入验证(设备ID、密钥验证),防止非法设备接入;
-
支持设备在线状态监测,实时显示设备接入状态(在线、离线、异常),异常状态及时提醒;
-
支持设备接入日志记录,可查询设备接入时间、接入协议、接入状态等信息。
3.2.1.2 数据采集与存储模块
-
实时采集硬件设备上传的数据(如温湿度、设备运行参数、状态数据等),采集频率可配置;
-
支持数据格式解析与转换,确保不同设备的数据统一格式存储;
-
采用可靠的数据库存储数据,支持历史数据查询、导出,数据存储期限可配置;
-
支持数据异常检测,当数据超出预设阈值时,触发异常提醒;
-
保障数据传输过程中的完整性,防止数据丢失、篡改。
3.2.1.3 远程控制模块
-
支持通过软件平台向硬件设备下发控制指令(如开关控制、参数调节等);
-
实时反馈指令执行结果,显示设备执行状态;
-
支持控制指令日志记录,可查询指令下发时间、指令内容、执行结果;
-
支持批量控制多个设备,提高操作效率;
-
当指令执行失败时,提供失败原因提示,并支持重试功能。
3.2.1.4 数据分析模块
-
支持对采集的数据进行统计分析(如平均值、最大值、最小值、趋势分析等);
-
提供数据可视化展示(图表、报表等),便于客户直观查看数据变化;
-
支持自定义分析规则,根据客户需求生成针对性的分析报告;
-
针对不同场景(如智慧农业的土壤湿度分析、智能工厂的设备运行效率分析)提供专属分析功能。
3.2.1.5 用户管理与权限管理模块
-
支持用户注册、登录、密码重置、账号注销等功能;
-
支持用户信息管理(修改个人信息、绑定联系方式等);
-
支持权限分级管理,不同角色(管理员、普通用户、运维人员)拥有不同的操作权限;
-
管理员可管理所有用户、设备、数据,普通用户仅可查看自身绑定的设备与数据,运维人员可进行设备调试与故障处理。
3.2.1.6 系统管理模块
-
支持系统参数配置(如数据采集频率、异常阈值、存储期限等);
-
支持系统日志记录,可查询操作日志、设备日志、异常日志等;
-
支持系统升级与维护,确保系统稳定运行;
-
支持数据备份与恢复,防止数据丢失。
3.2.2 非功能需求
-
性能需求:平台响应时间≤1s,数据传输延迟≤500ms;支持至少1000台设备同时在线接入,可扩展至10000台以上;系统可用性≥99.9%;
-
安全性需求:采用加密技术(如SSL/TLS)保障数据传输安全;设备接入采用密钥验证,防止非法接入;数据存储加密,防止数据泄露;定期进行安全检测,及时修复安全漏洞;
-
可扩展性需求:采用模块化设计,支持功能模块扩展(如新增协议接入、新增分析功能);支持设备类型扩展,可适配新的硬件设备;
-
易用性需求:操作界面简洁、直观,导航清晰,客户可快速掌握操作方法;提供操作指引与帮助文档;
-
兼容性需求:支持Windows、Linux等主流操作系统,支持Chrome、Edge等主流浏览器;
-
可维护性需求:系统日志清晰,便于问题排查;模块之间低耦合,便于维护与升级。
3.2.3 接口需求
-
设备接入接口:支持MQTT、TCP、HTTP协议接口,用于设备与平台的数据交互;
-
硬件对接接口:提供标准化接口,用于软件平台与硬件设备的协同对接,支持数据采集与指令下发;
-
内部接口:各模块之间的接口,确保模块之间的数据交互顺畅;
-
外部接口(可选):提供API接口,支持与第三方平台对接,实现数据共享。
3.3 硬件需求(独立模块)
3.3.1 硬件设备类型及需求
3.3.1.1 感知设备
-
温湿度传感器:用于采集环境温湿度数据,精度≥±0.5℃(温度)、±5%RH(湿度);支持低功耗运行;支持MQTT/TCP/HTTP协议;适配室内外多种场景;
-
其他感知设备(可选):根据场景需求,配置土壤湿度传感器、光照传感器、压力传感器等,要求精度达标、运行稳定、支持对应协议。
3.3.1.2 控制终端
-
用于接收软件平台下发的控制指令,执行相应操作(如开关控制、参数调节);
-
支持与感知设备、通信模块对接,实现数据采集与指令执行;
-
运行稳定,响应迅速,指令执行延迟≤300ms;
-
支持低功耗模式,适配不同供电场景(市电、电池)。
3.3.1.3 通信模块
-
支持MQTT、TCP、HTTP三种核心协议,可实现设备与软件平台的数据传输;
-
通信稳定,信号强度强,支持远距离传输(根据场景需求,可选择Wi-Fi、4G/5G、LoRa等通信方式);
-
低功耗、小体积,便于部署;
-
支持自动重连功能,当网络中断后,可自动重新连接平台。
3.3.1.4 辅助设备
-
电源设备:为感知设备、控制终端、通信模块提供稳定供电,支持市电、太阳能、电池等多种供电方式;
-
部署支架:用于设备固定,适配室内外部署场景,防水、防尘、抗干扰。
3.3.2 硬件性能需求
-
运行可靠性:硬件设备平均无故障运行时间(MTBF)≥10000小时;
-
环境适应性:适应温度范围-20℃~60℃,湿度范围10%~90%RH;防水、防尘、抗电磁干扰;
-
功耗需求:感知设备、通信模块采用低功耗设计,电池供电模式下,续航时间≥6个月;
-
数据采集精度:各类传感器的数据采集精度符合行业标准,确保数据准确性;
-
兼容性:硬件设备之间可无缝对接,与软件平台通过标准化接口对接,支持协议适配。
3.3.3 硬件接口需求
-
通信接口:支持UART、SPI、I2C等常用接口,用于与通信模块、感知设备对接;
-
供电接口:标准化供电接口,支持不同供电方式接入;
-
扩展接口:预留扩展接口,便于后续新增设备或功能扩展。
3.3.4 硬件部署需求
-
部署便捷:设备体积小、重量轻,便于安装与部署,无需复杂施工;
-
维护便捷:设备支持远程调试、固件升级,减少现场维护工作量;
-
场景适配:根据智能家居、智慧农业、智能工厂等不同场景,提供对应的部署方案,确保设备运行稳定。
3.4 需求确认
需求提出人:__________
需求确认人:__________
确认日期:__________
核心逻辑:三层结构
文档可以理解为三个逻辑层次:
- 共识层(第1-3章):对齐背景、目标、用户,回答 “为什么做”和“为谁做”。
- 定义层(第4-7章):详细描述解决方案,回答 “做什么”和“做成什么样”。
- 约束与收尾层(第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. 文档概述
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 项目架构设计
项目采用“云-边-端”协同的四层架构,实现软硬件一体化协同运行,具体架构如下:
-
感知层:由各类硬件设备组成(感知设备、控制终端、通信模块等),负责数据采集与指令执行,是项目的“终端入口”;
-
边缘层:部署在设备侧的边缘计算节点,承担协议转换、数据预处理、本地规则计算等功能,减少网络带宽占用,实现本地故障快速响应;
-
平台层:即物联网设备管理软件平台,包含各类核心功能模块,负责设备管理、数据处理、远程控制、数据分析等,是项目的“核心大脑”;
-
应用层:面向不同场景的可视化界面与服务,为客户提供设备操作、数据查看、分析报告等服务,适配智能家居、智慧农业、智能工厂等多场景。
4.1.2 软硬件协同架构
软件平台与硬件设备通过标准化接口实现协同,具体交互流程如下:
-
硬件设备通过通信模块,采用MQTT/TCP/HTTP协议接入软件平台,完成身份验证;
-
感知设备采集数据后,通过通信模块上传至软件平台,软件平台对数据进行解析、存储与分析;
-
软件平台下发控制指令,通过通信模块传输至控制终端,控制终端执行指令,并将执行结果反馈至软件平台;
-
边缘层负责数据预处理与本地决策,当出现紧急情况时,可直接控制硬件设备,同时将相关信息上报至软件平台。
4.1.3 设计原则
-
模块化设计:软件、硬件均采用模块化设计,便于功能扩展、维护与升级;
-
标准化设计:接口、协议采用行业标准,确保软硬件兼容性与可扩展性;
-
稳定性设计:优先选择成熟技术与设备,确保系统与设备运行稳定,降低故障发生率;
-
安全性设计:融入安全设计理念,保障设备接入、数据传输、存储的安全性;
-
易用性设计:兼顾软件操作与硬件部署的易用性,降低客户使用与维护成本。
4.2 软件概要设计(独立模块)
4.2.1 软件架构设计
软件平台采用分层架构设计,从上至下分为应用层、业务逻辑层、数据访问层、数据存储层,各层独立运行、相互协作,具体如下:
-
应用层:面向用户的操作界面,包括设备管理界面、数据查看界面、远程控制界面、数据分析界面等,负责用户交互;
-
业务逻辑层:核心业务处理层,包含设备接入、数据采集、远程控制、数据分析、用户管理等功能模块,负责业务逻辑处理;
-
数据访问层:负责与数据存储层交互,实现数据的查询、新增、修改、删除等操作,为业务逻辑层提供数据支持;
-
数据存储层:采用数据库存储各类数据(设备数据、用户数据、日志数据等),确保数据安全、可靠。
4.2.2 核心模块设计
4.2.2.1 设备接入模块
-
模块功能:负责设备接入验证、协议解析、在线状态监测、接入日志记录;
-
核心逻辑:设备发起接入请求→模块验证设备身份(设备ID、密钥)→验证通过后,根据接入协议解析数据→记录接入日志,更新设备在线状态;
-
依赖模块:数据访问层(存储设备信息、接入日志)、数据采集模块(接收设备上传的数据)。
4.2.2.2 数据采集与存储模块
-
模块功能:接收设备上传的数据、解析数据格式、存储数据、异常数据检测、数据备份与恢复;
-
核心逻辑:接收设备数据→解析数据格式(统一为标准格式)→检测数据是否异常→将正常数据存储至数据库,异常数据触发提醒并记录→定期进行数据备份;
-
依赖模块:设备接入模块(获取设备数据)、数据访问层(存储数据)、数据分析模块(提供数据支持)。
4.2.2.3 远程控制模块
-
模块功能:下发控制指令、接收指令执行结果、记录指令日志、指令重试;
-
核心逻辑:用户发起控制指令→模块生成标准化指令→通过通信接口下发至设备→接收设备执行结果→记录指令日志,若执行失败则提醒并支持重试;
-
依赖模块:设备接入模块(获取设备在线状态)、数据访问层(存储指令日志)。
4.2.2.4 数据分析模块
-
模块功能:数据统计分析、数据可视化、自定义分析规则、生成分析报告;
-
核心逻辑:从数据存储层获取数据→根据预设规则或自定义规则进行统计分析→生成可视化图表与分析报告→提供数据查询与导出功能;
-
依赖模块:数据采集与存储模块(获取数据)、数据访问层(存储分析结果)。
4.2.2.5 用户管理与权限管理模块
-
模块功能:用户注册、登录、信息管理、权限分配、角色管理;
-
核心逻辑:用户注册/登录→验证用户信息→根据角色分配权限→用户可修改个人信息,管理员可管理用户与权限;
-
依赖模块:数据访问层(存储用户信息、权限信息)。
4.2.2.6 系统管理模块
-
模块功能:系统参数配置、日志管理、系统升级、数据备份与恢复;
-
核心逻辑:管理员配置系统参数→模块记录各类日志→支持系统在线升级→定期进行数据备份,出现异常时可恢复数据;
-
依赖模块:数据访问层(存储系统参数、日志数据、备份数据)。
4.2.3 接口设计
4.2.3.1 设备接入接口
-
MQTT接口:用于设备与平台的数据交互,采用MQTT 3.1.1协议,端口:1883(TCP)、8883(SSL);
-
TCP接口:用于设备与平台的双向通信,端口:8080,数据格式:JSON;
-
HTTP接口:用于设备上传数据与接收指令,请求方式:POST/GET,数据格式:JSON。
4.2.3.2 内部模块接口
-
设备接入模块→数据采集模块:提供设备数据接口,传递设备上传的数据;
-
远程控制模块→设备接入模块:提供指令下发接口,传递控制指令;
-
各模块→数据访问层:提供数据查询、新增、修改、删除接口,实现数据交互。
4.2.3.3 外部接口(可选)
提供RESTful API接口,支持与第三方平台对接,数据格式:JSON,采用API密钥验证,确保接口安全。
4.2.4 数据存储设计
-
数据库选型:__________(根据自身技术选型填写,如MySQL、MongoDB等);
-
数据分类存储:
(1)设备数据:存储设备ID、设备类型、接入协议、在线状态、部署位置等信息;
(2)采集数据:存储设备上传的温湿度、运行参数等数据,按时间戳排序;
(3)用户数据:存储用户账号、密码(加密存储)、个人信息、角色权限等信息;
(4)日志数据:存储设备接入日志、操作日志、指令日志、异常日志等信息;
(5)系统数据:存储系统参数、配置信息、备份数据等信息。
4.2.5 技术选型补充
-
开发语言:__________(如Java、Python、Go等);
-
前端框架:__________(如Vue、React等);
-
服务器:__________(如阿里云、腾讯云服务器等);
-
其他技术:__________(如消息队列、缓存技术等,根据自身选型填写)。
4.3 硬件概要设计(独立模块)
4.3.1 硬件整体架构设计
硬件系统由感知层设备、控制终端、通信模块、辅助设备组成,各设备通过标准化接口对接,形成完整的硬件体系,具体架构如下:
-
感知层:由温湿度传感器、其他场景化传感器组成,负责采集环境与设备数据;
-
控制层:由控制终端组成,负责接收软件平台指令,控制感知设备与执行器;
-
通信层:由通信模块组成,负责实现硬件设备与软件平台的数据传输与指令交互;
-
辅助层:由电源设备、部署支架等组成,为硬件系统提供供电与部署支持。
4.3.2 核心硬件设备设计
4.3.2.1 感知设备设计
- 温湿度传感器:
(1)核心组件:传感器芯片、数据处理单元、接口单元;
(2)工作原理:传感器芯片采集温湿度数据,经数据处理单元转换为标准格式,通过接口单元传输至控制终端;
(3)选型补充:__________(根据自身硬件选型填写传感器型号、厂商等)。
4.3.2.2 控制终端设计
-
核心组件:主控芯片、接口单元、执行单元、电源单元;
-
工作原理:主控芯片接收通信模块传输的控制指令,控制执行单元执行相应操作,同时通过接口单元获取感知设备的数据,上传至通信模块;
-
选型补充:__________(根据自身硬件选型填写主控芯片型号、厂商等)。
4.3.2.3 通信模块设计
-
核心组件:通信芯片、天线、接口单元、电源单元;
-
工作原理:通过通信芯片实现与软件平台的协议对接,接收平台指令并传输至控制终端,同时将控制终端上传的数据传输至平台;支持自动重连功能;
-
选型补充:__________(根据自身硬件选型填写通信模块型号、厂商、通信方式等)。
4.3.2.4 辅助设备设计
-
电源设备:采用市电+电池双供电模式,确保供电稳定;电池采用锂电池,支持充电与低功耗保护;
-
部署支架:采用防水、防尘、抗干扰设计,适配室内外部署,便于安装与固定。
4.3.3 硬件接口设计
-
感知设备与控制终端接口:采用UART接口,用于数据传输,波特率:9600bps;
-
控制终端与通信模块接口:采用SPI接口,用于指令与数据传输;
-
电源接口:采用DC 5V接口,支持市电与电池接入;
-
扩展接口:预留I2C接口,用于后续新增设备扩展。
4.3.4 硬件协同设计
-
数据传输流程:感知设备采集数据→通过UART接口传输至控制终端→控制终端处理数据→通过SPI接口传输至通信模块→通信模块通过MQTT/TCP/HTTP协议上传至软件平台;
-
指令执行流程:软件平台下发指令→通信模块接收指令→通过SPI接口传输至控制终端→控制终端解析指令→控制执行单元执行操作→将执行结果反馈至软件平台;
-
异常处理:当硬件设备出现故障时,控制终端触发异常提醒,通过通信模块上传至软件平台,同时启动备用机制(如备用电源、本地控制),确保业务连续性。
4.4 概要设计评审
评审人:__________
评审意见:__________
评审日期:__________
第五部分:详细设计文档
本章节在概要设计基础上,对软件各模块、硬件各部件、数据库、接口等进行细化设计,明确具体实现逻辑、数据结构、流程细节、硬件电路与固件设计,为编码与硬件集成提供直接依据。
5.1 软件详细设计(独立模块)
5.1.1 设备接入模块详细设计
5.1.1.1 模块类设计(以Java/Spring为例)
| 类名 | 职责 | 关键方法 |
|---|---|---|
DeviceAuthService | 设备身份验证 | authenticate(deviceId, secret) |
MqttGateway | MQTT协议处理 | handleMqttMessage(topic, payload) |
TcpServerHandler | TCP长连接处理 | channelRead(ChannelHandlerContext, Object) |
HttpDeviceController | HTTP设备接入接口 | uploadData(@RequestBody DeviceData) |
DeviceStatusManager | 设备在线状态管理 | updateStatus(deviceId, status), heartbeat(deviceId) |
5.1.1.2 设备接入流程
- MQTT接入:设备连接Broker(EMQX/Mosquitto)→ 携带用户名/密码 → Broker回调
MqttGateway→ 验证设备身份 → 订阅系统主题 → 记录接入日志 → 更新在线状态。 - TCP接入:设备建立Socket连接 → 发送认证JSON(
{deviceId, secret})→ 服务端解析验证 → 维持长连接 → 心跳保活(每30秒)。 - HTTP接入:设备POST
/api/device/upload→ 携带X-Device-Id与X-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 数据采集流程
-
设备上报数据 → 模块接收(同步或异步消息队列Kafka/RabbitMQ)→ 格式校验 → 解析为标准JSON结构:
json
{ "deviceId": "xxx", "timestamp": 1700000000000, "data": {"temperature": 25.6, "humidity": 60} } -
异常检测:根据预设阈值(如温度>50℃)触发告警,写入告警表。
-
存储策略:高频采集数据(秒级)写入时序数据库(如InfluxDB/TimescaleDB);设备配置、元数据写入关系库(MySQL/PG)。
5.1.2.2 数据清理与备份
- 原始数据保存30天,自动转存到冷存储(对象存储)或删除。
- 每日凌晨2点执行数据备份(全量+增量),备份保留7天。
5.1.3 远程控制模块详细设计
5.1.3.1 控制指令下发流程
- 用户在Web端点击“关闭开关” → 前端调用
POST /api/control/send - 后端生成指令ID(UUID) → 记录到
control_log表,状态为pending - 根据设备协议类型:
- MQTT:发布到设备专属topic
device/{deviceId}/control - TCP:通过对应Channel写入指令JSON
- HTTP:调用设备提供的回调URL
- MQTT:发布到设备专属topic
- 设备执行后回复ACK → 模块更新指令状态为
succeeded/failed - 若10秒内未收到ACK,触发重试(最多3次),仍失败则状态置为
failed并告警。
5.1.3.2 批量控制设计
- 支持选择多个设备 → 后台并发调用单设备控制逻辑,使用线程池(最大10线程)。
- 记录批量任务ID,可查询每个设备的执行结果。
5.1.4 用户管理与权限模块详细设计
5.1.4.1 权限模型(RBAC)
- 表结构:
user、role、permission、user_role、role_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 |
| 主控MCU | ESP32-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_id | VARCHAR(32) PK | 设备唯一标识 |
| device_name | VARCHAR(64) | 设备名称 |
| protocol | ENUM('MQTT','TCP','HTTP') | 接入协议 |
| product_key | VARCHAR(32) | 产品型号 |
| secret | VARCHAR(64) | 设备密钥(加密存储) |
| status | TINYINT | 0-离线,1-在线 |
| last_active_time | DATETIME | 最后心跳时间 |
| created_time | DATETIME | 注册时间 |
5.3.1.2 用户表 user
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | INT AUTO PK | |
| username | VARCHAR(32) UNIQUE | |
| password | VARCHAR(128) | bcrypt加密 |
| role_id | INT | 关联角色表 |
| ... | ... |
5.3.1.3 指令日志表 control_log
| 字段 | 类型 | 说明 |
|---|---|---|
| command_id | VARCHAR(36) PK | |
| device_id | VARCHAR(32) | |
| command | TEXT | 指令内容 |
| status | VARCHAR(16) | pending/succeeded/failed |
| retry_count | INT | 重试次数 |
| create_time | DATETIME | |
| finish_time | DATETIME |
5.3.2 时序数据库设计(InfluxDB)
- 测量名:
device_data - Tag:
device_id,sensor_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个并发指令 | 响应时间≤1s | 95%指令在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-002 | SQL注入 | 在设备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 部署步骤(摘要)
- 安装Docker及docker-compose(或K8s)
- 拉取镜像:MySQL, InfluxDB, EMQX, Redis, Kafka, 后端服务镜像
- 配置环境变量:数据库连接、JWT密钥、MQTT地址
- 执行数据库初始化脚本(schema.sql,seed.sql)
- 启动所有容器,验证健康检查
- 配置Nginx反向代理与SSL证书(Let's Encrypt)
- 硬件设备配置:烧录固件,配置平台域名
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 硬件安装指南(以温湿度传感器为例)
- 打开包装,取出传感器主体和支架
- 使用附赠的Micro-USB线充电2小时(红灯充电,绿灯满电)
- 下载配网App或通过微信小程序,长按设备按键5秒进入配网模式
- 输入Wi-Fi密码,等待提示“配网成功”
- 登录平台查看设备是否在线
第九部分:项目验收文档
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 做缓冲
- 架构模式:
- 设备上报 -> MQTT Broker (如EMQX) -> Redis 队列/消息队列。
- 消费组 -> 批量写入 -> 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风格命名
五、选择指南
判断方法:
-
查看目标CPU规格:
bash
# 在目标板上执行 cat /proc/cpuinfo uname -m -
检查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/A8 | arm-linux-gnueabihf | 有VFP/NEON |
| Cortex-A9/A15 | arm-linux-gnueabihf | 性能最优 |
| ARMv8 64位 | aarch64-linux-gnu | 64位系统 |
| 未知平台 | 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
}
}
分析:
myObj是一个引用变量,它的值是一个指向堆中MyClass对象的地址。- 调用
modifyObject(myObj)时,传递的是myObj这个引用变量里存储的地址值的副本。所以形参obj和实参myObj指向同一个对象。 obj.value = 10;通过副本地址找到了原始对象,并修改了它的属性。所以外部的myObj.value也变成了10。obj = new MyClass();这一步是让形参obj这个副本指向了一个全新的对象。这与外部的myObj已经完全无关了。myObj仍然指向原来的对象。- 结论: 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) | 可以 | 高 | 标准做法。传递指针副本,通过指针修改原结构体。 |
核心记忆点:
- Java: 只有一种方式(值传递引用),效果是“半修改”(能改内容,不能改指向)。
- C++: 有三种方式。想要在函数内修改外部对象,优先使用引用传递(
&)。指针传递也可以,但语法更繁琐。值传递通常不用于需要修改的场景。 - C: 只有两种方式。必须使用指针传递(
*)来修改外部结构体。值传递只用于不需要修改且结构体很小的场景。
针对java:传参可以理解成指针的副本,myClass clazz = new myClass(), 将clazz参数传入方法。可以类比成,clazz是一个指针指向一个对象(new myClass()),传入时类比成新建了一个指针也只向new myClass()。所以就能修改属性。
针对java:基本数据类型,int,long,char之类的8种基本数据类型。采用的是真正的值传递
创建
-
检查现状 确认当前无Swap,并确保有足够磁盘空间。 swapon --show df -h /
-
创建Swap文件 创建大小为2GB的Swap文件。 fallocate -l 2G /swap
-
有些文件系统不支持fallocate指令使用以下命令 2048为2g dd if=/dev/zero of=/swap bs=1M count=2048 status=progress
-
设置权限 锁定文件权限,防止被随意读取。 chmod 600 /swap
-
格式化 将文件标记为交换空间格式。 mkswap /swap
-
立即启用 激活Swap。 swapon /swap
-
验证启用 检查Swap是否成功启用并查看大小。 swapon --show free -h
-
永久生效 配置系统启动时自动挂载此Swap。 echo '/swap none swap sw 0 0' | sudo tee -a /etc/fstab
-
设置交换积极性 echo 'vm.swappiness=60' | sudo tee -a /etc/sysctl.conf
-
确定交互积极性 sysctl -p
移除Swap文件步骤
-
查看当前状态 确认正在使用的Swap文件路径。 sudo swapon --show
-
停用Swap 关闭所有Swap交换空间。 sudo swapoff /swap
-
验证已停用 确认Swap已完全关闭。 sudo swapon --show free -h
-
删除自动挂载 从系统配置中移除Swap,使其开机不启动。 打开文件 vim /etc/fstab
删除这行 /swap none swap sw 0 0 -
删除Swap文件 永久删除磁盘上的文件以释放空间。 rm /swap
-
(可选)清理内核参数 移除之前调整的swappiness优化设置。 打开文件 vim /etc/sysctl.conf 修改vm.swappiness=0
-
重启生效 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 服务,右键选择 “重新启动” 。
- Linux :
检测生效
-
在非 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
这个脚本会自动执行:
git pull— 从 Gitee 拉取最新笔记- 编译
s.c— 生成侧边栏文件SUMMARY.md 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 接口,而非具体的实现类。
单一职责原则
一个类只应承担一个职责。 例如员工类可以有打卡方法,但不应包含计算工资的方法,后者应交由专门的薪资类处理。
接口隔离原则
接口应尽量做到“最小化”,即一个接口中只包含与其职责相关的抽象方法。 如果某个接口中存在与自身无关的方法,则应将这部分方法拆分到另一个接口中。
迪米特法则(最少知识原则)
一个对象应尽可能少地与其他对象发生交互,只与直接的朋友通信。 例如明星与经纪人之间的关系,外界只需与经纪人沟通,无需直接接触明星,从而降低耦合。
合成复用原则
优先使用组合(合成)或聚合,而不是继承来实现复用。 例如人有姓名、年龄等属性,同时拥有汽车。不应将汽车属性直接写入人类,而应将汽车独立为一个类,在人类中通过组合的方式引用汽车对象,使结构更清晰、复用性更强。

