RK3568驱动指南|第十一篇 pinctrl 子系统-第126章 通过pinctrl状态设置引脚复用实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。


【公众号】迅为电子

【粉丝群】824412014(加群获取驱动文档+例程)

【视频观看】嵌入式学习之Linux驱动(第十一篇 pinctrl 子系统_全新升级)_基于RK3568

【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板


第126章 通过pinctrl状态设置引脚复用实验

在上一个小节中讲解了add_setting函数,关于他的上层函数create_pinctrl也就讲解完成了,然后继续分析pinctrl_bind_pins函数,pinctrl_bind_pins函数内容如下所示:

int pinctrl_bind_pins(struct device *dev)
{int ret;// 检查设备是否重用了节点if (dev->of_node_reused)return 0;// 为设备的引脚分配内存空间dev->pins = devm_kzalloc(dev, sizeof(*(dev->pins)), GFP_KERNEL);if (!dev->pins)return -ENOMEM;// 获取设备的 pinctrl 句柄dev->pins->p = devm_pinctrl_get(dev);if (IS_ERR(dev->pins->p)) {dev_dbg(dev, "没有 pinctrl 句柄\n");ret = PTR_ERR(dev->pins->p);goto cleanup_alloc;}// 查找设备的默认 pinctrl 状态dev->pins->default_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_DEFAULT);if (IS_ERR(dev->pins->default_state)) {dev_dbg(dev, "没有默认的 pinctrl 状态\n");ret = 0;goto cleanup_get;}// 查找设备的初始化 pinctrl 状态dev->pins->init_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_INIT);if (IS_ERR(dev->pins->init_state)) {/* 不提供此状态是完全合法的 */dev_dbg(dev, "没有初始化的 pinctrl 状态\n");// 选择默认的 pinctrl 状态ret = pinctrl_select_state(dev->pins->p, dev->pins->default_state);} else {// 选择初始化的 pinctrl 状态ret = pinctrl_select_state(dev->pins->p, dev->pins->init_state);}if (ret) {dev_dbg(dev, "无法激活初始的 pinctrl 状态\n");goto cleanup_get;}#ifdef CONFIG_PM/** 如果启用了电源管理,我们还会寻找可选的睡眠和空闲的引脚状态,其语义在* <linux/pinctrl/pinctrl-state.h> 中定义*/dev->pins->sleep_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_SLEEP);if (IS_ERR(dev->pins->sleep_state))/* 不提供此状态是完全合法的 */dev_dbg(dev, "没有睡眠的 pinctrl 状态\n");dev->pins->idle_state = pinctrl_lookup_state(dev->pins->p, PINCTRL_STATE_IDLE);if (IS_ERR(dev->pins->idle_state))/* 不提供此状态是完全合法的 */dev_dbg(dev, "没有空闲的 pinctrl 状态\n");
#endifreturn 0;/** 如果对于此设备没有找到 pinctrl 句柄或默认状态,* 让我们明确释放设备中的引脚容器,因为保留它没有意义。*/
cleanup_get:devm_pinctrl_put(dev->pins->p);
cleanup_alloc:devm_kfree(dev, dev->pins);dev->pins = NULL;/* 返回延迟 */if (ret == -EPROBE_DEFER)return ret;/* 返回严重错误 */if (ret == -EINVAL)return ret;/* 我们忽略诸如 -ENOENT 的错误,表示没有 pinctrl 状态 */return 0;
}

前面的小节都只是对pinctrl_bind_pins函数的第15行获取设备的pinctrl句柄所使用的 devm_pinctrl_get 函数进行的讲解,接下来将继续对后面重要的内容进行讲解。

  1. 第22-28行:查找设备的默认pinctrl状态。使用 pinctrl_lookup_state 函数通过 dev->pins->p 和 PINCTRL_STATE_DEFAULT 参数查找设备的默认pinctrl状态,并将其赋值给 dev->pins->default_state。如果查找失败,函数会打印一条调试信息,并将返回值设置为0,表示继续执行。

第23行调用了pinctrl_lookup_state函数来查找设备的pinctrl状态,该函数定义在内核源码目录下的“drivers/pinctrl/core.c”文件中,具体内容如下所示:

