ArduPilot开源代码之AP_OSD

news2025/3/10 19:02:03

ArduPilot开源代码之AP_OSD

  • 1. 源由
  • 2. 简介
  • 3. 补丁
  • 4. 框架设计
    • 4.1 启动代码 (AP_OSD::init)
    • 4.2 任务代码 (AP_OSD::osd_thread)
    • 4.3 实例初始化 (AP_OSD::init_backend)
  • 5. 重要例程
    • 5.1 AP_OSD::update_stats
    • 5.2 AP_OSD::update_current_screen
    • 5.3 AP_OSD::update_osd
  • 6. 总结
  • 7. 参考资料

1. 源由

因为自己有两个摄像头:模拟+数字;而数字OpenIPC地面端Jetson-fpv还不太成熟,所以暂时还想使用模拟的飞,等稳定了切换成数字。

问题是数字录像还是要的,以便更好的了解OpenIPC作为数字图传的效果。

  • Is it possible for two OSD resolution working at the same time?
  • How to setup two VTX (one for analog camera, another for digital camera)

从代码和咨询的角度看,似乎Ardupilot并不支持同时两个OSD在不同分辨率的情况下工作。

所以,还是需要从代码入手,DIY玩的就是“心跳”,对吧。

2. 简介

最多支持两种OSD实例切换,不支持同一时刻,两种OSD同时使用。

#define OSD_MAX_INSTANCES 2

基于AP_OSD_Backend支持以下四种OSD类型:

  • OSD_MAX7456: AP_OSD_MAX7456
  • OSD_SITL: AP_OSD_SITL
  • OSD_MSP: AP_OSD_MSP
  • OSD_MSP_DISPLAYPORT: AP_OSD_MSP_DisplayPort

3. 补丁

但是通过AP_OSD: add two osd resolution concurrently support #29149 已经打破该困局,支持同一时刻,两种尺寸的OSD的同时显示:

  • Git Repo下如何制作一个patch文件

patch分享/更好的差异化比较,减少宝贵Review时间浪费,也是对代码熟悉程度的体现。

另外,也可以作为系统集成的差异化补丁,快速实现本地集成、编译、测试、验证等。

4. 框架设计

接下来,我们来看下该模块的设计。

4.1 启动代码 (AP_OSD::init)

这里不做过多解释,详见:启动代码流程-ArduPilot飞控启动&运行过程简介

Copter::init_ardupilot
 └──> osd.init();
  1. 根据配置内容直接对实例类型进行赋值
  2. 然后针对对应的四种类型OSD进行初始化
  3. 创建osd_thread任务,依据优先级执行例程
AP_OSD::init
 ├──> const AP_OSD::osd_types types[OSD_MAX_INSTANCES] = {
 │      osd_types(osd_type.get()),
 │      osd_types(osd_type2.get())
 │  };
 ├──> for <instance < OSD_MAX_INSTANCES>
 │   ├──> init_backend(types[instance], instance)
 │   └──> _backend_count++;
 └──> <_backend_count > 0>
     └──> hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&AP_OSD::osd_thread, void), "OSD", 1280, AP_HAL::Scheduler::PRIORITY_IO, 1);

4.2 任务代码 (AP_OSD::osd_thread)

  1. 例程会首先执行osd_thread_run_once
  2. 然后每隔100ms唤醒对2个OSD实例进行更新
  3. disable OSD,则无需更新状态
  4. 对OSD Overlay进行刷新

 ├──> for <instance < OSD_MAX_INSTANCES>
 │   └──> _backends[instance]->osd_thread_run_once()
 └──> <loop>
     ├──> hal.scheduler->delay(100)
     ├──> <!_disable>
     │   ├──> update_stats();
     │   └──> update_current_screen();
     └──> update_osd()

4.3 实例初始化 (AP_OSD::init_backend)

每种实例需要对软硬件进行除此环境设定,这里采用类似probe的方式执行。

  • 若,probe失败,则该OSD实例为空指针
  • 若,该实例与第一个默认OSD实例冲突,则第二个实例不进行初始化
