WebRTC GCC 拥塞控制算法(TFB-GCC)

news2024/11/18 18:37:50

目录

一. 前言 

二. TFB-GCC原理

1. 接收端记录并反馈收包情况

(1)transport-wide sequence nunmber

(2)RTCP RTPFB TW 报文

 2. 发送端结合包接收反馈情况进行带宽预估拥塞控制

(1)基于延时梯度的带宽预估

(2)基于丢包率的带宽预估

三. 参考资料


一. 前言 

        网络传输中链路的带宽是有限的,为避免往链路发送过载的数据量导致网络拥塞,我们需要进行带宽预估,结合预估带宽作出调整避免网络拥塞。

        在《WebRTC GCC 拥塞控制算法(REMB-GCC)》中我们总结了 REMB-GCC 拥塞控制算法,并在文末提到 Google 已经推出 TFB-GCC 取代 REMB-GCC。TFB-GCC 的原理也是基于延时梯度和丢包率进行带宽预估并避免网络拥塞,不同之处在于逻辑都在发送端进行计算,接收端只是反馈包的接收状态(是否收到,以及与上一个包的接收时间差)。

        本文主要讲解 TFB-GCC 相关的原理和实现,如果你对基于延时梯度和基于丢包预估带宽的原理不清楚,可以先阅读这篇文章。

二. TFB-GCC原理

        如上是 TFB-GCC 的架构图,左边是发送端部分,右边是接收端部分。

        接收端负责记录发送端数据包的到达情况, 并构造 RTCP 报文反馈给发送端,它不进行延时梯度计算的逻辑。

        发送端收到 RTCP 反馈报文后,一是根据丢包率预估带宽 As,二是根据延时梯度预估带宽 Ar,最终预估带宽为二者较小值 A=min(As, Ar),以此进行带宽预估拥塞控制。

1. 接收端记录并反馈收包情况

        WebRTC 想要使用 TFB-GCC,需要开启 RTP 报文扩展字段(transport-wide sequence number)以及使用 RTCP RTPFB TW 报文反馈(传输带宽反馈报文),关于 RTP,RTCP 协议的详细内容可以参考对应链接的文章。

(1)transport-wide sequence nunmber

        RTP 扩展字段 transport-wide sequence number 结构如下,它是一个 one-header 的扩展头部,长度为 2 字节,可以理解为通道序列号。

问题:为什么需要使用 transport-wide sequence number?

        一个通道经常会同时传输音频和视频,当我们进行带宽预估时需要预估整个通道的带宽,而不是只结合音频流的收发包情况或者视频流的收发包情况来进行预估。而音频流和视频流发送时 RTP 包初始序号 sequence number 是不同的,并且 sequence number 的增长速度也是不同的,因此我们在发送包时给音频包和视频包打上一个通道序列号,统一计数,这样接收端也方便对通道包接收情况进行应答。

        下面是通过 Wireshark 抓取的 RTP 发送包信息,payload-type: 96 是视频流的包,payload-type: 111 是音频流的包,可以看到这四个包的 transport-wide sequence number 是递增的。

备注:如上是使用 ID=3 代表 transport-wide-cc 扩展头,a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01。

(2)RTCP RTPFB TW 报文

RTCP  RTPFB TW 传输带宽反馈报文格式如下(PT=205,FMT=15)。

base sequence number:记录要反馈的第一个 RTP 包的 transport-wide sequence number

packet status count:该反馈报文包含了多少个 RTP 包的到达状态

reference time:接收端反馈报文的第一个包接收的基准时间(24bit),其值单位为 64ms

fb pkt.count:反馈报文发送的数量,相当于 RTCP RTPFB 报文的序号

packet chunk:记录发送端发送的 RTP 包的到达状态,该结构根据第 0 位的值可以表示为 Run length chunk 和 Status vector chunk

Run length chunk 结构

T (chunk type):该位为 0 表示这是一个 Run length chunk

