Linux学习第44天:Linux 多点电容触摸屏实验(二):难忘记第一次牵你手的温存

news2024/9/23 19:21:38

Linux版本号4.1.15   芯片I.MX6ULL                                    大叔学Linux    品人间百味  思文短情长 


        本章的思维导图内容如下:

二、硬件原理图分析

三、实验程序编写

1、修改设备树

1)、添加FT5426所使用的IO

        一个复位 IO、一个中断 IO、 I2C2 的 SCL 和 SDA。

        检查有没有被占用。

2)、添加FT5426节点

 &i2c2 {
2 clock_frequency = <100000>;
3 pinctrl-names = "default";
4 pinctrl-0 = <&pinctrl_i2c2>;
5 status = "okay";
6 7
/****************************/
8 /* 省略掉其他的设备节点 */
9 /****************************/
10
11 /* zuozhongkai FT5406/FT5426 */
12 ft5426: ft5426@38 {
13 compatible = "edt,edt-ft5426";
14 reg = <0x38>;
15 pinctrl-names = "default";
16 pinctrl-0 = <&pinctrl_tsc
17 &pinctrl_tsc_reset >;
18 interrupt-parent = <&gpio1>;
19 interrupts = <9 0>;
20 reset-gpios = <&gpio5 9 GPIO_ACTIVE_LOW>;
21 interrupt-gpios = <&gpio1 9 GPIO_ACTIVE_LOW>;
22 };
23 };

2、编写多点电容触摸驱动

