imx6ull-驱动开发篇23——Linux 内核定时器实验

目录

实验程序编写

修改设备树文件

定时器驱动程序

timer.c

测试 timerApp.c

Makefile 文件

运行测试


实验程序编写

本讲实验,我们使用正点原子I.MX6U-ALPHA 开发板,通过linux内核定时器周期性的点亮和熄灭开发板上的 LED 灯, LED 灯的闪烁周期由内核定时器来设置,测试应用程序可以控制内核定时器周期。

修改设备树文件

和之前文章一致,GPIO子系统驱动LED,主要是以下几点:

  • 添加 pinctrl 节点
  • 添加 LED 设备节点
  • 检查 PIN 是否被其他外设使用

定时器驱动程序

timer.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>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>#define TIMER_CNT		1		/* 设备号个数 	*/
#define TIMER_NAME		"timer"	/* 名字 		*/
#define CLOSE_CMD 		(_IO(0XEF, 0x1))	/* 关闭定时器 */
#define OPEN_CMD		(_IO(0XEF, 0x2))	/* 打开定时器 */
#define SETPERIOD_CMD	(_IO(0XEF, 0x3))	/* 设置定时器周期命令 */
#define LEDON 			1		/* 开灯 */
#define LEDOFF 			0		/* 关灯 *//* timer设备结构体 */
struct timer_dev{dev_t devid;			/* 设备号 	 */struct cdev cdev;		/* cdev 	*/struct class *class;	/* 类 		*/struct device *device;	/* 设备 	 */int major;				/* 主设备号	  */int minor;				/* 次设备号   */struct device_node	*nd; /* 设备节点 */int led_gpio;			/* key所使用的GPIO编号		*/int timeperiod; 		/* 定时周期,单位为ms */struct timer_list timer;/* 定义一个定时器*/spinlock_t lock;		/* 定义自旋锁 */
};struct timer_dev timerdev;	/* timer设备 *//** @description	: 初始化LED灯IO,open函数打开驱动的时候* 				  初始化LED灯所使用的GPIO引脚。* @param 		: 无* @return 		: 无*/
static int led_init(void)
{int ret = 0;timerdev.nd = of_find_node_by_path("/gpioled");if (timerdev.nd== NULL) {return -EINVAL;}timerdev.led_gpio = of_get_named_gpio(timerdev.nd ,"led-gpio", 0);if (timerdev.led_gpio < 0) {printk("can't get led\r\n");return -EINVAL;}/* 初始化led所使用的IO */gpio_request(timerdev.led_gpio, "led");		/* 请求IO 	*/ret = gpio_direction_output(timerdev.led_gpio, 1);if(ret < 0) {printk("can't set gpio!\r\n");}return 0;
}/** @description		: 打开设备* @param - inode 	: 传递给驱动的inode* @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量* 					  一般在open的时候将private_data指向设备结构体。* @return 			: 0 成功;其他 失败*/
static int timer_open(struct inode *inode, struct file *filp)
{int ret = 0;filp->private_data = &timerdev;	/* 设置私有数据 */timerdev.timeperiod = 1000;		/* 默认周期为1s */ret = led_init();				/* 初始化LED IO */if (ret < 0) {return ret;}return 0;
}/** @description		: ioctl函数,* @param - filp 	: 要打开的设备文件(文件描述符)* @param - cmd 	: 应用程序发送过来的命令* @param - arg 	: 参数* @return 			: 0 成功;其他 失败*/
static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{struct timer_dev *dev =  (struct timer_dev *)filp->private_data;int timerperiod;unsigned long flags;switch (cmd) {case CLOSE_CMD:		/* 关闭定时器 */del_timer_sync(&dev->timer);break;case OPEN_CMD:		/* 打开定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));break;case SETPERIOD_CMD: /* 设置定时器周期 */spin_lock_irqsave(&dev->lock, flags);dev->timeperiod = arg;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));break;default:break;}return 0;
}/* 设备操作函数 */
static struct file_operations timer_fops = {.owner = THIS_MODULE,.open = timer_open,.unlocked_ioctl = timer_unlocked_ioctl,
};/* 定时器回调函数 */
void timer_function(unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)arg;static int sta = 1;int timerperiod;unsigned long flags;sta = !sta;		/* 每次都取反,实现LED灯反转 */gpio_set_value(dev->led_gpio, sta);/* 重启定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); }/** @description	: 驱动入口函数* @param 		: 无* @return 		: 无*/
static int __init timer_init(void)
{/* 初始化自旋锁 */spin_lock_init(&timerdev.lock);/* 注册字符设备驱动 *//* 1、创建设备号 */if (timerdev.major) {		/*  定义了设备号 */timerdev.devid = MKDEV(timerdev.major, 0);register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);} else {						/* 没有定义设备号 */alloc_chrdev_region(&timerdev.devid, 0, TIMER_CNT, TIMER_NAME);	/* 申请设备号 */timerdev.major = MAJOR(timerdev.devid);	/* 获取分配号的主设备号 */timerdev.minor = MINOR(timerdev.devid);	/* 获取分配号的次设备号 */}/* 2、初始化cdev */timerdev.cdev.owner = THIS_MODULE;cdev_init(&timerdev.cdev, &timer_fops);/* 3、添加一个cdev */cdev_add(&timerdev.cdev, timerdev.devid, TIMER_CNT);/* 4、创建类 */timerdev.class = class_create(THIS_MODULE, TIMER_NAME);if (IS_ERR(timerdev.class)) {return PTR_ERR(timerdev.class);}/* 5、创建设备 */timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);if (IS_ERR(timerdev.device)) {return PTR_ERR(timerdev.device);}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */init_timer(&timerdev.timer);timerdev.timer.function = timer_function;timerdev.timer.data = (unsigned long)&timerdev;return 0;
}/** @description	: 驱动出口函数* @param 		: 无* @return 		: 无*/
static void __exit timer_exit(void)
{gpio_set_value(timerdev.led_gpio, 1);	/* 卸载驱动的时候关闭LED */del_timer_sync(&timerdev.timer);		/* 删除timer */
#if 0del_timer(&timerdev.tiemr);
#endif/* 注销字符设备驱动 */gpio_free(timerdev.led_gpio);		cdev_del(&timerdev.cdev);/*  删除cdev */unregister_chrdev_region(timerdev.devid, TIMER_CNT); /* 注销设备号 */device_destroy(timerdev.class, timerdev.devid);class_destroy(timerdev.class);
}module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("huax");