S (packet status symbol):标记包的到达状态

        00:Packet not received

        01:Packet received, small delta

        10:Packet received, large or negative delta

        11:Reserved

Run Length:长度,表示有连续多少包为相同到达状态

如下的 Run length chunk 表示连续 221 个包没有收到。

Status vector chunk 结构

T (chunk type):该位为 1 表示这是一个 Status vector chunk

S (symbol size):如果该位为 0 表示只包含 packet not received / packet received 两种状态,这样 14bit 的 symbol list 可以表示 14 个包的状态,如果该位为 1 表示使用 2bit 来表示包的状态,这样 symbol list 可以表示 7 个包的状态

symbol list:标识一系列包的状态

备注:当很多包的接收状态都是一致时,比如都是 Received 且到达间隔为 SmallDelta,则使用 Run length chunk 来表示这些包的接收状态,当包的接收状态不一致时就不能使用 Run length chunk 表示相同的接收状态了,此时则使用 Status vector chunk。

        上图所示的 Status vector chunk,S 位为 0 表示 symbol list 中 1bit 代表一个包的接收状态,0 表示未接收,1 代表接收,因此上图表示 1 个包未收到,接下来的 5 个包是收到的,接下来的 3 个包未收到,之后的 3 个包是收到的,最后 2 个包没有收到。

        上图所示的  Status vector chunk,S 位为 1 表示 symbol list 中 2bit 代表一个包的接收状态,因此上图表示第一个包是未接收到的,第二个包是接收到的(w/o timestamp),接下来的 3 个包是接收到的,最后的 2 个包是未收到的。

recv delta:前后两个 RTP 包到达时间间隔,单位值代表 250us,如果到达时间间隔 <= 63.75ms 则认为是 SmallDelta,使用 1 字节,如果大于 63.75ms 则认为是 LargeDelta,使用 2 字节。

 2. 发送端结合包接收反馈情况进行带宽预估拥塞控制

(1)基于延时梯度的带宽预估

        如下图所示,发送端在 T(i-1) 和 T(i) 发送了两个数据包,接收端分别在 t(i-1) 和 t(i) 接收到了这两个数据包,延时梯度 d(t(i)) = [t(i) - t(i-1)] - [T(i) - T(i-1)]。

        在理想状态下的网络传输,d(t(i)) 应该为 0,如果网络发生拥塞,T(i) 时刻发出来的包被接收端接收需要更长的时间,此时 d(t(i)) 大于 0,如果 d(t(i)) 大于 0 且越来越大,说明网络拥塞更严重,如果 d(t(i)) 大于 0 但是越来越小,说明网络拥塞状况处于好转状态。

         我们以 WebRTC 代码说明发送端是如何根据 RTCP 反馈报文以及丢包率情况预估带宽并进行拥塞控制的处理逻辑,对于 TFB-GCC,基于延时梯度的带宽预估主要包括:Arrival-time filter,TrendlineEstimator。

a. Arrival-time filter

        当收到 RTCP RTPFB TW 报文后会调用 RtpTransportControllerSend::OnTransportFeedback 函数,该函数先调用 TransportFeedbackAdapter::ProcessTransportFeedback 获取包的到达状态信息,再调用 GoogCcNetworkController::OnTransportPacketsFeedback 根据包的到达状态信息基于延时梯度进行带宽预估,最后将预估带宽值更新到 Pace,编码等模块,具体逻辑如下。

        TransportFeedbackAdapter::ProcessTransportFeedback 主要是调用 ProcessTransportFeedbackInner 根据 RTCP RTPFB TW 报文的内容获取一组包的相对到达时间,用于计算包的延时梯度。