1 #include <linux/module.h>
2 #include <linux/ratelimit.h>
......
15 #include <linux/i2c.h>
16 /***************************************************************
17 Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
18 文件名 : ft5x06.c
19 作者 : 左忠凯
20 版本 : V1.0
21 描述 : FT5X06,包括 FT5206、 FT5426 等触摸屏驱动程序
22 其他 : 无
23 论坛 : www.openedv.com
24 日志 : 初版 V1.0 2019/12/23 左忠凯创建个
25 ***************************************************************/
26
27 #define MAX_SUPPORT_POINTS 5 /* 5 点触摸 */
28 #define TOUCH_EVENT_DOWN 0x00 /* 按下 */
29 #define TOUCH_EVENT_UP 0x01 /* 抬起 */
30 #define TOUCH_EVENT_ON 0x02 /* 接触 */
31 #define TOUCH_EVENT_RESERVED 0x03 /* 保留 */
32
33 /* FT5X06 寄存器相关宏定义 */
34 #define FT5X06_TD_STATUS_REG 0X02 /* 状态寄存器地址 */
35 #define FT5x06_DEVICE_MODE_REG 0X00 /* 模式寄存器 */
36 #define FT5426_IDG_MODE_REG 0XA4 /* 中断模式 */
37 #define FT5X06_READLEN 29 /* 要读取的寄存器个数 */
38
39 struct ft5x06_dev {
40 struct device_node *nd; /* 设备节点 */
41 int irq_pin,reset_pin; /* 中断和复位 IO */
42 int irqnum; /* 中断号 */
43 void *private_data; /* 私有数据 */
44 struct input_dev *input; /* input 结构体 */
45 struct i2c_client *client; /* I2C 客户端 */
46 };
47
48 static struct ft5x06_dev ft5x06;
49
50 /*
51 * @description : 复位 FT5X06
52 * @param - client : 要操作的 i2c
53 * @param – multidev : 自定义的 multitouch 设备
54 * @return : 0,成功;其他负值,失败
55 */
56 static int ft5x06_ts_reset(struct i2c_client *client,
struct ft5x06_dev *dev)
57 {
58 int ret = 0;
59
60 if (gpio_is_valid(dev->reset_pin)) { /* 检查 IO 是否有效 */
61 /* 申请复位 IO,并且默认输出低电平 */
62 ret = devm_gpio_request_one(&client->dev,
63 dev->reset_pin, GPIOF_OUT_INIT_LOW,
64 "edt-ft5x06 reset");
65 if (ret) {
66 return ret;
67 }
68 msleep(5);
69 gpio_set_value(dev->reset_pin, 1); /* 输出高电平,停止复位 */
70 msleep(300);
71 }
72 return 0;
73 }
74
75 /*
76 * @description : 从 FT5X06 读取多个寄存器数据
77 * @param – dev : ft5x06 设备
78 * @param – reg : 要读取的寄存器首地址
79 * @param – val : 读取到的数据
80 * @param – len : 要读取的数据长度
81 * @return : 操作结果
82 */
83 static int ft5x06_read_regs(struct ft5x06_dev *dev, u8 reg,
void *val, int len)
84 {
85 int ret;
86 struct i2c_msg msg[2];
87 struct i2c_client *client = (struct i2c_client *)dev->client;
......
101 ret = i2c_transfer(client->adapter, msg, 2);
102 if(ret == 2) {
103 ret = 0;
104 } else {
105 ret = -EREMOTEIO;
106 }
107 return ret;
108 }
109
110 /*
111 * @description : 向 ft5x06 多个寄存器写入数据
112 * @param – dev : ft5x06 设备
113 * @param – reg : 要写入的寄存器首地址
114 * @param – val : 要写入的数据缓冲区
115 * @param – len : 要写入的数据长度
116 * @return : 操作结果
117 */
118 static s32 ft5x06_write_regs(struct ft5x06_dev *dev, u8 reg,
u8 *buf, u8 len)
119 {
120 u8 b[256];
121 struct i2c_msg msg;
122 struct i2c_client *client = (struct i2c_client *)dev->client;
123
124 b[0] = reg; /* 寄存器首地址 */
125 memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组 b 里面 */
126
127 msg.addr = client->addr; /* ft5x06 地址 */
128 msg.flags = 0; /* 标记为写数据 */
129
130 msg.buf = b; /* 要写入的数据缓冲区 */
131 msg.len = len + 1; /* 要写入的数据长度 */
132
133 return i2c_transfer(client->adapter, &msg, 1);
134 }
135
136 /*
137 * @description : 向 ft5x06 指定寄存器写入指定的值,写一个寄存器
138 * @param – dev : ft5x06 设备
139 * @param – reg : 要写的寄存器
140 * @param – data : 要写入的值
141 * @return : 无
142 */
143 static void ft5x06_write_reg(struct ft5x06_dev *dev, u8 reg,
u8 data)
144 {
145 u8 buf = 0;
146 buf = data;
147 ft5x06_write_regs(dev, reg, &buf, 1);
148 }
149
150 /*
151 * @description : FT5X06 中断服务函数
152 * @param - irq : 中断号
153 * @param - dev_id : 设备结构。
154 * @return : 中断执行结果
155 */
156 static irqreturn_t ft5x06_handler(int irq, void *dev_id)
157 {
158 struct ft5x06_dev *multidata = dev_id;
159
160 u8 rdbuf[29];
161 int i, type, x, y, id;
162 int offset, tplen;
163 int ret;
164 bool down;
165
166 offset = 1; /* 偏移 1,也就是 0X02+1=0x03,从 0X03 开始是触摸值 */
167 tplen = 6; /* 一个触摸点有 6 个寄存器来保存触摸值 */
168
169 memset(rdbuf, 0, sizeof(rdbuf)); /* 清除 */
170
171 /* 读取 FT5X06 触摸点坐标从 0X02 寄存器开始,连续读取 29 个寄存器 */
172 ret = ft5x06_read_regs(multidata, FT5X06_TD_STATUS_REG,
rdbuf, FT5X06_READLEN);
173 if (ret) {
174 goto fail;
175 }
176
177 /* 上报每一个触摸点坐标 */
178 for (i = 0; i < MAX_SUPPORT_POINTS; i++) {
179 u8 *buf = &rdbuf[i * tplen + offset];
180
181 /* 以第一个触摸点为例,寄存器 TOUCH1_XH(地址 0X03),各位描述如下:
182 * bit7:6 Event flag 0:按下 1:释放 2:接触 3:没有事件
183 * bit5:4 保留
184 * bit3:0 X 轴触摸点的 11~8 位。
185 */
186 type = buf[0] >> 6; /* 获取触摸类型 */
187 if (type == TOUCH_EVENT_RESERVED)
188 continue;
189
190 /* 我们所使用的触摸屏和 FT5X06 是反过来的 */
191 x = ((buf[2] << 8) | buf[3]) & 0x0fff;
192 y = ((buf[0] << 8) | buf[1]) & 0x0fff;
193
194 /* 以第一个触摸点为例,寄存器 TOUCH1_YH(地址 0X05),各位描述如下:
195 * bit7:4 Touch ID 触摸 ID,表示是哪个触摸点
196 * bit3:0 Y 轴触摸点的 11~8 位。
197 */
198 id = (buf[2] >> 4) & 0x0f;
199 down = type != TOUCH_EVENT_UP;
200
201 input_mt_slot(multidata->input, id);
202 input_mt_report_slot_state(multidata->input, MT_TOOL_FINGER,
down);
203
204 if (!down)
205 continue;
206
207 input_report_abs(multidata->input, ABS_MT_POSITION_X, x);
208 input_report_abs(multidata->input, ABS_MT_POSITION_Y, y);
209 }
210
211 input_mt_report_pointer_emulation(multidata->input, true);
212 input_sync(multidata->input);
213
214 fail:
215 return IRQ_HANDLED;
216
217 }
218
219 /*
220 * @description : FT5x06 中断初始化
221 * @param - client : 要操作的 i2c
222 * @param – multidev : 自定义的 multitouch 设备
223 * @return : 0,成功;其他负值,失败
224 */
225 static int ft5x06_ts_irq(struct i2c_client *client,
struct ft5x06_dev *dev)
226 {
227 int ret = 0;
228
229 /* 1,申请中断 GPIO */
230 if (gpio_is_valid(dev->irq_pin)) {
231 ret = devm_gpio_request_one(&client->dev, dev->irq_pin,
232 GPIOF_IN, "edt-ft5x06 irq");
233 if (ret) {
234 dev_err(&client->dev,
235 "Failed to request GPIO %d, error %d\n",
236 dev->irq_pin, ret);
237 return ret;
238 }
239 }
240
241 /* 2,申请中断,client->irq 就是 IO 中断, */
242 ret = devm_request_threaded_irq(&client->dev, client->irq, NULL,
243 ft5x06_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
244 client->name, &ft5x06);
245 if (ret) {
246 dev_err(&client->dev, "Unable to request
touchscreen IRQ.\n");
247 return ret;
248 }
249
250 return 0;
251 }
252
253 /*
254 * @description : i2c 驱动的 probe 函数,当驱动与
255 * 设备匹配以后此函数就会执行
256 * @param - client : i2c 设备
257 * @param - id : i2c 设备 ID
258 * @return : 0,成功;其他负值,失败
259 */
260 static int ft5x06_ts_probe(struct i2c_client *client,
const struct i2c_device_id *id)
261 {
262 int ret = 0;
263
264 ft5x06.client = client;
265
266 /* 1,获取设备树中的中断和复位引脚 */
267 ft5x06.irq_pin = of_get_named_gpio(client->dev.of_node,
"interrupt-gpios", 0);
268 ft5x06.reset_pin = of_get_named_gpio(client->dev.of_node,
"reset-gpios", 0);
269
270 /* 2,复位 FT5x06 */
271 ret = ft5x06_ts_reset(client, &ft5x06);
272 if(ret < 0) {
273 goto fail;
274 }
275
276 /* 3,初始化中断 */
277 ret = ft5x06_ts_irq(client, &ft5x06);
278 if(ret < 0) {
279 goto fail;
280 }
281
282 /* 4,初始化 FT5X06 */
283 ft5x06_write_reg(&ft5x06, FT5x06_DEVICE_MODE_REG, 0);
284 ft5x06_write_reg(&ft5x06, FT5426_IDG_MODE_REG, 1);
285
286 /* 5, input 设备注册 */
287 ft5x06.input = devm_input_allocate_device(&client->dev);
288 if (!ft5x06.input) {
289 ret = -ENOMEM;
290 goto fail;
291 }
292 ft5x06.input->name = client->name;
293 ft5x06.input->id.bustype = BUS_I2C;
294 ft5x06.input->dev.parent = &client->dev;
295
296 __set_bit(EV_KEY, ft5x06.input->evbit);
297 __set_bit(EV_ABS, ft5x06.input->evbit);
298 __set_bit(BTN_TOUCH, ft5x06.input->keybit);
299
300 input_set_abs_params(ft5x06.input, ABS_X, 0, 1024, 0, 0);
301 input_set_abs_params(ft5x06.input, ABS_Y, 0, 600, 0, 0);
302 input_set_abs_params(ft5x06.input, ABS_MT_POSITION_X,
0, 1024, 0, 0);
303 input_set_abs_params(ft5x06.input, ABS_MT_POSITION_Y,
0, 600, 0, 0);
304 ret = input_mt_init_slots(ft5x06.input, MAX_SUPPORT_POINTS, 0);
305 if (ret) {
306 goto fail;
307 }
308
309 ret = input_register_device(ft5x06.input);
310 if (ret)
311 goto fail;
312
313 return 0;
314
315 fail:
316 return ret;
317 }
318
319 /*
320 * @description : i2c 驱动的 remove 函数,移除 i2c 驱动的时候此函数会执行
321 * @param – client : i2c 设备
322 * @return : 0,成功;其他负值,失败
323 */
324 static int ft5x06_ts_remove(struct i2c_client *client)
325 {
326 /* 释放 input_dev */
327 input_unregister_device(ft5x06.input);
328 return 0;
329 }
330
331
332 /*
333 * 传统驱动匹配表
334 */
335 static const struct i2c_device_id ft5x06_ts_id[] = {
336 { "edt-ft5206", 0, },
337 { "edt-ft5426", 0, },
338 { /* sentinel */ }
339 };
340
341 /*
342 * 设备树匹配表
343 */
344 static const struct of_device_id ft5x06_of_match[] = {
345 { .compatible = "edt,edt-ft5206", },
346 { .compatible = "edt,edt-ft5426", },
347 { /* sentinel */ }
348 };
349
350 /* i2c 驱动结构体 */
351 static struct i2c_driver ft5x06_ts_driver = {
352 .driver = {
353 .owner = THIS_MODULE,
354 .name = "edt_ft5x06",
355 .of_match_table = of_match_ptr(ft5x06_of_match),
356 },
357 .id_table = ft5x06_ts_id,
358 .probe = ft5x06_ts_probe,
359 .remove = ft5x06_ts_remove,
360 };
361
362 /*
363 * @description : 驱动入口函数
364 * @param : 无
365 * @return : 无
366 */
367 static int __init ft5x06_init(void)
368 {
369 int ret = 0;
370
371 ret = i2c_add_driver(&ft5x06_ts_driver);
372
373 return ret;
374 }
375
376 /*
377 * @description : 驱动出口函数
378 * @param : 无
379 * @return : 无
380 */
381 static void __exit ft5x06_exit(void)
382 {
383 i2c_del_driver(&ft5x06_ts_driver);
384 }
385
386 module_init(ft5x06_init);
387 module_exit(ft5x06_exit);
388 MODULE_LICENSE("GPL");
389 MODULE_AUTHOR("zuozhongkai");

        代码挺多的,就不贴了。

四、运行测试

1、编译驱动程序

1 KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imxrel_imx_4.1.15_2.1.0_ga_alientek
......
4 obj-m := ft5x06.o
......
11 clean:
12 $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

2、运行测试

depmod //第一次加载驱动的时候需要运行此命令
modprobe ft5x06.ko //加载驱动模块

        生成eventx触摸文件,触摸屏幕,会看见文件上报信息内容变化。

3、将驱动添加到内核中

1)、将驱动文件放到合适的位置