关键代码分析如下:

定时器设备结构体timer_dev,定义了一个定时器成员变量 timer:

/* timer设备结构体 */
struct timer_dev{dev_t devid;			/* 设备号 	 */struct cdev cdev;		/* cdev 	*/struct class *class;	/* 类 		*/struct device *device;	/* 设备 	 */int major;				/* 主设备号	  */int minor;				/* 次设备号   */struct device_node	*nd; /* 设备节点 */int led_gpio;			/* key所使用的GPIO编号		*/int timeperiod; 		/* 定时周期,单位为ms */struct timer_list timer;/* 定义一个定时器*/spinlock_t lock;		/* 定义自旋锁 */
};

函数 timer_open,对应应用程序的 open 函数,应用程序调用 open 函数打开/dev/timer 驱动文件的时候此函数就会执行。

static int timer_open(struct inode *inode, struct file *filp)
{int ret = 0;filp->private_data = &timerdev;	/* 设置私有数据 */timerdev.timeperiod = 1000;		/* 默认周期为1s */ret = led_init();				/* 初始化LED IO */if (ret < 0) {return ret;}return 0;
}

此函数设置文件私有数据为 timerdev,并且初始化定时周期默认为 1 秒,最后调用 led_init 函数初始化 LED 所使用的 IO。

函数 timer_unlocked_ioctl,对应应用程序的 ioctl 函数,应用程序调用 ioctl函数向驱动发送控制信息,此函数响应并执行。

static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{struct timer_dev *dev =  (struct timer_dev *)filp->private_data;int timerperiod;unsigned long flags;switch (cmd) {case CLOSE_CMD:		/* 关闭定时器 */del_timer_sync(&dev->timer);break;case OPEN_CMD:		/* 打开定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(timerperiod));break;case SETPERIOD_CMD: /* 设置定时器周期 */spin_lock_irqsave(&dev->lock, flags);dev->timeperiod = arg;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(arg));break;default:break;}return 0;
}

此函数有三个参数: filp, cmd 和 arg,其中 :

  • filp是对应的设备文件,
  • cmd 是应用程序发送过来的命令信息,
  • arg 是应用程序发送过来的参数,

在本讲定时器实验里, arg 参数表示定时周期。