std::vector<PacketResult>
TransportFeedbackAdapter::ProcessTransportFeedbackInner(
    const rtcp::TransportFeedback& feedback,
    Timestamp feedback_receive_time) {
  // Add timestamp deltas to a local time base selected on first packet arrival.
  // This won't be the true time base, but makes it easier to manually inspect
  // time stamps.
  if (last_timestamp_.IsInfinite()) {
    current_offset_ = feedback_receive_time;
  } else {
    // TODO(srte): We shouldn't need to do rounding here.
    const TimeDelta delta = feedback.GetBaseDelta(last_timestamp_)
                                .RoundDownTo(TimeDelta::Millis(1));
    // Protect against assigning current_offset_ negative value.
    if (delta < Timestamp::Zero() - current_offset_) {
      RTC_LOG(LS_WARNING) << "Unexpected feedback timestamp received.";
      current_offset_ = feedback_receive_time;
    } else {
      current_offset_ += delta;
    }
  }
  last_timestamp_ = feedback.GetBaseTime();

  std::vector<PacketResult> packet_result_vector;
  packet_result_vector.reserve(feedback.GetPacketStatusCount());

  size_t failed_lookups = 0;
  size_t ignored = 0;
  TimeDelta packet_offset = TimeDelta::Zero();
  for (const auto& packet : feedback.GetAllPackets()) {
    int64_t seq_num = seq_num_unwrapper_.Unwrap(packet.sequence_number());

    if (seq_num > last_ack_seq_num_) {
      // Starts at history_.begin() if last_ack_seq_num_ < 0, since any valid
      // sequence number is >= 0.
      for (auto it = history_.upper_bound(last_ack_seq_num_);
           it != history_.upper_bound(seq_num); ++it) {
        in_flight_.RemoveInFlightPacketBytes(it->second);
      }
      last_ack_seq_num_ = seq_num;
    }

    auto it = history_.find(seq_num);
    if (it == history_.end()) {
      ++failed_lookups;
      continue;
    }

    if (it->second.sent.send_time.IsInfinite()) {
      // TODO(srte): Fix the tests that makes this happen and make this a
      // DCHECK.
      RTC_DLOG(LS_ERROR)
          << "Received feedback before packet was indicated as sent";
      continue;
    }

    PacketFeedback packet_feedback = it->second;
    if (packet.received()) {
      packet_offset += packet.delta();
      packet_feedback.receive_time =
          current_offset_ + packet_offset.RoundDownTo(TimeDelta::Millis(1));
      // Note: Lost packets are not removed from history because they might be
      // reported as received by a later feedback.
      history_.erase(it);
    }
    if (packet_feedback.network_route == network_route_) {
      PacketResult result;
      result.sent_packet = packet_feedback.sent;
      result.receive_time = packet_feedback.receive_time;
      packet_result_vector.push_back(result);
    } else {
      ++ignored;
    }
  }

  if (failed_lookups > 0) {
    RTC_LOG(LS_WARNING) << "Failed to lookup send time for " << failed_lookups
                        << " packet" << (failed_lookups > 1 ? "s" : "")
                        << ". Send time history too small?";
  }
  if (ignored > 0) {
    RTC_LOG(LS_INFO) << "Ignoring " << ignored
                     << " packets because they were sent on a different route.";
  }

  return packet_result_vector;
}

         获取包的相对到达时间后再调用 DelayBasedBwe::IncomingPacketFeedbackVector 分析延时梯度变化,关键调用流程为:DelayBasedBwe::IncomingPacketFeedbackVector -> DelayBasedBwe::IncomingPacketFeedback -> InterArrival::ComputeDeltas。

b. Trendline estimator

        REMB-GCC 中是使用的是卡尔曼滤波计算延时梯度的变化,而在 TFB-GCC 中使用的是线性滤波计算累计延时梯度的变化趋势,即通过最小二乘法拟合一堆样本点 (x, y) 的关系,通过直线斜率判断变化趋势。代入到拟合直线方程中,x 相当于时间,y 相当于平滑后的累计延时梯度。

        得到线性滤波的斜率后再调用 TrendlineEstimator::Detect 判断当前带宽的使用状态,TrendlineEstimator::Detect 根据累计延时梯度的趋势 trend 与动态阈值的大小关系判断当前带宽处于 overuse/normal/underuse 状态,动态阈值 threshold_ 将在下一小节 Adaptive threshold 中讲解。