AP_OSD::init_backend
 ├──> <instance > 0 && _backends[0] && !_backends[0]->is_compatible_with_backend_type(type)>
 │   └──> return false; // 第二种类型OSD与默认第一类OSD不兼容
 ├──> <switch>
 │   ├──> <OSD_NONE> break
 │   ├──> <OSD_TXONLY> break
 │   ├──> <OSD_MAX7456> <HAL_WITH_SPI_OSD> <HAL_WITH_OSD_BITMAP>
 │   │   ├──> AP_HAL::OwnPtr<AP_HAL::Device> spi_dev = std::move(hal.spi->get_device("osd"));
 │   │   ├──> _backends[instance] = AP_OSD_MAX7456::probe(*this, std::move(spi_dev))
 │   │   └──>  break
 │   ├──> <WITH_SITL_OSD> 
 │   │   ├──> _backends[instance] = AP_OSD_SITL::probe(*this)
 │   │   └──>  break
 │   ├──> <OSD_MSP> 
 │   │   ├──> _backends[instance] = AP_OSD_MSP::probe(*this);
 │   │   └──>  break
 │   └──> <OSD_MSP_DISPLAYPORT> <HAL_WITH_MSP_DISPLAYPORT>
 │       ├──> _backends[instance] = AP_OSD_MSP_DisplayPort::probe(*this)
 │       └──>  break
 ├──> <OSD_ENABLED && _backends[instance] != nullptr>
 │   ├──> _backends[instance]->init_symbol_set(AP_OSD_AbstractScreen::symbols_lookup_table, AP_OSD_NUM_SYMBOLS)
 │   └──> return true;
 └──> return false;

5. 重要例程

5.1 AP_OSD::update_stats

对系统内部的参数进行定期更新

  • 地速
  • 位置
  • 高度
  • 空速
  • 飞行距离
  • 最大地面速度
  • 最大高度
  • 最大直线与HOME的距离
  • 最大电流
  • 最小电压
  • 最小RSSI
  • 最大空速
  • 最大ESC温度
AP_OSD::update_stats
 ├──> WITH_SEMAPHORE(_sem);
 ├──> uint32_t now = AP_HAL::millis();
 ├──> <!AP_Notify::flags.armed> //没有启动,则无需更新状态
 │   ├──> _stats.last_update_ms = now;
 │   └──> return;
 │
 ├──> [更新delta_ms]
 │   ├──> uint32_t delta_ms = now - _stats.last_update_ms;
 │   ├──> _stats.last_update_ms = now;
 │   │
 │   ├──> Vector2f v;
 │   ├──> Location loc {};
 │   ├──> Location home_loc;
 │   ├──> bool home_is_set;
 │   ├──> bool have_airspeed_estimate;
 │   ├──> float alt;
 │   ├──> float aspd_mps = 0.0f;
 │   └──> [获取地速、HOME以及当前位置、高度、空速]
 │       ├──> AP_AHRS &ahrs = AP::ahrs();
 │       ├──> WITH_SEMAPHORE(ahrs.get_semaphore()); // minimize semaphore scope
 │       ├──> v = ahrs.groundspeed_vector();
 │       ├──> home_is_set = ahrs.get_location(loc) && ahrs.home_is_set();
 │       ├──> <home_is_set>
 │       │   └──>home_loc = ahrs.get_home();
 │       ├──> ahrs.get_relative_position_D_home(alt);
 │       └──> have_airspeed_estimate = ahrs.airspeed_estimate(aspd_mps);
 │
 ├──> [飞行距离更新]
 │   ├──> float speed = v.length();
 │   ├──> <speed < 0.178>
 │   │   └──> speed = 0.0;
 │   ├──> float dist_m = (speed * delta_ms)*0.001;
 │   └──> _stats.last_distance_m += dist_m;
 │
 ├──> [最大地面速度更新]
 │   └──> _stats.max_speed_mps = fmaxf(_stats.max_speed_mps,speed);
 │
 ├──> [最大距离HOME位置更新]<home_is_set>
 │   ├──> float distance = home_loc.get_distance(loc);
 │   └──> _stats.max_dist_m = fmaxf(_stats.max_dist_m, distance);
 │
 ├──> [最大高度更新]
 │   ├──> alt = -alt;
 │   └──> _stats.max_alt_m = fmaxf(_stats.max_alt_m, alt);
 │
 ├──> <AP_BATTERY_ENABLED>
 │   ├──> [最大电流更新]
 │   │   ├──> AP_BattMonitor &battery = AP::battery();
 │   │   ├──> float amps;
 │   │   └──> <battery.current_amps(amps)>  _stats.max_current_a = fmaxf(_stats.max_current_a, amps)
 │   └──> [最小电压更新]
 │       ├──> float voltage = battery.voltage();
 │       └──> <voltage > 0> _stats.min_voltage_v = fminf(_stats.min_voltage_v, voltage)
 │
 ├──> <AP_RSSI_ENABLED>
 │   └──> [最小RSSI更新]
 │       ├──> AP_RSSI *ap_rssi = AP_RSSI::get_singleton();
 │       └──> <ap_rssi> _stats.min_rssi = fminf(_stats.min_rssi, ap_rssi->read_receiver_rssi());
 │
 ├──> [最大空速更新]
 │   └──> <have_airspeed_estimate> _stats.max_airspeed_mps = fmaxf(_stats.max_airspeed_mps, aspd_mps);
 │
 └──> <HAL_WITH_ESC_TELEM>
     └──> [最大ESC温度更新]
         ├──> AP_ESC_Telem& telem = AP::esc_telem();
         ├──> int16_t highest_temperature = 0;
         ├──> telem.get_highest_temperature(highest_temperature);
         └──> _stats.max_esc_temp = MAX(_stats.max_esc_temp, highest_temperature);