cmd 有三种命令:

  • CLOSE_CMD: 关闭定时器命令, 调用 del_timer_sync 函数关闭定时器。
  • OPEN_CMD:打开定时器命令,调用 mod_timer 函数打开定时器,定时周期为 timerdev 的timeperiod 成员变量,定时周期默认是 1 秒。
  • SETPERIOD_CMD:设置定时器周期命令,参数 arg 就是新的定时周期,设置 timerdev 的timeperiod 成员变量为 arg 所表示定时周期指。并且使用 mod_timer 重新打开定时器,使定时器以新的周期运行。

函数 timer_function,定时器服务函数,此函有一个参数 arg, 对应timerdev 的地址,这样通过 arg 参数就可以访问到设备结构体。

void timer_function(unsigned long arg)
{struct timer_dev *dev = (struct timer_dev *)arg;static int sta = 1;int timerperiod;unsigned long flags;sta = !sta;		/* 每次都取反,实现LED灯反转 */gpio_set_value(dev->led_gpio, sta);/* 重启定时器 */spin_lock_irqsave(&dev->lock, flags);timerperiod = dev->timeperiod;spin_unlock_irqrestore(&dev->lock, flags);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(dev->timeperiod)); }

当定时周期到了以后此函数就会被调用。在此函数中将 LED 灯的状态取反,实现 LED 灯闪烁的效果。

函数 timer_init,驱动入口函数里,初始化定时器,设置定时器的定时处理函数为 timer_function,另外设置要传递给 timer_function 函数的参数为 timerdev的地址。

	/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */init_timer(&timerdev.timer);timerdev.timer.function = timer_function;timerdev.timer.data = (unsigned long)&timerdev;

函数 timer_exit里,调用 del_timer_sync 函数删除定时器。

	del_timer_sync(&timerdev.timer);		/* 删除timer */
#if 0del_timer(&timerdev.tiemr);
#endif

测试 timerApp.c

测试 APP 我们要实现的内容如下:

  • 运行 APP 以后提示我们输入要测试的命令,输入 1 表示关闭定时器、输入 2 表示打开定时器,输入 3 设置定时器周期。
  • 如果要设置定时器周期的话,需要让用户输入要设置的周期值,单位为毫秒。

timerApp.c文件代码如下:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include "linux/ioctl.h"/* 命令值 */
#define CLOSE_CMD 		(_IO(0XEF, 0x1))	/* 关闭定时器 */
#define OPEN_CMD		(_IO(0XEF, 0x2))	/* 打开定时器 */
#define SETPERIOD_CMD	(_IO(0XEF, 0x3))	/* 设置定时器周期命令 *//** @description		: main主程序* @param - argc 	: argv数组元素个数* @param - argv 	: 具体参数* @return 			: 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int fd, ret;char *filename;unsigned int cmd;unsigned int arg;unsigned char str[100];if (argc != 2) {printf("Error Usage!\r\n");return -1;}filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0) {printf("Can't open file %s\r\n", filename);return -1;}while (1) {printf("Input CMD:");ret = scanf("%d", &cmd);if (ret != 1) {				/* 参数输入错误 */gets(str);				/* 防止卡死 */}if(cmd == 1)				/* 关闭LED灯 */cmd = CLOSE_CMD;else if(cmd == 2)			/* 打开LED灯 */cmd = OPEN_CMD;else if(cmd == 3) {cmd = SETPERIOD_CMD;	/* 设置周期值 */printf("Input Timer Period:");ret = scanf("%d", &arg);if (ret != 1) {			/* 参数输入错误 */gets(str);			/* 防止卡死 */}}ioctl(fd, cmd, arg);		/* 控制定时器的打开和关闭 */	}close(fd);
}

while(1)循环,让用户输入要测试的命令,然后通过 ioctl 函数发送给驱动程序。

如果是设置定时器周期命令 SETPERIOD_CMD,那么 ioctl 函数的 arg 参数就是用户输入的周期值。

Makefile 文件

makefile文件只需要修改 obj-m 变量的值,改为timer.o

KERNELDIR := /home/huax/linux/linux_test/linux-imx-rel_imx_4.1.15_2.1.0_gaCURRENT_PATH := $(shell pwd)
obj-m := timer.obuild: kernel_modules
kernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

运行测试

编译代码:

make -j32 //编译makefile文件
arm-linux-gnueabihf-gcc timerApp.c -o timerApp    //编译测试程序