a. 如果 trend > threshold_,说明网络网络拥塞队列在增大,目前处于拥塞状态,如果拥塞持续时间大于 overusing_time_threshold_,并且延时梯度比上一次延时梯度大,判断处于 overuse 状态,注意不是一旦大于阈值就判断处于 overuse,需要持续一段时间并且延时梯度在变大才判断处于 overuse

b. 如果 trend < -threshold_,说明网络拥塞队列在变小,拥塞情况在改善,判断处于 underuse 状态

c. 如果 -threshold_ <= trend <= threshold_,判断处于 normal 状态

(3)Adaptive threshold

        如上所述,TrendlineEstimator 通过比较累计延时梯度的变化与阈值的大小关系判断当前的带宽使用状况,理想网络情况下延时梯度为 0,但是正常的带宽占用情况下,延时梯度也可能在 0 上下波动,但是累计延时梯度应该趋近于 0,因此累计延时梯度的变化趋近于 0,因此想根据累计延时梯度的变化来判断带宽使用状况,阈值的设置很重要,如果阈值是固定值,设置太大可能检测不到网络拥塞,设置太小可能又太过敏感,WebRTC 使用了一种自适应动态阈值的方式。

计算方式:threshold(t(i)) = threshold(t(i-1)) + k * [ t(i) - t(i-1) ] * [ | trend(t(i)) | - threshold(t(i-1)) ]

其中 k 表示变化率,当 | trend(t(i)) | < threshold(t(i-1)) 时,k 值为 0.039,否则 k 值为 0.0087,

threshold(t(i)) 表示当我们计算第 i 个包后需要新确定的阈值,threshold(t(i-1)) 表示计算第 i-1 个包后确定的阈值,t(i) - t(i-1) 表示两个包计算延时梯度的时间差,trend(t(i)) 表示当前算出的延时梯度的趋势(经过放大后的值)。

(4)Rate controller

通过判断当前处于带宽的何种使用状态后,需要根据当前状态对最大码率值做出调整,如下图所示。

当处于 overuse 状态,对应处于 Decr 状态,此时应该降低最大码率值,降低为过去 500ms 时间窗内最大 acked_bitrate 的 0.85 倍,acked_bitrate 可以通过 RTCP 反馈报文的包接收情况并结合本地维护的发送列表得到

当处于 underuse 状态,对应 Hold 状态,此时应该维持当前最大码率不变 

当处于 normal 状态,对应 Incr,此时可以适当增大码率,增大为原来最大码率值的 1.08 倍

WebRTC 对应的 Rate Controller 调整最大码率的代码如下。