5.2 AP_OSD::update_current_screen

默认配置情况下:

  • arm_scr = 0
  • disarm_scr = 0
  • failsafe_scr = 0
  • rc_channel = 0
    // @Param: _CHAN
    // @DisplayName: Screen switch transmitter channel
    // @Description: This sets the channel used to switch different OSD screens.
    // @Values: 0:Disable,5:Chan5,6:Chan6,7:Chan7,8:Chan8,9:Chan9,10:Chan10,11:Chan11,12:Chan12,13:Chan13,14:Chan14,15:Chan15,16:Chan16
    // @User: Standard
    AP_GROUPINFO("_CHAN", 2, AP_OSD, rc_channel, 0),
    
    // @Param: _ARM_SCR
    // @DisplayName: Arm screen
    // @Description: Screen to be shown on Arm event. Zero to disable the feature.
    // @Range: 0 4
    // @User: Standard
    AP_GROUPINFO("_ARM_SCR", 17, AP_OSD, arm_scr, 0),

    // @Param: _DSARM_SCR
    // @DisplayName: Disarm screen
    // @Description: Screen to be shown on disarm event. Zero to disable the feature.
    // @Range: 0 4
    // @User: Standard
    AP_GROUPINFO("_DSARM_SCR", 18, AP_OSD, disarm_scr, 0),

    // @Param: _FS_SCR
    // @DisplayName: Failsafe screen
    // @Description: Screen to be shown on failsafe event. Zero to disable the feature.
    // @Range: 0 4
    // @User: Standard
    AP_GROUPINFO("_FS_SCR", 19, AP_OSD, failsafe_scr, 0),

所以,默认情况以下代码不执行。