编译成功以后,就会生成一个名为“timer.ko”的驱动模块文件,和timerApp 这个应用程序。

将编译出来的 timer.ko 和 timerApp 这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中,重启开发板。

进入到目录 lib/modules/4.1.15 中,输入如下命令加载 timer.ko 驱动模块:

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

驱动加载成功以后,输入命令来测试:

/timerApp /dev/timer

打印如下:

提示我们输入cmd指令。

输入“2”,打开定时器,此时 LED 灯就会以默认的 1 秒周期开始闪烁。

再输入“3”来设置定时周期,根据提示输入要设置的周期值:

输入“500”,表示设置定时器周期值为 500ms,设置好以后 LED 灯就会以 500ms 为间隔,开始闪烁。

最后可以通过输入“1”来关闭定时器。

卸载驱动的话输入如下命令:

rmmod timer.ko

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

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

相关文章

IPTV系统:开启视听与管理的全新篇章

在当今数字化飞速发展的时代&#xff0c;IPTV系统正以前所未有的姿态&#xff0c;重塑着我们的视听体验与管理模式。它不仅仅是一套技术系统&#xff0c;更是连接信息、沟通情感、提升效率的桥梁&#xff0c;为各个领域带来了全新的变革与发展机遇。从电视直播的角度来看&#…

PyTorch笔记9----------Cifar10图像分类

1.图像分类网络模型框架解读 分类网络的基本结构 数据加载模块&#xff1a;对训练数据加载数据重组&#xff1a;组合成网络需要的形式&#xff0c;例如预处理、增强、各种网络处理、loss函数计算优化器 数据加载模块 使用公开数据集&#xff1a;torchvision.datasets使用自定义…

飞凌OK3568开发板QT应用程序编译流程

飞凌OK3568开发板QT应用程序编译流程开发环境&#xff1a;ubuntu20.04&#xff08;主机&#xff09;、飞凌OK3568开发板一般在linux系统下开发用于ARM开发板的QT应用程序时&#xff0c;直接在主机上开发然后进行交叉编译即可&#xff0c;但有时候ARM开发板的厂家提供的SDK中可能…

飞算JavaAI合并项目实战:7天完成3年遗留系统重构

引言 企业数字化进程中&#xff0c;遗留系统改造始终是CIO面临的头号难题。某电商平台的实践数据显示&#xff1a;3年以上的Java项目平均存在47%的冗余代码&#xff0c;63%的架构设计不符合当前业务需求&#xff0c;进行系统性重构需要投入相当于原开发量200%的资源。传统&quo…

卫星速度增量和比冲及推力之间的关系

一、定义1.1.比冲&#xff08;Isp&#xff09;&#xff1a;比冲是衡量发动机性能的重要指标&#xff0c;反映了单位重量推进剂在发动机中产生的冲量&#xff0c;单位为米/秒&#xff08;m/s&#xff09;&#xff0c;代表燃料燃烧时喷流速度。这个单位与速度单位“米/秒”相同&a…

MATLAB绘制各种心形曲线

1.方程(1)心形线的经典隐函数方程为&#xff1a;(2)参数方程&#xff08;更平滑的心形&#xff09;&#xff1a;(3)极坐标心形线(4)参数方程&#xff08;3D心形&#xff09;(5)隐函数3D心形2. MATLAB代码clc;close all;clear all;warning off;%清除变量 rand(seed, 100); randn…

Django REST Framework视图

Django REST Framework (DRF) 视图类详解DRF 提供了丰富的视图类来构建 API&#xff0c;从基础到高级&#xff0c;满足不同复杂度的需求。以下是 DRF 的主要视图类及其使用场景&#xff1a;1. 基础视图类APIView所有 DRF 视图的基类&#xff0c;相当于 Django 的 View 类的增强…

Linux面试题及详细答案 120道(1-15)-- 基础概念

《前后端面试题》专栏集合了前后端各个知识模块的面试题&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

week1-[分支结构]中位数

week1-[分支结构]中位数 题目描述 给定 444 个正整数 a,b,c,da,b,c,da,b,c,d&#xff0c;输出它们的中位数&#xff0c;答案四舍五入保留 111 位小数。 输入格式 输入共 111 行 444 个正整数 a,b,c,da,b,c,da,b,c,d。 输出格式 输出共 111 行 111 个浮点数表示答案。 样例 #1 样…