2)、修改对应的Makefile

        最后一行,添加一行:

obj-y += ft5x06.o

        修改完成以后重新编译 linux 内核,然后用新的 zImage 启动开发板。

hexdump /dev/input/event1 //查看触摸屏原始数据上报信息

        查看上报数据内容,检验触屏驱动正确性。

以下内容将在下节更新:

五、tslib移植与使用

1、tslib移植

1)、获取tslib源码

2)、修改tslib源码所属用户

3)、ubuntu工具安装

4)、编译tslib

5)、配置tslib

6)、tslib测试

六、使用内核自带的驱动

1、修改edt-ft5x06.c

2、使能内核自带的FT5X06驱动

3、修改设备树

七、4.3.寸屏触摸驱动实验

1、修改设备树pinctrl_tsc节点内容

2、在设备树的i2c2节点下添加tgt9147子节点

3、修改设备树的lcdif节点

4、编译GT9147驱动文件

八、总结


本文为参考正点原子开发板配套教程整理而得,仅用于学习交流使用,不得用于商业用途。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1237299.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想录算法训练营第四十二天【动态规划part04】 | 01背包、416. 分割等和子集

01背包问题 题目链接&#xff1a; 题目页面 求解思路&#xff1a; 确定dp数组及其下标含义&#xff1a;dp[i][j] 表示从下标为 [0] 到 [i] 的物品里任意选取&#xff0c;放进容量为j的背包&#xff0c;此时的价值总和最大值确定递推公式&#xff1a; 不放物品i&#xff0c;…