void AimdRateControl::ChangeBitrate(const RateControlInput& input,
                                    Timestamp at_time) {
  absl::optional<DataRate> new_bitrate;
  DataRate estimated_throughput =
      input.estimated_throughput.value_or(latest_estimated_throughput_);
  if (input.estimated_throughput)
    latest_estimated_throughput_ = *input.estimated_throughput;

  // An over-use should always trigger us to reduce the bitrate, even though
  // we have not yet established our first estimate. By acting on the over-use,
  // we will end up with a valid estimate.
  if (!bitrate_is_initialized_ &&
      input.bw_state != BandwidthUsage::kBwOverusing)
    return;

  ChangeState(input, at_time);

  // We limit the new bitrate based on the troughput to avoid unlimited bitrate
  // increases. We allow a bit more lag at very low rates to not too easily get
  // stuck if the encoder produces uneven outputs.
  const DataRate troughput_based_limit =
      1.5 * estimated_throughput + DataRate::KilobitsPerSec(10);

  switch (rate_control_state_) {
    case kRcHold:
      break;

    case kRcIncrease:
      if (estimated_throughput > link_capacity_.UpperBound())
        link_capacity_.Reset();

      // Do not increase the delay based estimate in alr since the estimator
      // will not be able to get transport feedback necessary to detect if
      // the new estimate is correct.
      // If we have previously increased above the limit (for instance due to
      // probing), we don't allow further changes.
      if (current_bitrate_ < troughput_based_limit &&
          !(send_side_ && in_alr_ && no_bitrate_increase_in_alr_)) {
        DataRate increased_bitrate = DataRate::MinusInfinity();
        if (link_capacity_.has_estimate()) {
          // The link_capacity estimate is reset if the measured throughput
          // is too far from the estimate. We can therefore assume that our
          // target rate is reasonably close to link capacity and use additive
          // increase.
          DataRate additive_increase =
              AdditiveRateIncrease(at_time, time_last_bitrate_change_);
          increased_bitrate = current_bitrate_ + additive_increase;
        } else {
          // If we don't have an estimate of the link capacity, use faster ramp
          // up to discover the capacity.
          DataRate multiplicative_increase = MultiplicativeRateIncrease(
              at_time, time_last_bitrate_change_, current_bitrate_);
          increased_bitrate = current_bitrate_ + multiplicative_increase;
        }
        new_bitrate = std::min(increased_bitrate, troughput_based_limit);
      }

      time_last_bitrate_change_ = at_time;
      break;

    case kRcDecrease: {
      DataRate decreased_bitrate = DataRate::PlusInfinity();

      // Set bit rate to something slightly lower than the measured throughput
      // to get rid of any self-induced delay.
      decreased_bitrate = estimated_throughput * beta_;
      if (decreased_bitrate > current_bitrate_ && !link_capacity_fix_) {
        // TODO(terelius): The link_capacity estimate may be based on old
        // throughput measurements. Relying on them may lead to unnecessary
        // BWE drops.
        if (link_capacity_.has_estimate()) {
          decreased_bitrate = beta_ * link_capacity_.estimate();
        }
      }
      if (estimate_bounded_backoff_ && network_estimate_) {
        decreased_bitrate = std::max(
            decreased_bitrate, network_estimate_->link_capacity_lower * beta_);
      }

      // Avoid increasing the rate when over-using.
      if (decreased_bitrate < current_bitrate_) {
        new_bitrate = decreased_bitrate;
      }

      if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {
        if (!new_bitrate.has_value()) {
          last_decrease_ = DataRate::Zero();
        } else {
          last_decrease_ = current_bitrate_ - *new_bitrate;
        }
      }
      if (estimated_throughput < link_capacity_.LowerBound()) {
        // The current throughput is far from the estimated link capacity. Clear
        // the estimate to allow an immediate update in OnOveruseDetected.
        link_capacity_.Reset();
      }

      bitrate_is_initialized_ = true;
      link_capacity_.OnOveruseDetected(estimated_throughput);
      // Stay on hold until the pipes are cleared.
      rate_control_state_ = kRcHold;
      time_last_bitrate_change_ = at_time;
      time_last_bitrate_decrease_ = at_time;
      break;
    }
    default:
      assert(false);
  }

  current_bitrate_ = ClampBitrate(new_bitrate.value_or(current_bitrate_));
}

        最后将基于延时梯度预估的最大码率值保存到 SendSideBandwidthEstimation 的 delay_based_limit_ 变量中。

(2)基于丢包率的带宽预估

        发送端基于丢包的带宽预估思想主要是根据丢包率大小来判断是否拥塞。

        当丢包率大于 10% 时认为拥塞,此时应该主动降低发送码率减少拥塞;当丢包率小于 2% 时认为网络状况较好,可以适当提高发送码率,探测是否有更多的可用带宽;当丢包率介于 2% ~ 10% 时认为网络状况一般,此时保持与上一次相同的发送码率即可。

        对于丢包率的获取,发送端通过 RTCP RR 报文的丢包数和接收到的最大序号包数来判断丢包率,RR 报文格式和字段含义如下所示。

        WebRTC 接收 RR 报文并根据丢包率预估带宽的代码如下所示。