[激光原理与应用-259]:理论 - 几何光学 - 平面镜的反射、平面透镜的折射、平面镜的反射成像、平面透镜的成像的规律

一、平面镜的反射规律平面镜的反射遵循镜面反射定律&#xff0c;即光线在光滑表面&#xff08;反射面平整度远大于波长&#xff09;发生反射时&#xff0c;满足以下条件&#xff1a;反射光线、入射光线与法线共面&#xff1a;反射光线、入射光线和法线&#xff08;垂直于反射面…

相机按键功能解析

相机按键功能解析佳能相机按键机身背面机身正面机身顶部机身侧面 佳能相机按键 机身背面取景器目镜&#xff1a;用于拍摄时观察相机形成的图像。实拍显示/视频拍摄按钮&#xff1a;按下即可开始拍摄或录制视频。光圈/曝光补偿键&#xff1a;调整光圈大小和曝光补偿&#xff0c;…

51单片机-驱动LED模块教程

本章思维导图&#xff1a; 51单片机驱动LED灯模块 LED灯元器件简介 LED&#xff08;Light Emitting Diode&#xff0c;发光二极管&#xff09; 是一种固态半导体器件&#xff0c;通过P-N结中电子与空穴复合直接将电能转化为光能。其核心结构由P型半导体&#xff08;空穴主导&a…

Git 完全手册:从入门到团队协作实战(2)

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《C修炼之路》、《Linux修炼&#xff1a;终端之内 洞悉真理…

c语言中堆和栈的区别

1.栈区(stack):由编译器自动分配释放&#xff0c;栈主要用于存储局部变量、函数参数、函数调用和返回信息等。其操作方式类似于数据结构中的栈。 2.堆区(heap):一般由程序员分配释放&#xff0c;若程序员不释放&#xff0c;则可能会引起内存泄漏。注堆和数据结构中的堆栈不一样…

华为实验WLAN 基础配置随练

业务vlan 20 192.168.20.x管理vlan 100 192.168.100.x步骤① 网络互通Core sw:vlan batch 20 100 dhcp enable int vlanif 20IP add 192.168.20.1 24dhcp select interfaceinterface GigabitEthernet0/0/1/2port link-type trunkport trunk pvid vlan 100port trunk allow-pas…

CMake 如何查找 Python2和Python3

问题 在一个CMakeLists.txt文件里面看到了下面的这句话 find_package(Python2 COMPONENTS Interpreter Development NumPy)这个好有趣啊&#xff0c;Python2也是一个C的库吗&#xff0c;也有Python2Config.cmake或者FindPython2.cmake? 回答 find_package(Python2 COMPONENTS …

心灵笔记:刻意练习

心灵笔记&#xff1a;刻意练习提要 所有人都以为“杰出”源于“天赋”&#xff0c;而“天才”却说&#xff1a;我的成就源于“正确的练习”&#xff01; 定义&#xff1a;刻意练习是一种有目的、有方法、能带来能力持续提升的结构化训练方式&#xff0c;它并非简单的重复劳动&a…

langchain入门笔记03:使用fastapi部署本地大模型后端接口,优化局域网内的问答响应速度

文章目录前言一、fastapi的简单入门1&#xff1a;安装必要的包&#xff08;python3.11&#xff09;&#xff1a;2&#xff1a;快速搭建一个fastapi&#xff1a;二、提升问答的响应速度1. fastapi部署后端接口&#xff0c;在局域网内访问的方法2. 局域网内的测试&#xff1a;“未…

【CDA 新一级】学习笔记第1篇:数据分析的时代背景

作者&#xff1a;CDA持证人 张九领我们要学习数据分析&#xff0c;就要从当前时代的数据特点&#xff0c;找到在时代特点下企业需要数据分析的痛点&#xff0c;然后理解数据分析在企业中的作用。当前时代&#xff0c;数据分析的特征是哪些呢&#xff1f;我们用VUCA来概括数据分…

Vite 为什么比 Webpack 快?原理深度分析

Hi&#xff0c;我是布兰妮甜 &#xff01;在现代前端开发中&#xff0c;构建工具的性能直接影响开发体验和生产力。Webpack 作为传统打包工具的代表&#xff0c;长期以来主导着前端构建领域&#xff0c;而 Vite 作为新一代的前端构建工具&#xff0c;凭借其出色的开发服务器启动…