常见树种(贵州省):006栎类

摘要&#xff1a;本专栏树种介绍图片来源于PPBC中国植物图像库&#xff08;下附网址&#xff09;&#xff0c;本文整理仅做交流学习使用&#xff0c;同时便于查找&#xff0c;如有侵权请联系删除。 图片网址&#xff1a;PPBC中国植物图像库——最大的植物分类图片库 一、麻栎 …

suricata识别菜刀流量

一、捕获菜刀流量 payload特征&#xff1a; PHP: <?php eval($_POST[caidao]);?> ​ ASP: <%eval request(“caidao”)%> ​ ASP.NET: <% Page Language“Jscript”%><%eval(Request.Item[“caidao”],“unsafe”);%>数据包流量特征&#xff1a; …

第二证券:大盘仍有向上修复空间

大盘周二冲高轰动&#xff0c;上档均线压力显着。沪指当日小幅低开后快速反弹&#xff0c;盘中指数打破60日均线后不坚决加大。跟着午后抛压显着加重&#xff0c;指数回落并在收盘前翻绿。深成指全日走势同沪指相仿&#xff0c;跌幅高于沪指。值得一提的是&#xff0c;大盘自8月…

python数据结构与算法-13_高级排序算法-分治法

分治法 (Divide and Conquer) 很多有用的算法结构上是递归的&#xff0c;为了解决一个特定问题&#xff0c;算法一次或者多次递归调用其自身以解决若干子问题。 这些算法典型地遵循分治法的思想&#xff1a;将原问题分解为几个规模较小但是类似于原问题的子问题&#xff0c;递…