AP_OSD::update_current_screen
 ├──> [Switch on ARM/DISARM event]
 │   ├──> <AP_Notify::flags.armed>
 │   │   ├──> <!was_armed && arm_scr > 0 
 │   │   │   │    && arm_scr <= AP_OSD_NUM_DISPLAY_SCREENS 
 │   │   │   │    && get_screen(arm_scr-1).enabled>
 │   │   │   └──> current_screen = arm_scr-1;
 │   │   └──> was_armed = true;
 │   └──> <was_armed>
 │       ├──> <disarm_scr > 0>
 │       │   │    && disarm_scr <= AP_OSD_NUM_DISPLAY_SCREENS 
 │       │   │    && get_screen(disarm_scr-1).enabled>
 │       │   └──> current_screen = disarm_scr-1;
 │       └──> was_armed = false;
 │
 ├──> [Switch on failsafe event]
 │   ├──> <AP_Notify::flags.failsafe_radio 
 │   │     || AP_Notify::flags.failsafe_battery>
 │   │   ├──> <!was_failsafe && failsafe_scr > 0 
 │   │   │   │    && failsafe_scr <= AP_OSD_NUM_DISPLAY_SCREENS 
 │   │   │   │    && get_screen(failsafe_scr-1).enabled>
 │   │   │   ├──> pre_fs_screen = current_screen;
 │   │   │   └──> current_screen = failsafe_scr-1;
 │   │   └──> was_failsafe = true;
 │   └──> <was_failsafe>
 │       ├──> <get_screen(pre_fs_screen).enabled>
 │       │   └──> current_screen = pre_fs_screen;
 │       └──> was_failsafe = false;
 │
 ├──> <rc_channel == 0> return
 │
 └──> <AP_RC_CHANNEL_ENABLED>
     ├──> RC_Channel *channel = RC_Channels::rc_channel(rc_channel-1);
     ├──> <channel == nullptr> return;
     ├──> int16_t channel_value = channel->get_radio_in();
     ├──> [switch (sw_method)] 
     │   ├──> <default/TOGGLE> //switch to next screen if channel value was changed
     │   │   ├──> <previous_channel_value == 0>
     │   │   │   └──> previous_channel_value = channel_value; //do not switch to the next screen just after initialization
     │   │   └──> <abs(channel_value-previous_channel_value) > 200>
     │   │       ├──> switch_debouncer) {
     │   │       │   ├──> next_screen();
     │   │       │   └──> previous_channel_value = channel_value;
     │   │       └──> <else>
     │   │           ├──> switch_debouncer = true;
     │   │           └──> return;
     │   │
     │   ├──> <PWM_RANGE> //select screen based on pwm ranges specified
     │   │   └──> <for (int i=0; i<AP_OSD_NUM_SCREENS; i++>
     │   │       └──> <get_screen(i).enabled 
     │   │           │ && get_screen(i).channel_min <= channel_value 
     │   │           │ && get_screen(i).channel_max > channel_value>
     │   │           ├──> <previous_pwm_screen == i>
     │   │           │   └──> break;
     │   │           └──> <else>
     │   │               └──> current_screen = previous_pwm_screen = i;
     │   │
     │   └──> <AUTO_SWITCH> //switch to next screen after low to high transition and every 1s while channel value is high
     │       ├──> <channel_value > channel->get_radio_trim()>
     │       │   ├──> <switch_debouncer>
     │       │   │   ├──> uint32_t now = AP_HAL::millis();
     │       │   │   └──> <now - last_switch_ms > 1000>
     │       │   │       ├──> next_screen();
     │       │   │       └──> last_switch_ms = now;
     │       │   └──> <else>
     │       │       ├──> switch_debouncer = true;
     │       │       └──> return;
     │       └──> <else>
     │           └──>last_switch_ms = 0;
     └──> switch_debouncer = false;

5.3 AP_OSD::update_osd

默认初始化时,current_screen = 0

所以,从这里可以看出,始终更行current_screen对应的OSD。

AP_OSD::update_osd
 └──> for <instance < _backend_count>
     ├──> _backends[instance]->clear()
     ├──> <!_disable>
     │   ├──> get_screen(current_screen).set_backend(_backends[instance])
     │   └──> <_backends[instance]->get_backend_type() != OSD_MSP> get_screen(current_screen).draw()
     └──> _backends[instance]->flush()

6. 总结

为了让Ardupilot代码支持模拟+数字OSD同时显示更新,需定制固件。

目前,前面提及的补丁尚未合入,且存在一个模拟越来越少使用的问题,要合入可能也存在一定的苦难。

不过,对于我们的测试样机:

  • ArduPilot开源飞控之lida2003-H743-5inch套机配置
  • ArduPilot开源飞控之lida2003-H743-5inch配置调整

这里已经整理了代码:

  • AP_OSD: add two osd resolution concurrently support
  • hwdef: enable two osd resolution concurrently feature for Aocoda-RC H743 target
$ git clone git@github.com:SnapDragonfly/ardupilot.git
or
$ git clone https://github.com/SnapDragonfly/ardupilot.git
$ cd ardupilot
$ git checkout Copter-4.5-lida2003

然后进行编译:

  • ArduPilot飞控AOCODARC-H7DUAL固件编译
  • Ardupilot开源飞控工程项目编译回顾

7. 参考资料

【1】ArduPilot开源飞控系统之简单介绍
【2】ArduPilot之开源代码Task介绍
【3】ArduPilot飞控启动&运行过程简介
【4】ArduPilot之开源代码Library&Sketches设计
【5】ArduPilot之开源代码Sensor Drivers设计

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

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

相关文章

腾讯元宝:AI 时代的快速论文阅读助手

1. 背景与需求 在 AI 研究领域&#xff0c;每天都会涌现大量学术论文。如何高效阅读并提取关键信息成为研究者的一大难题。腾讯元宝是腾讯推出的一款大模型&#xff0c;结合了**大语言模型&#xff08;LLM&#xff09;和自然语言处理&#xff08;NLP&#xff09;**技术&#x…

重构谷粒商城09:人人开源框架的快速入门

谷粒商城09——人人开源框架的快速入门 前言&#xff1a;这个系列将使用最前沿的cursor作为辅助编程工具&#xff0c;来快速开发一些基础的编程项目。目的是为了在真实项目中&#xff0c;帮助初级程序员快速进阶&#xff0c;以最快的速度&#xff0c;效率&#xff0c;快速进阶…

redis数据迁移教程(使用RedisShake实现不停机迁移十分便捷)

1.我的场景 需要把本地的redis数据上传到阿里云服务器上面,服务器上redis并没有开aof持久化,但是将rdb文件上传至服务器后每次重启redis,rdb文件会被覆盖导致无法同同步数据,最终决定使用RedisShake 2.RedisShake介绍 什么是 RedisShake​ RedisShake 是一个用于处理和迁移…

2025年2月平价旗舰手机性能对比

1、荣耀Magic7 点评&#xff1a;缺席潜望式长焦&#xff0c;3X直立长焦体验还行。兼顾性能、游戏、屏幕、影像、续航、快充等诸多方面&#xff0c;且外围配置比较齐全。 2、vivo x200 点评&#xff1a;潜望式长焦相机&#xff0c;拍照效果好&#xff0c;30W无线充电着实鸡肋&a…

Golang学习笔记_44——命令模式

Golang学习笔记_41——观察者模式 Golang学习笔记_42——迭代器模式 Golang学习笔记_43——责任链模式 文章目录 一、核心概念1. 定义2. 解决的问题3. 核心角色4. 类图 二、特点分析三、适用场景1. 事务管理系统2. 多媒体遥控器3. 操作审计系统 四、Go语言实现示例五、高级应用…

【单片机通信技术】STM32 HAL库 SPI主从机通过串口发送数据

一、说明 使用STM32F103C8T6最小系统板&#xff0c;让板载SPI1与SPI2通信&#xff0c;通过串口收发数据。本文章说明了在配置与编写时遇到的一些问题&#xff0c;以及详细说明如何使用cubeMAX进行代码编写。 二、CubeMAX配置 1.时钟配置选择外部高速时钟 2.系统模式与时钟配…

laravel中 添加公共/通用 方法/函数

一&#xff0c;现在app 下面创建Common目录&#xff0c;然后在创建Common.php 文件 二&#xff0c;修改composer.json文件 添加这个到autoload 中 "files": ["app/Common/Common.php"]"autoload": {"psr-4": {"App\\": &quo…

Jetpack Compose — 入门实践

一、项目中使用 Jetpack Compose 从此节开始,为方便起见,如无特殊说明,Compose 均指代 Jetpack Compose。 开发工具: Android Studio 1.1 创建支持 Compose 新应用 新版 Android Studio 默认创建新项目即为 Compose 项目。 注意:在 Language 下拉菜单中,Kotlin 是唯一可…

P8686 [蓝桥杯 2019 省 A] 修改数组--并查集 or Set--lower_bound()的解法!!!

P8686 [蓝桥杯 2019 省 A] 修改数组--并查集 题目 并查集解析代码【并查集解】 Set 解法解析lower_bound代码 题目 并查集解析 首先先让所有的f&#xff08;i&#xff09;i&#xff0c;即每个人最开始的祖先都是自己&#xff0c;然后就每一次都让轮到那个数的父亲1&#xff08…

应用案例 | 精准控制,高效运行—宏集智能控制系统助力SCARA机器人极致性能

概述 随着工业4.0的深入推进&#xff0c;制造业对自动化和智能化的需求日益增长。传统生产线面临空间不足、效率低下、灵活性差等问题&#xff0c;尤其在现有工厂改造项目中&#xff0c;如何在有限空间内实现高效自动化成为一大挑战。 此次项目的客户需要在现有工厂基础上进行…

Greenplum6.19集群搭建

一&#xff0c;安装说明 1.1环境说明 1、首先确定部署的环境&#xff0c;确定下服务器的端口&#xff0c;一般默认是22的端口&#xff1b; 2、当前这份文档是服务器处于10022端口下部署的&#xff08;现场生产环境要求&#xff0c;22端口在生产环境存在安全隐患&#xff09;&…

胜软科技冲刺北交所一年多转港股:由盈转亏,毛利率大幅下滑

《港湾商业观察》施子夫 近期&#xff0c;山东胜软科技股份有限公司&#xff08;以下简称&#xff0c;胜软科技&#xff09;递表港交所获受理&#xff0c;独家保荐机构为广发证券&#xff08;香港&#xff09;。 在赴港上市之前&#xff0c;胜软科技还曾谋求过A股上市&#x…

Java零基础入门笔记:多线程

前言 本笔记是学习狂神的java教程&#xff0c;建议配合视频&#xff0c;学习体验更佳。 【狂神说Java】Java零基础学习视频通俗易懂_哔哩哔哩_bilibili 第1-2章&#xff1a;Java零基础入门笔记&#xff1a;(1-2)入门&#xff08;简介、基础知识&#xff09;-CSDN博客 第3章…

数据类设计_图片类设计之1_矩阵类设计(前端架构基础)

前言 学的东西多了,要想办法用出来.C和C是偏向底层的语言,直接与数据打交道.尝试做一些和数据方面相关的内容 引入 图形在底层是怎么表示的,用C来表示 认识图片 图片是个风景,动物,还是其他内容,人是可以看出来的.那么计算机是怎么看懂的呢?在有自主意识的人工智能被设计出来…

C++:入门详解(关于C与C++基本差别)

目录 一.C的第一个程序 二.命名空间&#xff08;namespace&#xff09; 1.命名空间的定义与使用&#xff1a; &#xff08;1&#xff09;命名空间里可以定义变量&#xff0c;函数&#xff0c;结构体等多种类型 &#xff08;2&#xff09;命名空间调用&#xff08;&#xf…

linux下 jq 截取json文件信息

背景&#xff1a;通过‘登录名‘ 获取该对象的其他个人信息如名字。 环境准备&#xff1a;麒麟操作系统V10 jq安装包 jq安装包获取方式&#xff1a;yum install jq 或 使用附件中的rpm 或 git自行下载 https://github.com/stedolan/jq/releases/download/ 实现过程介绍&am…

软件工程:软件需求之需求分析方法

目录 前言 需求分析方法 工具和方法 具体分析方法 对运行环境的影响 ​编辑 前言 本文重点介绍开展软件需求分析的方法。 需求分析方法 工具和方法 软件需求可以维护在ALM系统中&#xff0c;譬如&#xff1a;doors&#xff0c;codeBeamer等&#xff0c;JIRA适合互联网行…

【网络编程】WSAAsyncSelect 模型

十、基于I/O模型的网络开发 接着上次的博客继续分享&#xff1a;select模型 10.8 异步选择模型WSAAsyncSelect 10.8.1 基本概念 WSAAsyncSelect模型是Windows socket的一个异步I/O 模型&#xff0c;利用这个模型&#xff0c;应用程序 可在一个套接字上接收以Windows 消息为基…

视觉-语言模型-出发点CLIP--(精读论文)

阅读建议&#xff1a;配合这个源码分析阅读效果更加 研究背景和目的 介绍当前计算机视觉系统依赖固定类别标签训练的局限性&#xff0c;以及自然语言监督作为一种有潜力替代方式的研究现状。强调论文旨在探索从自然语言监督中学习可迁移视觉模型&#xff0c;实现零样本学习&a…

任务11:路由器配置与静态路由配置

目录 一、概念 二、路由器配置 三、配置静态路由CSDN 原创主页&#xff1a;不羁https://blog.csdn.net/2303_76492156?typeblog 一、概念 1、路由器的作用&#xff1a;通过路由表进行数据的转发。 2、交换机的作用&#xff1a;通过学习和识别 MAC 地址&#xff0c;依据 M…