最后 UpdateTargetBitrate 中会取根据丢包得到的预估值和根据延时梯度得到的预估值中的较小值作为最终预估的最大码率。

确定完目标码率后会更新到 pacer,fec,编码模块中发挥作用。

三. 参考资料

RFC transport wide cc extensions

小议WebRTC拥塞控制算法:GCC介绍

Analysis and Design of the Google Congestion Control for Web Real-time

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

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

相关文章

新手必看!jenkins邮件发送配置,一教就会!

最近刚学习jenkins&#xff0c;在配置邮件发送的时候&#xff0c;踩了很多坑&#xff0c;各种百度查询&#xff0c;调试了大半天&#xff0c;终于成功解决 &#xff01;特此记录&#xff01; 遇到最让我头痛的问题&#xff0c;就是明明控制台显示邮件发送成功&#xff0c;但是…

【STM32笔记】HAL库低功耗模式配置(ADC唤醒无法使用的解决方案)

【STM32笔记】HAL库低功耗模式配置&#xff08;ADC唤醒无法使用的解决方案&#xff09; 理论转载&#xff1a; leung-manwah.blog.csdn.net/article/details/114675725 一、低功耗模式简介 系统提供了多个低功耗模式&#xff0c;可在 CPU 不需要运行时&#xff08;例如等待外…

Python实现PSO粒子群优化循环神经网络LSTM分类模型项目实战

说明&#xff1a;这是一个机器学习实战项目&#xff08;附带数据代码文档视频讲解&#xff09;&#xff0c;如需数据代码文档视频讲解可以直接到文章最后获取。 1.项目背景 PSO是粒子群优化算法&#xff08;Particle Swarm Optimization&#xff09;的英文缩写&#xff0c;是一…

配置别名 配置alias

Linux: ~/.bashrc MAC bash 的配置文件是 ~/.bash_profile zsh的配置文件是~/.zsh 查看一下你的shell类型 terminal->performance->shells open with 方案是 1. 创建新的文件存放alias cd ~ vi .myalias edit .myalias esc :wq保存 2. add config to ~/.zshrc …

Java数据结构与Java算法学习Day09---并查集(简略笔记记录)

目录 并查集的功能&#xff1a; 1.1并查集结构 136 1.2并查集API设计 137 1.3并查集代码的实现 137 1.3.1UF&#xff08;int N&#xff09;构造方法实现 1.3.2并查集代码测试 138 1.4并查集应用案例 139 1.5UF_Tree算法优化 139 1.5.1UF_tree API设计 1.5.2优化后的…

git基本操作

目录 1 git命令与状态 1.1 常用git命令 1.2 不那么常用的git命令 1.3 常见状态 2 一些概念 2.1 版本控制 2.2 git简介 2.3 开源许可协议 3 常见git操作 3.1 下载git 3.2 安装git 3.3 配置用户信息 3.4 初始化仓库 3.5 查看git仓库状态 3.6 将文件…

手机软件测试用例设计

实例讲解手机软件测试用例设计 实例讲解手机软件测试用例设计,测试伴随在整个手机软件开发的各个阶段中&#xff0c;测试质量的高低直接关系到手机软件的可用性&#xff0c;友好性&#xff0c;可靠性。可以说&#xff0c;测试环节是手机软件开发的重要环节&#xff0c;是整个开…

JavaScript大作业:基于HTML实现紫色化妆品包装设计公司企业网站

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Linux与Nginx

Linux 一、Linux的概述&#xff1a; Linux是基于Unix的&#xff0c;Linux是一种自由和开放源码的操作系统&#xff0c;存在着许多不同的Linux版本&#xff0c;但它们都使用了Linux内核。Linux可安装在各种计算机硬件设备中&#xff0c;比如手机、平板电脑、路由器、台式计算机…

前端学习教程-SVG描边属性

前端学习教程-SVG描边属性&#xff0c;SVG提供了大量的笔画属性&#xff0c;实现各种各样的描边效果。我们来介绍一下&#xff1a; stroke&#xff1a;笔画属性&#xff0c; stroke-width&#xff1a;笔画宽度属性&#xff0c; stroke-linecap&#xff1a;笔画线帽属性&…