C盘变红怎么办?一个快速解决C盘快满的方法

前情提要 通常解决C盘快满的方法是&#xff1a; 找到C盘—右击选择“属性”—选择“详细信息”—卸载不常用的软件或者清除临时文件 缺点&#xff1a;成效甚微 今日重点 1.背景知识&#xff1a;微信是我们日常工作和生活都离不开的工具&#xff0c;我们每天使用微信会产生大量…

系列四、ThreadLocal的工作原理

一、内存结构图 二、工作原理 &#xff08;1&#xff09;Thread有一个类型为ThreadLocal.ThreadLocalMap threadLocals 的实例变量&#xff0c;即每个线程都有一个属于自己的ThreadLocalMap&#xff1b; &#xff08;2&#xff09;ThreadLocalMap内部维护着Entry数组&#xff0…

用合成数据训练语义分割模型【裂缝检测】

最近&#xff0c;我们推出了合成裂缝分割数据集&#xff0c;在本文中&#xff0c;我们将深入探讨应用于合成数据生成过程的改进和启发式方法。 阅读完这篇文章后&#xff0c;你将了解我们如何设法创建一个数据集&#xff0c;该数据集可以像使用真实数据一样高效地训练模型。 在…

干货分享:本地生活服务商入驻申请需要哪些条件?附更快捷的方法!

在数字化时代&#xff0c;本地服务市场已经成为各大平台争夺的重要阵地。抖音不仅在短视频市场占有一席之地&#xff0c;同时也在如火如荼的开发着本地服务市场&#xff0c;相继支付宝、视频号也推出了本地生活服务商模式。本文将介绍抖音本地生活服务商的申请条件&#xff0c;…

Django ORM 执行复杂查询的技术与实践