struct pinctrl_state *pinctrl_lookup_state(struct pinctrl *p,const char *name)
{struct pinctrl_state *state;// 在状态链表中查找指定名称的状态对象state = find_state(p, name);if (!state) {if (pinctrl_dummy_state) {/* 创建虚拟状态 */dev_dbg(p->dev, "使用 pinctrl 虚拟状态(%s)\n",name);// 如果找不到 指定的状态对象,并且存在虚拟状态,则创建一个虚拟状态对象state = create_state(p, name);} else// 如果找不到指定的状态对象,并且不存在虚拟状态,则返回错误指针 -ENODEVstate = ERR_PTR(-ENODEV);}return state;
}

(2)第30-41行:查找设备的初始化pinctrl状态。使用 pinctrl_lookup_state 函数通过 dev->pins->p 和 PINCTRL_STATE_INIT 参数查找设备的初始化pinctrl状态,并将其赋值给 dev->pins->init_state。如果查找失败,函数会打印一条调试信息。如果找不到初始化状态,会选择默认的 pinctrl 状态,并将返回值设置为 pinctrl_select_state 函数的返回值。

(3)第48-56行:如果配置了电源管理(CONFIG_PM 宏定义),则会继续执行以下步骤。首先,查找设备的睡眠pinctrl状态。使用 pinctrl_lookup_state 函数通过 dev->pins->p 和 PINCTRL_STATE_SLEEP 参数查找设备的睡眠pinctrl状态,并将其赋值给 dev->pins->sleep_state。如果查找失败,函数会打印一条调试信息。

(4)第58-64行:查找设备的空闲pinctrl状态。使用pinctrl_lookup_state 函数通过 dev->pins->p 和 PINCTRL_STATE_IDLE 参数查找设备的空闲pinctrl状态,并将其赋值给 dev->pins->idle_state。如果查找失败,函数会打印一条调试信息。函数返回0,表示引脚绑定成功。

第37行和第40行会使用 pinctrl_select_state 选择并切换到指定的pinctrl_state(引脚控制状态),该函数定义在内核源码目录下的“drivers/pinctrl/core.c”文件中,具体内容如下所示:

int pinctrl_select_state(struct pinctrl *p, struct pinctrl_state *state)
{// 如果当前状态已经是要选择的状态,则无需进行任何操作,直接返回0表示成功if (p->state == state)return 0;// 调用 pinctrl_commit_state 函数来应用并切换到新的状态return pinctrl_commit_state(p, state);
}

第8行的返回值中又调用的pinctrl_commit_state函数应用并切换到新的状态,该函数的具体内容如下所示:

static int pinctrl_commit_state(struct pinctrl *p, struct pinctrl_state *state)
{struct pinctrl_setting *setting, *setting2;struct pinctrl_state *old_state = p->state;int ret;if (p->state) {/** 对于旧状态中的每个引脚复用设置,取消 SW 记录的该引脚组的复用所有者。* 任何仍由新状态拥有的引脚组将在下面循环中的 pinmux_enable_setting() 调用中重新获取。*/list_for_each_entry(setting, &p->state->settings, node) {if (setting->type != PIN_MAP_TYPE_MUX_GROUP)continue;pinmux_disable_setting(setting);}}p->state = NULL;/* 应用新状态的所有设置 */list_for_each_entry(setting, &state->settings, node) {switch (setting->type) {case PIN_MAP_TYPE_MUX_GROUP:ret = pinmux_enable_setting(setting);break;case PIN_MAP_TYPE_CONFIGS_PIN:case PIN_MAP_TYPE_CONFIGS_GROUP:ret = pinconf_apply_setting(setting);break;default:ret = -EINVAL;break;}if (ret < 0) {// 如果应用设置失败,则回滚新状态的设置goto unapply_new_state;}}p->state = state;return 0;unapply_new_state:// 回滚新状态的设置list_for_each_entry_safe(setting, setting2, &state->settings, node) {switch (setting->type) {case PIN_MAP_TYPE_MUX_GROUP:pinmux_disable_setting(setting);break;case PIN_MAP_TYPE_CONFIGS_PIN:case PIN_MAP_TYPE_CONFIGS_GROUP:pinconf_remove_setting(setting);break;}}// 回滚完成后,将状态恢复为旧状态p->state = old_state;return ret;
}

(1)第4行:保存当前的状态对象到变量 old_state 中。

(2)第7-18行:检查是否存在旧的状态,如果存在则取消记录在软件中的旧状态中每个引脚复用设置的复用所有者。这样做是为了确保任何仍由新状态拥有的引脚组在后面的循环中重新获取(通过pinmux_enable_setting()函数调用)。

(3)第20行:将当前的状态设置为NULL,以便在应用新状态时可以正确处理。

(4)第22-41行:遍历新状态的所有设置,并根据设置的类型执行相应的操作:

·对于引脚复用设置(PIN_MAP_TYPE_MUX_GROUP),调用pinmux_enable_setting()函数来启用该设置。

·对于引脚配置设置(PIN_MAP_TYPE_CONFIGS_PIN或PIN_MAP_TYPE_CONFIGS_GROUP),调用pinconf_apply_setting()函数来应用该设置。

·对于其他类型的设置,将返回一个错误码(-EINVAL)。

·如果应用设置失败,则跳转到标签unapply_new_state,执行回滚操作。

最后对52行的pinmux_enable_setting函数和56行的pinconf_apply_setting()函数进行讲解,pinmux_enable_setting函数定义在内核源码目录下的“drivers/pinctrl/pinmux.c”文件中,具体内容如下所示:

int pinmux_enable_setting(const struct pinctrl_setting *setting)
{// 获取相关结构体指针struct pinctrl_dev *pctldev = setting->pctldev;const struct pinctrl_ops *pctlops = pctldev->desc->pctlops;const struct pinmux_ops *ops = pctldev->desc->pmxops;int ret = 0;const unsigned *pins = NULL;unsigned num_pins = 0;int i;struct pin_desc *desc;// 如果pctlops->get_group_pins函数存在,则调用该函数获取组中的引脚信息if (pctlops->get_group_pins)ret = pctlops->get_group_pins(pctldev, setting->data.mux.group,&pins, &num_pins);if (ret) {const char *gname;// 错误只影响调试数据,因此只发出警告gname = pctlops->get_group_name(pctldev,setting->data.mux.group);dev_warn(pctldev->dev,"could not get pins for group %s\n",gname);num_pins = 0;}// 逐个申请组中的引脚for (i = 0; i < num_pins; i++) {ret = pin_request(pctldev, pins[i], setting->dev_name, NULL);if (ret) {const char *gname;const char *pname;desc = pin_desc_get(pctldev, pins[i]);pname = desc ? desc->name : "non-existing";gname = pctlops->get_group_name(pctldev,setting->data.mux.group);dev_err(pctldev->dev,"could not request pin %d (%s) from group %s "" on device %s\n",pins[i], pname, gname,pinctrl_dev_get_name(pctldev));goto err_pin_request;}}// 分配引脚后,编码复用设置for (i = 0; i < num_pins; i++) {desc = pin_desc_get(pctldev, pins[i]);if (desc == NULL) {dev_warn(pctldev->dev,"could not get pin desc for pin %d\n",pins[i]);continue;}desc->mux_setting = &(setting->data.mux);}// 调用ops->set_mux函数设置复用ret = ops->set_mux(pctldev, setting->data.mux.func,setting->data.mux.group);if (ret)goto err_set_mux;return 0;err_set_mux:// 复用设置失败,清除复用设置for (i = 0; i < num_pins; i++) {desc = pin_desc_get(pctldev, pins[i]);if (desc)desc->mux_setting = NULL;}
err_pin_request:// 在错误发生时释放已申请的引脚while (--i >= 0)pin_free(pctldev, pins[i], NULL);return ret;
}

该函数用于启用引脚复用设置。

(1)第13-28行:调用pctlops->get_group_pins函数获取指定组中的引脚信息,如果函数存在,则调用该函数,并将引脚信息存储在pins和num_pins变量中。如果获取引脚信息失败,发出警告并将num_pins设置为0。

(2)第30-48行:使用pin_request函数申请引脚,并传入引脚控制器设备、引脚编号、设备名称和其他参数。如果申请引脚失败,发出错误信息并跳转到错误处理步骤。

(3)第50-60行:分配引脚后,使用pin_desc_get函数获取引脚描述符,并将复用设置指针指向引脚复用信息。

(4)第62-64行:调用ops->set_mux函数设置引脚复用,传入引脚控制器设备、复用功能和组信息,以便设置引脚复用。

(5)第66行:如果引脚复用设置失败,跳转到错误处理步骤。

pinconf_apply_setting()函数定义在内核源码目录下的“drivers/pinctrl/pinconf.c”文件中,具体内容如下所示:

int pinconf_apply_setting(const struct pinctrl_setting *setting)
{// 获取相关结构体指针struct pinctrl_dev *pctldev = setting->pctldev;const struct pinconf_ops *ops = pctldev->desc->confops;int ret;// 检查是否存在 pinconf 操作函数集if (!ops) {dev_err(pctldev->dev, "missing confops\n");return -EINVAL;}// 根据设置类型选择相应的操作switch (setting->type) {case PIN_MAP_TYPE_CONFIGS_PIN:// 检查是否存在 pin_config_set 操作函数if (!ops->pin_config_set) {dev_err(pctldev->dev, "missing pin_config_set op\n");return -EINVAL;}// 调用 pin_config_set 函数设置单个引脚的配置ret = ops->pin_config_set(pctldev,setting->data.configs.group_or_pin,setting->data.configs.configs,setting->data.configs.num_configs);if (ret < 0) {dev_err(pctldev->dev,"pin_config_set op failed for pin %d\n",setting->data.configs.group_or_pin);return ret;}break;case PIN_MAP_TYPE_CONFIGS_GROUP:// 检查是否存在 pin_config_group_set 操作函数if (!ops->pin_config_group_set) {dev_err(pctldev->dev, "missing pin_config_group_set op\n");return -EINVAL;}// 调用 pin_config_group_set 函数设置引脚组的配置ret = ops->pin_config_group_set(pctldev,setting->data.configs.group_or_pin,setting->data.configs.configs,setting->data.configs.num_configs);if (ret < 0) {dev_err(pctldev->dev,"pin_config_group_set op failed for group %d\n",setting->data.configs.group_or_pin);return ret;}break;default:return -EINVAL;}return 0;
}

该函数用于应用引脚配置设置。

(1)第8-12行:检查是否存在引脚配置操作函数集,如果不存在,则返回错误码 -EINVAL。

(2)第14-59:根据设置类型进行相应的处理:

·如果设置类型为 PIN_MAP_TYPE_CONFIGS_PIN,表示对单个引脚进行配置设置。然后检查是否存在 pin_config_set 操作函数,如果存在则调用 pin_config_set 函数设置单个引脚的配置。如果设置失败,则返回相应的错误码。

·如果设置类型为 PIN_MAP_TYPE_CONFIGS_GROUP,表示对引脚组进行配置设置,然后检查是否存在 pin_config_group_set 操作函数,如果存在则调用 pin_config_group_set 函数设置引脚组的配置。如果设置失败,则返回相应的错误码。

·如果设置类型不是上述两种类型,则返回错误码 -EINVAL。

至此,关于pinctrl_bind_pins函数的重要内容就讲解完成了,通过pinctrl_bind_pins函数实现了为给定的设备绑定引脚,并在绑定过程中选择和设置适当的pinctrl状态,在124.1小节最后提出的struct pinctrl_state *default_state跟pinctrl_map结构体是什么时候建立起联系的问题也就解决了。

到这里关于pinctrl子系统第二阶段的讲解也就完成了,为了帮助大家整合讲解过的知识点,作者绘制了以下思维导图:

注:该思维导图的存放路径为iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\05_思维导图\02_pinctrl 阶段2.jpg

图 126-1

大家可以根据该思维导图,对上面章节的内容进行梳理,从而真正理解pinctrl子系统框架。

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

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

相关文章

电影《艾里甫与赛乃姆》简介

电影《艾里甫与赛乃姆》由天山电影制片厂于1981年摄制&#xff0c;该片由傅杰执导&#xff0c;由买买提祖农司马依、布维古丽、阿布里米提沙迪克、努力曼阿不力孜、买买提依不拉音江、阿不都热合曼艾力等主演。 该片改编自维吾尔族民间爱情叙事长诗《艾里甫与赛乃姆》&#xf…

如何正确使用高速探头前端--probe head

目前市面上的高速有源探头种类丰富&#xff0c;使用灵活&#xff0c;如下图所示&#xff0c;结构多为放大器焊接前端的组合&#xff0c;以E2677B探头前端为例&#xff0c;其焊接前端电阻有三种选择&#xff0c;91ohm时可实现全带宽使用&#xff08;12GHz&#xff09;&#xff0…

互联网 HR 眼中的好简历是什么样子的?

HR浏览一份简历也就25秒左右&#xff0c;如果你连「好简历」都没有&#xff0c;怎么能找到好工作呢&#xff1f; 如果你不懂得如何在简历上展示自己&#xff0c;或者觉得怎么改简历都不出彩&#xff0c;那请你一定仔细读完。 互联网运营个人简历范文> 男 22 本科 AI简历…

PaddleSeg的训练与测试推理全流程(超级详细)

LeNet模型量化 参考文档一.下载项目地址&#xff1a;https://gitee.com/paddlepaddle/PaddleSeg/tree/release%2F2.5/特别注意下载版本&#xff1a; 二.paddlepaddle-gpu安装1.环境安装参考文档&#xff1a;https://gitee.com/paddlepaddle/PaddleSeg/blob/release/2.8/docs/in…

JetPack组件学习ViewModel

目录 ViewModel的使用 简要分析 问答 如何实现旋转屏幕数据保持不变&#xff1f; 和之前的Presenter有什么区别 ViewModel的使用 1.需要先创建ViewModel类&#xff0c;继承自ViewModel重写onclear方法&#xff0c;使得页面销毁的时候能够走到自定义的onClear方法中 clas…

ALIENWARE:卓越游戏体验,源自创新基因

美国拉斯维加斯当地时间1月9日&#xff0c;CES 2024在万众期盼中如约而至。 作为全球消费电子领域一年一度的盛宴和行业风向标&#xff0c;CES 2024汇聚了来自全球的众多消费电子企业以及令人目不暇接的最新科技产品&#xff0c;因而受到了全球广大消费者的密切关注。 众所周知…

日期类的实现|运算符重载的复用

前言 通过前面C入门与类与对象的学习&#xff0c;今天我们将运用所学的知识点完成一个Date类。 本节目标 运用所学知识完成Date类。详细讲解运算符各种重载。理解运算符重载的复用。 一、Date类的六个默认成员函数 六个成员函数&#xff0c;Date类只需要自己实现构造函数即可…

新一代工厂融合广播系统,助力工业行业可持续发展

在当今高度竞争的工业环境中&#xff0c;工厂的运营效率和生产安全至关重要。为了实现这一目标&#xff0c;新一代工厂融合广播系统应运而生&#xff0c;将指挥中心、值班中心、融合通信调度主机、厂区终端和防爆话机紧密连接&#xff0c;构建了一个全面、高效的通信网络。 系统…

Linux进程管理、ps命令、kill命令

每一个程序在运行的时候都会被操作系统注册为系统中的一个进程 补充一下操作系统的内容&#xff1a; 进程实体&#xff08;又称进程映像&#xff09;&#xff1a;程序段、相关数据段、PCB三部分构成 进程是进程实体的运行过程&#xff0c;是系统进行资源分配的一个独立单位 …

团结引擎的安装

团结引擎有多种方式可以安装&#xff0c;具体可以参考团结引擎官方文档&#xff0c;这里我们使用最简单的安装方式&#xff0c;通过团结Hub来安装。 1. 安装 Tuanjie Hub 进入团结引擎官网&#xff0c;点击右上角的【下载Unity】&#xff0c;进入下载界面&#xff0c;选择“下载…

C++——冒泡排序

作用&#xff1a;最常用的排序算法&#xff0c;对数组内元素进行排序 1&#xff0c;比较相邻的元素&#xff0c;如果第一个比第二个大&#xff0c;就交换他们两个。 2&#xff0c;对每一对相邻元素做同样的工作&#xff0c;执行完毕后&#xff0c;找到第一个最大值。 3&…

JDK21和 Flowable 7.0.0

JDK21和 Flowable 7.0.0 一.Flowable二.项目搭建1.依赖包2.数据库3.资源文件1.YML配置文件2.Drools kbase3.Drools rule4.DMN 决策表5.BPMN 流文件 4.BPMN 流程图绘制插件5.测试代码1.启动类2.Flowable 配置3.Camel 配置1.Camel 配置2.Camel Router 定义 4.扩展类监听1.外部工作…

docker compose安装gitlab

环境 查看GitLab镜像 docker search gitlab 拉取GitLab镜像 docker pull gitlab/gitlab-ce 准备gitlab-docker.yml文件 version: 3.1 services:gitlab:image: gitlab/gitlab-ce:latestcontainer_name: gitlabrestart: alwaysenvironment:GITLAB_OMNIBUS_CONFIG: |external_url…

在Windows Server 2012中部署war项目

目录 一.安装jdk 二.安装tomcat 三.安装MySQL 四.部署项目 好啦今天就到这了&#xff0c;希望帮到你了哦 前言&#xff1a;具体步骤&#xff1a; 1.安装JDK&#xff1a; 2.安装tomcat&#xff1a; 3.安装MySQL&#xff1a; 4.部署项目&#xff1a; 一.安装jdk 将所需文件放…

苍穹外卖学习----出错记录

1.微信开发者工具遇到的问题&#xff1a; 1.1appid消失报错&#xff1a; {errMsg: login:fail 系统错误,错误码:41002,appid missing [20240112 16:44:02][undefined]} 1.2解决方式&#xff1a; appid可在微信开发者官网 登录账号后在开发栏 找到 复制后按以下步骤粘贴即…

怎么将文件批量重命名为不同名称?

怎么将文件批量重命名为不同名称&#xff1f;有许多情况下可以考虑对文件进行批量重命名为不同名称&#xff0c;文件分类和整理&#xff1a;当您需要对一组文件进行分类、整理或重新组织时&#xff0c;可以考虑将它们批量重命名为不同的名称。这有助于更好地组织文件并使其更易…

提升测试多样性,揭秘Pytest插件pytest-randomly

大家可能知道在Pytest测试生态中&#xff0c;插件扮演着不可或缺的角色&#xff0c;为开发者提供了丰富的功能和工具。其中&#xff0c;pytest-randomly 插件以其能够引入随机性的特性而备受欢迎。本文将深入探讨 pytest-randomly 插件的应用&#xff0c;以及如何通过引入随机性…

在线项目实习分享:股票价格形态聚类与收益分析

01前置课程 数据挖掘基础数据探索数据预处理数据挖掘算法基础Python数据挖掘编程基础Matplotlib可视化Pyecharts绘图 02师傅带练 行业联动与轮动分析 通过分析申银万国行业交易指数的联动与轮动现象&#xff0c;获得有意义的行业轮动关联规则&#xff0c;并在此基础上设计量…

【NI-DAQmx入门】LabVIEW中DAQmx同步

1.同步解释 1.1 同步基础概念 触发器&#xff1a;触发器是控制采集的命令。您可以使用触发器来启动、停止或暂停采集。触发信号可以源自软件或硬件源。 时钟&#xff1a;时钟是用于对数据采集计时的周期性数字信号。根据具体情况&#xff0c;您可以使用时钟信号直接控制数据采…

基于SSM的驾校预约管理系统

基于SSM的驾校预约管理系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringSpringMVCMyBatis工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 详情 管理员界面 摘要 随着社会的不断发展&#xff0c;驾驶技能的需求逐渐增…