一文读懂如何使用PyCharm为飞桨提PR

本文以使用PyCharm进行飞桨中文文档修复为例&#xff0c;详细介绍了如何给飞桨提交PR&#xff08;Pull Request&#xff09;。 背景介绍 关于飞桨框架 飞桨框架是一个开源深度学习框架&#xff0c;源代码公开在GitHub上的PaddlePaddle中。该目录下包括飞桨核心框架代码库Pa…

51单片机——IO拓展(串转并)-74HC595 小白详解

74HC595 芯片介绍 74HC595 是一个 8 位串行输入、并行输出的位移缓存器&#xff0c;其中并行输出为三 态输出&#xff08;即高电平、低电平和高阻抗&#xff09;。芯片管脚及功能说明如下&#xff1a;图1 上面两张都是 74HC595 芯片管脚图&#xff0c;细心的朋友就会发现左侧的…

python+django在线考试系统39n99

目 录 目 录 III 第一章 概述 1 1.1 研究背景 1 1.2 开发意义 1 1.3 研究现状 1 1.4 研究内容 2 1.5 论文结构 2 第二章 开发技术介绍 1 第三章 系统分析 1 3.1 可行性分析 1 3.1.1 技术可行性 1 3.1.2 操作可行性 1 3.1.3 经济可行性 …

STM32频率测量

频率测量是个最基本的且常见的工业需求. 但是这种简单的需求却不是那么的好实现. 总体来看, 目前的单片机还是有很大的改进空间. 很少有频率测量能够覆盖所有的频率范围. 而使用 STM32F103 性能有限. 根据待测频率, 我分成低中高, 三个阶段. 分别对应着3种不同的测量方法. 低频…

springboot 协同办公OA管理系统源码带文字安装教程【免费分享】

Java协同办公OA管理系统源码带文字安装教程 文末获取源码&#xff01; 框架&#xff1a;springboot freemark jpa mybatis mysql 环境&#xff1a;jdk8 mysql5.7 IntelliJ IDEA maven nginx 宝塔面板 这是一套OA办公系统&#xff0c;使用Maven作项目管理&#xff0c;基…

NETCAD GIS快速而简单的搜索引擎

NETCAD GIS快速而简单的搜索引擎 NETCAD GIS是一种支持国际标准的CAD和GIS软件&#xff0c;旨在为用户设计和使用地理信息系统。它还提供了CAD、GIS和光栅与未来的强大连接。 该程序也是一个快速而简单的搜索引擎&#xff0c;只能通过向查看器键入几个单词来访问&#xff0c;并…

嵌入式开发学习之--中断应用概览

文章目录前言一、什么是NVIC1.1NVIC 简介1.2 NVIC 寄存器简介二、中断优先级2.1优先级定义2.2优先级分组三、中断编程总结前言 中断是系统逻辑十分常用的&#xff0c;学好中断&#xff0c;以及中断优先级的合理配置&#xff0c;才能使自己的项目更加稳定。 提示&#xff1a;以…

rdd 相关信息整理

RDD 是什么&#xff1f; 官网描述 A Resilient Distributed Dataset (RDD), the basic abstraction in Spark. Represents an immutable, partitioned collection of elements that can be operated on in parallelRDD 是三个单词的首字母缩写&#xff0c;它表示弹性分布式数…

火爆全网的ChatGPT上手体验,文尾免费送账号!

文章目录1. ChatGPT公开信息2. ChatGPT上手体验3. ChatGPT目前趋势4. 福利派送1. ChatGPT公开信息 近期关于网路上对 ChatGPT&#xff08;OpenAI发布的一款人工智能对话引擎&#xff09;的讨论比较多。官方对ChatGPT的定义如下&#xff1a; We’ve trained a model called Cha…

[Linux打怪升级之路]-文件操作

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正。 目录 一、认识操…