概要 Django ORM&#xff08;Object-Relational Mapping&#xff09;是 Django 框架的核心组件之一&#xff0c;提供了一种高效、直观的方式来处理数据库操作。尽管简单查询在 Django ORM 中相对容易实现&#xff0c;但在面对复杂的数据请求时&#xff0c;需要更深入的了解和技…

申银万国期货通过ZStack Cube信创超融合一体机打造金融信创平台

信创是数字中国建设的重要组成部分&#xff0c;也是数字经济发展的关键推动力量。作为云基础软件企业&#xff0c;云轴科技ZStack产品矩阵全面覆盖数据中心云基础设施&#xff0c;ZStack信创云首批通过可信云《一云多芯IaaS平台能力要求》先进级&#xff0c;是其中唯一兼容四种…

HUAWEI华为MateBook X Pro 2022 12代酷睿版(MRGF-16)笔记本电脑原装出厂Windows11系统工厂模式含F10还原

链接&#xff1a;https://pan.baidu.com/s/1ZI5mR6SOgFzMljbMym7u3A?pwdl2cu 提取码&#xff1a;l2cu 华为原厂Windows11系统工厂包&#xff0c;带F10一键智能还原恢复功能。 自带指纹、面部识别、声卡、网卡、显卡、蓝牙等所有驱动、出厂主题壁纸、Office办公软件、华为…

早安,朋友!每天问候语祝你天天好心情,事事都顺意

1、今天的风儿轻柔无比&#xff0c;今天的花儿香飘万里&#xff1b;今天的鸟儿十分欢喜&#xff0c;今天的云儿满载笑意&#xff1b;今天的事儿万分顺利&#xff0c;今天的人儿如此甜蜜&#xff0c;所有美好的一切同我的早安连在一起&#xff0c;祝你天天好心情&#xff0c;事事…

【C++ 学习 ㊴】- 详解 C++ 的 I/O 流

目录 一、C 的 I/O 流 二、C 的标准 I/O 流 三、C 的文件 I/O 流 一、C 的 I/O 流 C 语言有一套完成数据读写&#xff08;I/O&#xff09;的解决方案&#xff1a; 使用 scanf()、gets() 等函数从键盘读取数据&#xff0c;使用 printf()、puts() 等函数向屏幕输出数据&#…

未履行数据保护义务造成数据泄露,某大药房被罚110万

近日&#xff0c;温州网安部门发现温州某大药房销售数据在暗网售卖&#xff0c;侵犯了公民个人信息&#xff1b;同时发现该大药房未履行数据保护义务&#xff0c;造成了数据泄露&#xff0c;由此对售卖数据的大药房数据分析师采取刑事强制措施&#xff0c;并对该公司处罚款110万…

快速选择算法

前言 本文将会向你介绍什么是快速选择算法&#xff0c;&#xff08;用两道例题来讲解&#xff09;算法原理是什么 引入 快速选择算法和快速排序算法都是基于分治思想的算法&#xff0c;它们的基本原理是类似的&#xff0c;都是通过将数组分成两部分&#xff0c;然后递归地处理…

手写promis(2)-- 链式编程篇

目录 链式编程 处理异常 和普通内容 链式编程---处理返回promise 链式编程---处理重复引用 链式编程--rejected 链式编程--处理padding状态 链式编程 处理异常 和普通内容 1.返回promise实例&#xff1a;在原本then方法里边创建新promise2.获取返回值&#xff1a;把原本…

jQuery实现横版手风琴效果

一、实现效果 当鼠标滑过方块的时候&#xff0c;方块的状态就会发生如下图所示的变化&#xff0c;同理当鼠标滑到其他的方块也会发生同样的效果&#xff0c;不仅大小会改变同时方块的颜色也会跟着发生变化&#xff1a; 二、代码实现 <!DOCTYPE html> <html><h…

如何通过提升客户体验带来更大的增长、更好的客户留存率?

客户期望的转变 在一个日益数字化的世界里&#xff0c;有必要采取以客户为中心的思维方式。因为客户与企业互动的方式有很多是在数字空间发生的&#xff0c;客户的需求和模式已经转变。 这种情况已经酝酿了几年&#xff0c;但在2020年才打开闸门。随着疫情的爆发&#xff0c;企…