2024年3月

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍如何运用
QUdpSocket
组件实现基于UDP的网络通信功能。


QTcpSocket
组件功能类似,
QUdpSocket
组件是 Qt 中用于实现用户数据报协议(UDP,User Datagram Protocol)通信的类。UDP 是一种无连接的、不可靠的数据传输协议,它不保证数据包的顺序和可靠性,但具有低延迟和简单的特点。

以下是
QUdpSocket
类的完整函数及其简要解释:

函数 描述
QUdpSocket(QObject *parent = nullptr) 构造函数,创建一个新的
QUdpSocket
对象。
~QUdpSocket() 析构函数,释放
QUdpSocket
对象及其资源。
void bind(const QHostAddress &address, quint16 port, BindMode mode = DefaultForPlatform) 将套接字绑定到指定的本地地址和端口。
void close() 关闭套接字。
bool joinMulticastGroup(const QHostAddress &groupAddress, const QNetworkInterface &iface = QNetworkInterface()) 加入多播组。
bool leaveMulticastGroup(const QHostAddress &groupAddress, const QNetworkInterface &iface = QNetworkInterface()) 离开多播组。
qint64 pendingDatagramSize() const 返回下一个待读取的数据报的大小。
qint64 readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr) 读取数据报。
QByteArray readDatagram(qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr) 读取数据报,返回
QByteArray
对象。
qint64 writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port) 发送数据报。
qint64 writeDatagram(const QByteArray &datagram, const QHostAddress &address, quint16 port) 发送数据报,接受
QByteArray
对象。
QAbstractSocket::SocketState state() const 返回套接字的当前状态。
QAbstractSocket::SocketType socketType() const 返回套接字的类型。
bool isValid() const 如果套接字有效,则返回
true
;否则返回
false
int error() const 返回套接字的当前错误代码。
QHostAddress localAddress() const 返回本地地址。
quint16 localPort() const 返回本地端口。
int readBufferSize() const 返回读取缓冲区的大小。
void setReadBufferSize(int size) 设置读取缓冲区的大小。
QNetworkInterface multicastInterface() const 返回多播组的网络接口。
void setMulticastInterface(const QNetworkInterface &iface) 设置多播组的网络接口。
bool hasPendingDatagrams() const 如果有待读取的数据报,则返回
true
;否则返回
false
bool isReadable() const 如果套接字可读,则返回
true
;否则返回
false
bool isWritable() const 如果套接字可写,则返回
true
;否则返回
false
bool setSocketDescriptor(int socketDescriptor, QUdpSocket::SocketState socketState = ConnectedState, QIODevice::OpenMode openMode = ReadWrite) 设置套接字描述符。
int socketDescriptor() const 返回套接字描述符。
bool waitForReadyRead(int msecs = 30000) 等待套接字可读取数据。
bool waitForBytesWritten(int msecs = 30000) 等待套接字已写入指定字节数的数据。
void ignoreSslErrors(const QList<QSslError> &errors) 忽略 SSL 错误。
void abort() 强制关闭套接字。
QNetworkProxy proxy() const 返回套接字的代理设置。
void setProxy(const QNetworkProxy &networkProxy) 设置套接字的代理设置。
QString errorString() const 返回套接字的错误消息字符串。

这些函数提供了在 UDP 通信中使用
QUdpSocket
的各种功能,包括绑定、发送和接收数据报、设置和获取套接字的状态等。

1.1 初始化部分

在初始化部分我们首先通过
new QUdpSocket
来实现创建UDP对象,
QUdpSocket
构造函数的函数原型如下:

QUdpSocket::QUdpSocket(QObject * parent = nullptr)

如上构造函数创建一个新的
QUdpSocket
对象。如果提供了
parent
参数,则会将新创建的
QUdpSocket
对象添加到
parent
对象的子对象列表中,并且在
parent
对象被销毁时自动销毁
QUdpSocket
对象。如果没有提供
parent
参数,则
QUdpSocket
对象将不会有父对象,并且需要手动管理其生命周期。

初始化结束后,则下一步需要调用
bind()

bind()
函数是
QUdpSocket
类的一个成员函数,用于将套接字绑定到特定的本地地址和端口。它的函数原型如下:

void QUdpSocket::bind(const QHostAddress &address, quint16 port, BindMode mode = DefaultForPlatform)
  • address
    :要绑定的本地地址,通常是
    QHostAddress::Any
    ,表示绑定到所有可用的网络接口。
  • port
    :要绑定的本地端口号。
  • mode
    :绑定模式,指定套接字的行为。默认值是
    DefaultForPlatform
    ,表示使用平台默认的绑定模式。

该函数允许
QUdpSocket
在本地网络接口上监听传入的数据报。一旦调用了
bind()
函数,
QUdpSocket
就可以接收来自指定地址和端口的数据报。

在调用
bind()
函数之后,如果成功绑定了指定的地址和端口,套接字将处于
BoundState
状态。如果出现错误,可以通过检查
error()
函数获取错误代码,并通过
errorString()
函数获取错误消息。

接着我们通过
connect()
函数依次绑定套接字到
stateChanged
状态改变信号,以及
readyRead()
读取信号上,这段初始化代码如下所示;

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    udpSocket=new QUdpSocket(this);

    // 生成随机整数 包含2000 - 不包含65534
    int randomInt = QRandomGenerator::global()->bounded(2000, 65534);

    if(udpSocket->bind(randomInt))
    {
        this->setWindowTitle(this->windowTitle() + " | 地址: " + getLocalAddress() + " 绑定端口:" + QString::number(udpSocket->localPort()));
    }

    connect(udpSocket,SIGNAL(stateChanged(QAbstractSocket::SocketState)),this,SLOT(onSocketStateChange(QAbstractSocket::SocketState)));
    onSocketStateChange(udpSocket->state());
    connect(udpSocket,SIGNAL(readyRead()),this,SLOT(onSocketReadyRead()));
}

接着切换到读取信号所对应的槽函数上,
onSocketReadyRead
是我们自定义的一个槽,该槽函数功能如下所示;

// 读取收到的数据报
void MainWindow::onSocketReadyRead()
{
    while(udpSocket->hasPendingDatagrams())
    {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());

        QHostAddress peerAddr;
        quint16 peerPort;
        udpSocket->readDatagram(datagram.data(),datagram.size(),&peerAddr,&peerPort);
        QString str=datagram.data();

        QString peer="[消息来自 " + peerAddr.toString()+":"+QString::number(peerPort)+"] | ";

        ui->plainTextEdit->appendPlainText(peer+str);
    }
}

首先在代码中调用
pendingDatagramSize
函数,
pendingDatagramSize()

QUdpSocket
类的一个成员函数,用于获取下一个待读取的数据报的大小。它的函数原型如下:

qint64 QUdpSocket::pendingDatagramSize() const

该函数返回一个
qint64
类型的值,表示下一个待读取的数据报的大小(以字节为单位)。如果没有待读取的数据报,或者发生了错误,该函数将返回 -1。

通常,可以在调用
readDatagram()
函数之前调用
pendingDatagramSize()
函数来获取下一个待读取的数据报的大小。这样可以为数据缓冲区分配正确大小的空间,以确保完整地读取数据报。

当有了待读取字节后,接着就可以直接通过调用
readDatagram
函数来从套接字中读取数据报,
readDatagram()

QUdpSocket
类的一个成员函数,它有几个重载形式,其中最常用的是:

qint64 QUdpSocket::readDatagram(char * data, qint64 maxSize, QHostAddress * address = nullptr, quint16 * port = nullptr)

该函数用于读取数据报并将其存储到指定的缓冲区
data
中,最多读取
maxSize
个字节的数据。可选参数
address

port
用于返回数据报的源地址和端口号。如果不需要这些信息,可以将它们设置为
nullptr

函数返回实际读取的字节数,如果发生错误,返回 -1。要查看错误信息,可以使用
error()

errorString()
函数。

另外,还有一个更简单的重载形式:

QByteArray QUdpSocket::readDatagram(qint64 maxSize, QHostAddress * address = nullptr, quint16 * port = nullptr)

这个重载函数直接返回一个
QByteArray
对象,其中包含了读取的数据报。

1.2 单播与广播消息

单播(Unicast)和广播(Broadcast)是网络通信中常见的两种数据传输方式,它们在数据包的传输范围和目标数量上有所不同。

单播(Unicast)

单播是一种一对一的通信方式,其中数据包从一个发送者传输到一个接收者。在单播通信中,数据包只发送到目标主机的网络接口,并且只有目标主机能够接收和处理这个数据包。

  • 一对一通信:每个数据包只有一个发送者和一个接收者。
  • 目标明确:数据包只发送到特定的目标主机,其他主机不会接收到这个数据包。
  • 点到点通信:适用于直接通信的场景,如客户端与服务器之间的通信。

当按钮发送消息被点击后,则是一种单播模式,通常该模式需要得到目标地址与端口号,并通过调用
writeDatagram
来实现数据的发送,该函数通过传入三个参数,分别是发送字符串,目标地址与目标端口来实现一对一推送。

void MainWindow::on_pushButton_clicked()
{
    QHostAddress targetAddr(ui->lineEdit_addr->text());
    QString portString = ui->lineEdit_port->text();
    quint16 targetPort = portString.toUShort();

    QString msg=ui->lineEdit_msg->text();
    QByteArray str=msg.toUtf8();

    // 发送数据报
    udpSocket->writeDatagram(str,targetAddr,targetPort);
    ui->plainTextEdit->appendPlainText("[单播消息] | " + msg);
}

广播(Broadcast)

广播是一种一对多的通信方式,其中数据包从一个发送者传输到同一网络中的所有主机。在广播通信中,数据包被发送到网络中的所有主机,并且所有的主机都能够接收和处理这个数据包。

  • 一对多通信:每个数据包有一个发送者,但可以有多个接收者。
  • 目标不明确:数据包被发送到网络中的所有主机,不需要知道接收者的具体地址。
  • 广播域:在局域网中进行广播,只有在同一广播域内的主机才能接收到广播消息。
  • 网络负载:在大型网络中使用广播可能会产生大量的网络流量,影响网络性能。

当按钮广播消息被点击后,则同样是调用
writeDatagram
函数与,唯一的区别在于第二个参数并未指定地址,而是使用了
QHostAddress::Broadcast
来代替,意味着只要端口是一致的则对所有的客户推送消息,其他保持不变。

void MainWindow::on_pushButton_2_clicked()
{
    // 广播地址
    QString portString = ui->lineEdit_port->text();
    quint16 targetPort = portString.toUShort();

    QString  msg=ui->lineEdit_msg->text();
    QByteArray str=msg.toUtf8();
    udpSocket->writeDatagram(str,QHostAddress::Broadcast,targetPort);

    ui->plainTextEdit->appendPlainText("[广播消息] | " + msg);
}

读者可自行运行两次客户端,此时的端口将会随机分配,当指定对端端口后就可以向其发送数据,如下图所示;具体实现细节,请参考文章附件。

InfluxDB、Grafana、node_exporter、Prometheus搭建压测平台

我们的压测平台的架构图如下:

压测架构图

配置docker环境

1)yum 包更新到最新

sudo yum update

更新yum包

如果有提示,直接输入y,回车。

2)安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

安装所需软件包

3)设置yum源为阿里云

配置yum源的代理,类似于maven镜像仓库,加速下载软件。

sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

配置yum源的代理

4)安装docker

sudo yum install docker-ce
# 启动
systemctl start docker

安装并启动docker

5)安装后查看docker版本

docker -v

docker版本

安装InfluxDB

1)下载InfluxDB的镜像:

docker pull influxdb:1.8

下载InfluxDB镜像

2)启动InfluxDB的容器,并将端口 8083 和 8086 映射出来:

docker run -d --name influxdb -p 8086:8086 -p 8083:8083 influxdb:1.8

启动InfluxDB的容器

3)进入容器内部,创建名为jmeter的数据库:

进入 jmeter-influx 容器

docker exec -it influxdb /bin/bash
  • 输入
    influx
    命令,即可进入 influx 操作界面
  • 输入
    create database jmeter
    命令,创建名为 jmeter 的数据库
  • 输入
    show databases
    命令,查看数据库创建成功

创建jmeter数据库

4)使用jmeter库, select 查看数据,这个时候是没有数据的:

  • 输入
    use jmeter
    命令,应用刚才创建的数据库
  • 输入
    select * from jmeter
    命令,查询库中有哪些数据
> use jmeter
> select * from jmeter

使用jmeter库

设置JMeter脚本后置监听器

添加后置监听器:

添加后置监听器

设置后端监听器

设置后端监听器

配置如下:

配置

  • influxdbUrl:需要改为自己influxdb的部署ip和映射端口,db后面跟的是刚才创建的数据库名称
  • application:可根据需要自由定义,只是注意后面在 grafana 中选对即可
  • measurement:表名,默认是 jmeter ,也可以自定义
  • summaryOnly:选择true的话就只有总体的数据。false会记录总体数据,然后再将每个transaction都分别记录
  • samplersRegex:样本正则表达式,将匹配的样本发送到数据库
  • percentiles:响应时间的百分位P90、P95、P99
  • testTitle:events表中的text字段的内容
  • eventTags:任务标签,配合Grafana一起使用

运行测试

点击运行:

点击运行

查看数据库中的数据:

看到数据

表示配置成功。

安装Grafana

1)下载Grafana镜像:

docker pull grafana/grafana

下载Grafana镜像

2)启动Grafana容器:

启动Grafana容器,将3000端口映射出来

docker run -d --name grafana -p 3000:3000 grafana/grafana

启动Grafana容器

3)验证部署成功

网页端访问http://安装grafana的ip:3000验证部署成功

image-20240319073928218

默认账户密码:admin\admin

整合数据源

使用admin\admin登录

1)选择添加数据源

添加数据源

2)选择添加数据源

image-20240319073950010

3)配置数据源

配置数据源1

配置数据库,数据库名要和创建的数据库、Jmeter连接的数据库保持一致。

配置数据库

保存

导入模板

寻找模板

进入Grafana官网https://grafana.com/

在首页拉到最下方:

网站截图

搜索Jmeter相关的

搜索模板

我们使用如下两个模板:

  • Apache JMeter Dashboard
    • dashboad-ID:5496
  • JMeter Dashboard(3.2 and up)
    • dashboad-ID:3351

导入模板

点击导入模板

输入模板id

输入模板id,并load

修改名字和数据源:

截图

可以看到如下的界面

界面

保存

保存

安装node_exporter

# 下载 可以直接下载下来传到服务器
wget -c https://github.com/prometheus/node_exporter/releases/download/v0.18.1/node_exporter-0.18.1.linux-amd64.tar.gz
# 解压
tar zxvf node_exporter-0.18.1.linux-amd64.tar.gz -C /usr/local/sjdwz_test/
# 启动
cd /usr/local/sjdwz_test/node_exporter-0.18.1.linux-amd64
nohup ./node_exporter > node.log 2>&1 &

启动node_exporter

看到如下界面即可:

看到如下界面

安装Prometheus

1)下载解压运行

# 下载 可以下载压缩包传到如武器
wget -c https://github.com/prometheus/prometheus/releases/download/v2.15.1/prometheus-2.15.1.linux-amd64.tar.gz
# 解压
tar zxvf prometheus-2.15.1.linux-amd64.tar.gz -C /usr/local/sjdwz_test/
cd /usr/local/sjdwz_test/prometheus-2.15.1.linux-amd64
# 运行
nohup ./prometheus > prometheus.log 2>&1 &

测试浏览

2)配置prometheus

在prometheus.yml中加入如下配置:

scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'sjdwz-Linux'
    static_configs:
      - targets: ['192.168.225.206:9100']

配置

3)测试Prometheus

重启Prometheus,测试是否安装配置成功

配置

测试

3)在Grafana中配置Prometheus的数据源:

image-20240319073858418

保存即可。

4)Grafana导入Linux展示模板

导入Linux系统dashboard

  • Node Exporter for Prometheus Dashboard EN 20201010


    • dashboard-ID: 11074

      导入模板

      模板

      展示

  • Node Exporter Dashboard


    • dashboard-ID: 16098

TagProvider

[LogProperties] 与 [LogPropertyIgnore] 如果用在DTO不存在任何问题,如果用在Domain实体上,可能有点混乱。

您可能不希望因日志记录问题而使您的域模型变得混乱。对于这种情况,可以使用[TagProvider]属性来丰富日志。

我们仍然使用前面用的Network实体,这次它不再使用[LogPropertyIgnore]属性:

public classNetWorkInfo
{
public string IPAddress { get; set; }public int Port { get; set; }
}

相反,我们定义了一个专用的“TagProvider”实现。

不需要实现接口或任何类,只需要正确的方法格式。

下面是我们给Network对象的标签提供程序,我们只记录字段IPAddres字段,如下所示:


internal static classNetWorkInfoTagProvider
{
//

前言

BlazorChartjs是一个在Blazor中使用Chart.js的库(支持Blazor WebAssembly和Blazor Server两种模式),它提供了简单易用的组件来帮助开发者快速集成数据可视化图表到他们的 Blazor 应用程序中。本文我们将一起来学习一下在Blazor中使用Chart.js快速创建图表。

Blazor是什么?

Blazor是一种新兴的Web应用程序框架,具有很大的潜力和发展前景。Blazor是在.NET和Razor上构建的用户界面框架,它采用了最新的Web技术和.NET框架优势,可以使用C# 编程语言编写Web 应用程序,它不仅可以提高开发效率,还可以提供更好的用户体验和更好的可维护性。

详细介绍可以看这篇文章:
全面的ASP.NET Core Blazor简介和快速入门

创建Blazor WebAssembly应用

创建名为
ChartjsExercise
的Blazor WebAssembly应用:

安装NuGet

安装
PSC.Blazor.Components.Chartjs
包:

添加以下脚本

打开
index.html
文件,在页面末尾添加以下脚本:

<script src="_content/PSC.Blazor.Components.Chartjs/lib/Chart.js/chart.js"></script>
<script src="_content/PSC.Blazor.Components.Chartjs/Chart.js" type="module"></script>

引入组件

打开你的
_Imports.razor
文件并添加以下内容:

@using PSC.Blazor.Components.Chartjs
@using PSC.Blazor.Components.Chartjs.Enums
@using PSC.Blazor.Components.Chartjs.Models
@using PSC.Blazor.Components.Chartjs.Models.Common
@using PSC.Blazor.Components.Chartjs.Models.Bar
@using PSC.Blazor.Components.Chartjs.Models.Bubble
@using PSC.Blazor.Components.Chartjs.Models.Doughnut
@using PSC.Blazor.Components.Chartjs.Models.Line
@using PSC.Blazor.Components.Chartjs.Models.Pie
@using PSC.Blazor.Components.Chartjs.Models.Polar
@using PSC.Blazor.Components.Chartjs.Models.Radar
@using PSC.Blazor.Components.Chartjs.Models.Scatter

柱状图

创建
BarSimple.razor
组件:

razor页面代码

@page "/BarSimple"
@using ChartjsExercise.Model
<h3>柱状图</h3>

<Chart Config="_config" @ref="_chart" Height="500px"></Chart>

@code {
    private BarChartConfig? _config;
    private Chart? _chart;

    protected override async Task OnInitializedAsync()
    {
        _config = new BarChartConfig()
            {
                Options = new Options()
                {
                    Responsive = true,
                    MaintainAspectRatio = false,
                    Plugins = new Plugins()
                    {
                        Legend = new Legend()
                        {
                            Align = Align.Center,
                            Display = true,
                            Position = LegendPosition.Right
                        }
                    },
                    Scales = new Dictionary<string, Axis>()
                {
                    {
                        Scales.XAxisId, new Axis()
                        {
                            Stacked = true,
                            Ticks = new Ticks()
                            {
                                MaxRotation = 0,
                                MinRotation = 0
                            }
                        }
                    },
                    {
                        Scales.YAxisId, new Axis()
                        {
                            Stacked = true
                        }
                    }
                }
                }
            };

        _config.Data.Labels = BarSimpleData.SimpleBarText;
        _config.Data.Datasets.Add(new BarDataset()
            {
                Label = "Value",
                Data = BarSimpleData.SimpleBar.Select(l => l.Value).ToList(),
                BackgroundColor = Colors.Palette1,
                BorderColor = Colors.PaletteBorder1,
                BorderWidth = 1
            });
    }
}

BarSimpleData

    public class BarSimpleData
    {
        public static List<string> SimpleBarText = new List<string>() { "一月", "二月", "三月", "四月", "五月", "六月", "七月" };
        public static List<DataItem> SimpleBar = new List<DataItem>()
        {
            new DataItem() { Name = "一月", Value = 65 },
            new DataItem() { Name = "二月", Value = 59 },
            new DataItem() { Name = "三月", Value = 80 },
            new DataItem() { Name = "四月", Value = 81 },
            new DataItem() { Name = "五月", Value = 56 },
            new DataItem() { Name = "六月", Value = 55 },
            new DataItem() { Name = "七月", Value = 40 }
        };
    }

展示效果

饼图

创建
PieSimple.razor
组件:

razor页面代码

@page "/PieSimple"
@using ChartjsExercise.Model
<h3>饼图</h3>

<Chart Config="_config" @ref="_chart" Height="500px"></Chart>

@code {
    private PieChartConfig? _config;
    private Chart? _chart;

    protected override async Task OnInitializedAsync()
    {
        _config = new PieChartConfig()
            {
                Options = new PieOptions()
                {
                    Responsive = true,
                    MaintainAspectRatio = false
                }
            };

        _config.Data.Labels = PieSimpleData.SimplePieText;
        _config.Data.Datasets.Add(new PieDataset()
            {
                Label = "数据集",
                Data = PieSimpleData.SimplePie.ToList(),
                BackgroundColor = Colors.PaletteBorder1,
                HoverOffset = 4
            });
    }
}

PieSimpleData

    public class PieSimpleData
    {
        public static List<string> SimplePieText = new List<string>() { "一月", "二月", "三月", "四月" };
        public static List<decimal?> SimplePie = new List<decimal?>() { 300, 50, 100, 20 };
    }

展示效果

折线图

创建
LineSimple.razor
组件:

razor页面代码

@page "/LineSimple"
@using ChartjsExercise.Model

<h3>折线图</h3>

<Chart Config="_config" @ref="_chart" Height="500px"></Chart>

@code {
    private LineChartConfig? _config;
    private Chart? _chart;

    protected override async Task OnInitializedAsync()
    {
        _config = new LineChartConfig()
        {
        };

        _config.Data.Labels = LineSimpleData.SimpleLineText;
        _config.Data.Datasets.Add(new LineDataset()
            {
                Label = "数据集",
                Data = LineSimpleData.SimpleLine.ToList(),
                BorderColor = Colors.PaletteBorder1.FirstOrDefault(),
                Tension = 0.1M,
                Fill = false,
                PointRadius = 15,
                PointStyle = PointStyle.Cross
            });
    }

    private void AddValue()
    {
        Random rd = new Random();
        _chart.AddData(new List<string?>() { "August" }, 0, new List<decimal?>() { rd.Next(0, 200) });
    }
}

LineSimpleData

    public class LineSimpleData
    {
        public static List<string> SimpleLineText = new List<string>() { "一月", "二月", "三月", "四月", "五月", "六月", "七月" };
        public static List<decimal?> SimpleLine = new List<decimal?>() { 65, 59, 80, 81, 86, 55, 40 };
        public static List<decimal?> SimpleLine2 = new List<decimal?>() { 33, 25, 35, 51, 54, 76, 60 };
        public static List<decimal?> SimpleLine3 = new List<decimal?>() { 53, 91, 39, 61, 39, 87, 23 };
    }

展示效果

配置菜单导航栏

在组件
NavMenu.razor
中配置:

<div class="top-row ps-3 navbar navbar-dark">
    <div class="container-fluid">
        <a class="navbar-brand" href="">ChartjsExercise</a>
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
            <span class="navbar-toggler-icon"></span>
        </button>
    </div>
</div>

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
    <nav class="flex-column">
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="BarSimple">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>柱状图
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="PieSimple">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>饼图
            </NavLink>
        </div>
        <div class="nav-item px-3">
            <NavLink class="nav-link" href="LineSimple">
                <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>折线图
            </NavLink>
        </div>
    </nav>
</div>

@code {
    private bool collapseNavMenu = true;

    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}

更多图表效果截图

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

Keil µVision5 IDE(
MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

2、学习目标

本文主要学习 FreeRTOS 事件组的相关知识,
包括事件组概述、事件组特征、创建事件组、操作事件组、删除事件组等知识

3、前提知识

3.1、什么是事件组?

事件组(event group)也是FreeRTOS中另外一种进程间通信技术,
事件组适用于多个事件触发一个或多个任务运行,可以实现事件的广播,还可以实现多个任务的同步运行
,如下所述

  • 事件组允许任务等待一个或多个事件的组合
  • 事件组会解除所有等待同一事件的任务的阻塞状态

3.1、事件组特征

3.1.1、事件组、事件标志和事件位

事件 “标志” 是一个布尔值(1 或 0),用于指示事件是否发生,事件 “组” 是一组事件标志,事件标志只能为 1 或 0 ,允许事件标志的状态存储在单个位中,并且事件组中所有事件标志的状态存储在单个变量中

事件组中每个事件标志的状态由 EventBits_t 类型变量中的单个位表示。因此,
事件标志也称为事件 “位” ,如果 EventBits_t 变量中的某个位设置为 1 ,则该位表示的事件已发生,否则如果 EventBits_t 变量中的某个位设置为 0 ,则该位表示的事件尚未发生

如下图所示显示了各个事件标志如何映射到 EventBits_t 类型变量中的各个位
(注释1)

3.1.2、EventBits_t 数据类型

一个事件组对象有一个变量类型为 EventBits_t 的内部变量用于存储事件标志位,该变量可以设置为 16 位或 32 位,具体由参数
configUSE_16_BIT_TICKS
所决定,当参数设置为 1 时,那么每个事件组包含 8 个可用的事件位(包括 8 个保留位),否则设置为 0 时,每个事件组包含 24 个可用的事件位(包括 8 个保留位)

3.1.3、多个任务访问

事件组本身就是对象,任何知道其存在的任务或 ISR 都可以访问它们。任意数量的任务可以在同一事件组中设置位,并且任意数量的任务可以从同一事件组中读取位

3.2、创建事件组

一个事件组在使用之前必须先创建
,如下所示为使用动态/静态内存分配创建一个事件组的 API 函数

/**
  * @brief  动态分配内存创建事件组函数
  * @retval 返回成功创建的事件组的句柄,返回NULL表示因内存空间不足创建失败
  */
EventGroupHandle_t xEventGroupCreate(void);

/**
  * @brief  静态分配内存创建事件组函数
  * @param  pxEventGroupBuffer:指向StaticEventGroup_t类型的变量,该变量用于存储事件组数据结构体
  * @retval 返回成功创建的事件组的句柄,返回NULL表示因pxEventGroupBuffer空间不足创建失败
  */
EventGroupHandle_t xEventGroupCreateStatic(
								StaticEventGroup_t *pxEventGroupBuffer);

3.3、操作事件组

FreeRTOS 提供了两组 API 来对事件组的某些位进行置位和清零两种操作,具体如下所示

/**
  * @brief  将事件组某些位置位
  * @param  xEventGroup:要设置位的事件组
  * @param  uxBitsToSet:指定要在事件组中设置的一个或多个位的按位值,例如设置为0x09表示置位3 和位0
  * @retval 调用 xEventGroupSetBits()返回时事件组的值
  */
EventBits_t xEventGroupSetBits(EventGroupHandle_t xEventGroup,
							   const EventBits_t uxBitsToSet);

/**
  * @brief  将事件组某些位清零
  * @param  xEventGroup:要在其中清除位的事件组
  * @param  uxBitsToSet:表示要在事件组中清除一个或多个位的按位值
  * @retval 返回清除指定位之前的事件组的值
  */
EventBits_t xEventGroupClearBits(EventGroupHandle_t xEventGroup,
								 const EventBits_t uxBitsToClear);

/**
  * @brief  上述两个函数的中断安全版本
  * @param  pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
  * @retval 消息已发送到RTOS守护进程任务,则返回pdPASS,否则将返回pdFAIL
  */
BaseType_t xEventGroupSetBitsFromISR(EventGroupHandle_t xEventGroup,
									 const EventBits_t uxBitsToSet,
									 BaseType_t *pxHigherPriorityTaskWoken);

BaseType_t xEventGroupClearBitsFromISR(EventGroupHandle_t xEventGroup,
									   const EventBits_t uxBitsToClear);

/*example1: 将事件组 EventGroup_Test 的位 1 和 3 置位*/
EventBits_t return_value;
return_value = xEventGroupSetBits(EventGroup_Test, 0x0A);

/*example2: 将事件组 EventGroup_Test 的位 0 和 2 清零*/
EventBits_t return_value;
return_value = xEventGroupClearBits(EventGroup_Test, 0x05);

同时 FreeRTOS 也提供了查询事件组当前值的 API 函数,具体如下所示

/**
  * @brief  读取事件组的当前值
  * @param  xEventGroup:正在查询的事件组
  * @retval 返回事件组当前的值
  */
EventBits_t xEventGroupGetBits(EventGroupHandle_t xEventGroup);

/**
  * @brief  上述函数的中断安全版本
  */
EventBits_t xEventGroupGetBitsFromISR(EventGroupHandle_t xEventGroup);

3.4、xEventGroupWaitBits() API 函数

FreeRTOS 关于事件组提出了等待事件组和事件组同步两个比较重要的 API 函数,分别对应两种不同的使用场景,
等待事件组主要用于使用事件组进行事件的管理,而另外一主要用于使用事件组进行任务间的同步
,接下来主要详细介绍两个函数的具体用法

xEventGroupWaitBits() API 函数允许任务读取事件组的值,并且可以选择在阻塞状态下等待事件组中的一个或多个事件位被设置(如果事件位尚未设置),如下所示为其具体的函数声明

/**
  * @brief  等待事件组中多个事件位表示的事件成立
  * @param  xEventGroup:所操作事件组的句柄
  * @param  uxBitsToWaitFor:所等待事件位的掩码,例如设置为0x05表示等待第0位和/或第2位
  * @param  xClearOnExit:pdTRUE表示事件组条件成立退出阻塞状态时将掩码指定的所有位清零;pdFALSE表示事件组条件成立退出阻塞状态时不将掩码指定的所有位清零
  * @param  xWaitForAllBits:pdTRUE表示等待掩码中所有事件位都置1,条件才算成立(逻辑与);pdFALSE表示等待掩码中所有事件位中一个置1,条件就成立(逻辑或)
  * @param  xTicksToWait:任务进入阻塞状态等待时间成立的超时节拍数
  * @retval 返回事件位等待完成设置或阻塞时间过期时的事件组值
  */
EventBits_t xEventGroupWaitBits(const EventGroupHandle_t xEventGroup,
								const EventBits_t uxBitsToWaitFor,
								const BaseType_t xClearOnExit,
								const BaseType_t xWaitForAllBits,
								TickType_t xTicksToWait);

3.4.1、
uxBitsToWaitFor

xWaitForAllBits
参数

调度程序用来确定任务是否进入阻塞状态以及任务何时离开阻塞状态的条件称为 “解除阻塞条件” 。解锁条件由
uxBitsToWaitFor

xWaitForAllBits
参数值的组合指定:

  • uxBitsToWaitFor
    指定要测试事件组中的哪些事件位
  • xWaitForAllBits
    指定是使用按位 OR 测试还是按位 AND 测试

如果调用 xEventGroupWaitBits() 时满足解锁条件,任务将不会进入阻塞状态,下表提供了导致任务进入阻塞状态或退出阻塞状态的条件示例。表中列出的值仅显示事件组和
uxBitsToWaitFor
值的最低有效的四个二进制位,其他位均假定为零

现有事件组值 uxBitsToWaitFor xWaitForAllBits 导致的结果
0000 0101 pdFALSE 由于事件组中的位 0 或位 2 均未设置,调用任务将进入阻塞状态,并且当事件组中的位 0 或位 2 被设置时,调用任务将离开阻塞状态
0100 0101 pdTRUE 调用任务将进入阻塞状态,因为事件组中的位 0 和位 2 未同时设置,并且当事件组中的位 0 和位 2 均设置时,调用任务将离开阻塞状态
0100 0110 pdFALSE 调用任务不会进入阻塞状态,因为 xWaitForAllBits 为 pdFALSE,并且 uxBitsToWaitFor 指定的两个位之一已在事件组中设置
0100 0110 pdTRUE 调用任务将进入阻塞状态,因为 xWaitForAllBits 为pdTRUE,并且事件组中仅已设置 uxBitsToWaitFor 指定的两个位之一。 当事件组中的位 2 和位 3 均被设置时,任务将离开阻塞状态

3.4.2、
xClearOnExit
参数

调用任务使用
uxBitsToWaitFor
参数指定要测试的位,并且调用任务可能需要在满足其解锁条件后将这些位清零。可以使用 xEventGroupClearBits() API 函数清除事件位,但使用该函数手动清除事件位将导致应用程序代码中出现竞争条件

因此提供
xClearOnExit
参数就是为了避免这些潜在的竞争条件。如果
xClearOnExit
设置为 pdTRUE,则事件位的测试和清除对于调用任务来说是一个原子操作(不能被其他任务或中断中断),
简单来说就是如果
xClearOnExit
设置为 pdTRUE,则调用任务退出后会将事件组所有位清零,否则不清零

如果 xEventGroupWaitBits() 由于满足调用任务的解锁条件而返回,则返回值是满足调用任务的解锁条件时事件组的值(如果 xClearOnExit 为 pdTRUE,则在自动清除任何位之前),在这种情况下,返回值也将满足解锁条件。如果 xEventGroupWaitBits() 因为 xTicksToWait 参数指定的退出阻塞时间到期而返回,则返回值为退出阻塞时间到期时事件组的值,在这种情况下,返回值将不满足解锁条件

3.5、xEventGroupSync() API 函数

提供 xEventGroupSync() 是为了允许两个或多个任务使用事件组来相互同步
。该函数允许任务设置事件组中的一个或多个事件位,然后等待同一事件组中指定的事件位组合被设置

如下所示为 xEventGroupSync() API 函数的具体声明

/**
  * @brief  事件组同步
  * @param  uxBitsToSet:设置和测试位的事件组
  * @param  uxBitsToWaitFor:指定事件组中要测试的一个或多个事件位的按位值
  * @param  xTicksToWait:任务进入阻塞状态等待时间成立的超时节拍数
  * @retval 返回函数退出时事件组的值
  */
EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup,
							const EventBits_t uxBitsToSet,
							const EventBits_t uxBitsToWaitFor,
							TickType_t xTicksToWait);

3.5.1、函数返回值

xEventGroupSync() 函数返回函数退出时事件组的值,可能有以下两种情况

  1. xEventGroupSync() 函数的
    uxBitsToWaitFor
    参数指定了调用任务的解锁条件,
    如果该函数由于满足解锁条件而返回,则
    uxBitsToWaitFor
    指定的事件位将在 xEventGroupSync() 返回之前清回零,并且在自动清为零之前会将事件组的值作为函数返回值返回

  2. 如果 xEventGroupSync() 由于
    xTicksToWait
    参数指定的阻塞时间到期而返回,
    则返回值为阻塞时间到期时事件组的值
    ,在这种情况下,返回值将不满足调用任务的解锁条件

3.5.2、应用举例

举个简单的例子就容易理解:

假设目前有两个任务,分别为 TASK1 和 TASK2 ,如果 TASK1 被执行过程中因为延时等原因先于 TASK2 调用了 xEventGroupSync() 函数,参数
uxBitsToSet
被设置为 0x01(0000 0001),参数
uxBitsToWaitFor
被设置为 0x05(0000 0101),则 TASK1 执行到该函数时会将事件组中位 0 的值置 1 ,然后进入阻塞状态,等待位 2 和位 0 同时被置 1 ;

如果 TASK2 与 TASK1 一样,只不过落后于 TASK1 执行 xEventGroupSync() 函数,并且参数
uxBitsToSet
被设置为 0x04(0000 0100),当 TASK2 执行该函数时会将事件组中位 2 的值置 1 ,此时满足解锁条件,所以 TASK2 不会进入阻塞状态,同时 TASK1 也满足解锁条件,从阻塞状态中退出,这时候假设任务优先级一致,则 TASK1 和 TASK2 会同时从同步点开始运行后续的程序代码,从而达到同步的目的

3.5、删除事件组

/**
  * @brief  删除事件组
  * @param  xEventGroup:要删除事件组的句柄
  * @retval None
  */
void vEventGroupDelete(EventGroupHandle_t xEventGroup);

4、实验一:使用事件组进行事件管理

4.1、实验目标

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
手册第 “8.3” 小节最后介绍了关于事件组的事件管理示例 22 ,这里我们来复现一下

  1. 创建一个用于演示本实验的事件组 xEventGroup
  2. 创建一个负责将事件组 xEventGroup 位 0 和位 1 置位的任务 Task_SetBits
  3. 启动 RTC 1s 周期唤醒,在 RTC 周期唤醒回调函数中负责将事件组 xEventGroup 位 0 置位
  4. 创建一个负责等待事件组位 0 或位 1 或位 2 满足条件的任务 Task_ReadBits

4.2、CubeMX相关配置

首先读者应按照 "
FreeRTOS教程1 基础知识
"章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读 “
STM32CubeMX教程9 USART/UART 异步通信
” ,如下图所示

本实验需要配置 RTC 周期唤醒中断,具体配置步骤和参数介绍读者可阅读”
STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器
“实验,此处不再赘述,这里参数、时钟配置如下图所示

由于需要在 RTC 周期唤醒中断中使用 FreeRTOS 的 API 函数,因此 RTC 周期唤醒中断的优先级应该设置在 15~5 之间,此处设置为 7 ,具体如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务按任务 Task_SetBits 修改其参数,然后增加另外一个 Task_ReadBits 任务,具体如下图所示

然后在 Configuration 中单击 Events 选项卡,单击右下角的 Add 按钮增加一个事件组 xEventGroup ,具体如下图所示

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

按照 “
STM32CubeMX教程9 USART/UART 异步通信
” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

首先应该在 freertos.c 中添加信号量的头文件和定义需要用到的事件组位的宏定义,如下所述

/*freertos.c中添加头文件*/
#include "stdio.h"
#include "event_groups.h"

/*事件组位宏定义*/
#define mainFIRST_TASK_BIT 	( 1UL << 0UL ) /* 事件组位 0 */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* 事件组位 1 */
#define mainISR_BIT         ( 1UL << 2UL ) /* 事件组位 2 */

然后在该文件中重新实现周期唤醒回调函数,该函数用于 1s 周期将事件组 xEventGroup 的位 2 置 1 ,具体如下所示

/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
	/* 输出信息提示 */
	printf("Bit setting ISR -\t about to set bit 2.\r\n");
	/* 从中断中设置事件组位 2 为 1 */
	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
	xEventGroupSetBitsFromISR(xEventGroupHandle, mainISR_BIT, &xHigherPriorityTaskWoken);
	portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

最后仍然在该文件中实现任务 Task_SetBits 和任务 Task_ReadBits 两个任务函数体即可,具体如下所示

/*事件组置位任务*/
void AppTask_SetBits(void *argument)
{
  /* USER CODE BEGIN AppTask_SetBits */
	/* 400ms延时变量 */
	const TickType_t xDelay400ms = pdMS_TO_TICKS(400UL);
	/* Infinite loop */
	for(;;)
	{
		/* 在下次循环开始之前短延时 */
		vTaskDelay(xDelay400ms);
		/* 输出事件组位 0 被置位任务置 1 信息 */
		printf("Bit setting task -\t about to set bit 0.\r\n");
		xEventGroupSetBits(xEventGroupHandle, mainFIRST_TASK_BIT);
		/* 在置位下一位之前短延时 */
		vTaskDelay(xDelay400ms);
		/* 输出事件组位 1 被置位任务置 1 信息 */
		printf("Bit setting task -\t about to set bit 1.\r\n");
		xEventGroupSetBits(xEventGroupHandle, mainSECOND_TASK_BIT);
	}
	/* USER CODE END AppTask_SetBits */
}

/*事件组读取任务*/
void AppTask_ReadBits(void *argument)
{
	/* USER CODE BEGIN AppTask_ReadBits */
	/* 创建事件组 */
	EventBits_t xEventGroupValue;
	/* 设置要测试的位 */
	const EventBits_t xBitsToWaitFor = (mainFIRST_TASK_BIT |
										mainSECOND_TASK_BIT | 
										mainISR_BIT);
	/* Infinite loop */
	for(;;)
	{
	xEventGroupValue = xEventGroupWaitBits( 
							/* 被读的事件组 */
							xEventGroupHandle,
							/* 要测试的位 */
							xBitsToWaitFor,
							/* 阻塞条件满足退出时清除所有事件位 */
							pdTRUE,
							/* 不等待所有位. */
							pdFALSE,
							/* 永远等待,不会超时 */
							portMAX_DELAY);
		/* 位 0 被置 1 */
		if((xEventGroupValue & mainFIRST_TASK_BIT) != 0)
		{
			printf("Bit reading task -\t Event bit 0 was set\r\n");
		}
		/* 位 1 被置 1 */
		if((xEventGroupValue & mainSECOND_TASK_BIT ) != 0 )
		{
			printf("Bit reading task -\t Event bit 1 was set\r\n");
		}
		/* 位 2 被置 1 */
		if((xEventGroupValue & mainISR_BIT ) != 0 )
		{
			printf("Bit reading task -\t Event bit 2 was set\r\n");
		}
	}
	/* USER CODE END AppTask_ReadBits */
}

4.4、烧录验证

烧录程序,在 xEventGroupWaitBits() 函数
xWaitForAllBits
参数设置为 pdFALSE 的情况下串口产生的输出信息如下图所示

从图中可可以看出,因为对 xEventGroupWaitBits() 的调用中的
xWaitForAllBits
参数设置为 pdFALSE, 每次设置任何事件位时,从事件组读取的任务都会离开阻塞状态并立即执行

4.5、测试
xWaitForAllBits
参数

将任务 AppTask_ReadBits() 调用的 xEventGroupWaitBits() 函数
xWaitForAllBits
参数设置为 pdTRUE,表示需要等待所有事件组测试位满足才能离开阻塞状态,这种情况下串口产生的输出如下图所示

在上图中可以看出,由于
xWaitForAllBits
参数设置为 pdTRUE,从事件组读取的任务仅在所有三个事件位均置 1 后才可以离开阻塞状态

5、实验二:使用事件组进行任务同步

5.1、实验目标

  1. 创建一个用于演示本实验的事件组 xEventGroup
  2. 创建三个任务通过延时模拟不同时间到达任务同步点

5.2、CubeMX相关配置

首先读者应按照 "
FreeRTOS教程1 基础知识
" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读 “
STM32CubeMX教程9 USART/UART 异步通信
” ,如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,具体如下图所示

然后在 Configuration 中单击 Events 选项卡,单击右下角的 Add 按钮增加一个事件组 xEventGroup ,具体如下图所示

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

5.3、添加其他必要代码

按照 “
STM32CubeMX教程9 USART/UART 异步通信
” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

首先应该在 freertos.c 中添加信号量的头文件和定义需要用到的事件组位的宏定义,如下所述

/*头文件*/
#include "stdio.h"
#include "stdlib.h"
#include "event_groups.h"

/*事件组位宏定义*/
#define mainFIRST_TASK_BIT 	( 1UL << 0UL ) /* 事件组位 0 */
#define mainSECOND_TASK_BIT ( 1UL << 1UL ) /* 事件组位 1 */
#define mainTHIRD_TASK_BIT  ( 1UL << 2UL ) /* 事件组位 2 */

修改 MX_FREERTOS_Init() 函数,将默认生成的创建一个任务程序注释掉,然后利用一个任务回调函数通过不同的参数创建三个不同的任务,部分注释已经删除,具体如下所示

void MX_FREERTOS_Init(void) {
  /* Create the thread(s) */
  /* creation of Task_Syncing */
  //Task_SyncingHandle = osThreadNew(AppTask_Syncing, NULL, &Task_Syncing_attributes);

  /* USER CODE BEGIN RTOS_THREADS */
  /* add threads, ... */
	xTaskCreate(AppTask_Syncing, "Task 1", 1000, (void*)mainFIRST_TASK_BIT, 24, NULL);
	xTaskCreate(AppTask_Syncing, "Task 2", 1000, (void*)mainSECOND_TASK_BIT, 24, NULL);
	xTaskCreate(AppTask_Syncing, "Task 3", 1000, (void*)mainTHIRD_TASK_BIT, 24, NULL);
  /* USER CODE END RTOS_THREADS */

  /* Create the event(s) */
  /* creation of xEventGroup */
  xEventGroupHandle = osEventFlagsNew(&xEventGroup_attributes);

  /* USER CODE BEGIN RTOS_EVENTS */
  /* add events, ... */
  /* USER CODE END RTOS_EVENTS */
}

最后实现任务入口函数 AppTask_Syncing() 的函数体即可,具体如下所述

/*事件组同步任务函数*/
void AppTask_Syncing(void *argument)
{
	/* USER CODE BEGIN AppTask_Syncing */
	/* 创建两个延时用于合成随机延时时间 */
	const TickType_t xMaxDelay = pdMS_TO_TICKS(4000UL);
	const TickType_t xMinDelay = pdMS_TO_TICKS(200UL);
	/* 延时时间 */
	TickType_t xDelayTime;
	/* 任务要设置的事件组的位 */
	EventBits_t uxThisTasksSyncBit;
	/* 任务要等待的事件组的所有位 */
	const EventBits_t uxAllSyncBits = ( mainFIRST_TASK_BIT |
										mainSECOND_TASK_BIT |
										mainTHIRD_TASK_BIT );
	
	uxThisTasksSyncBit = ( EventBits_t )argument;
	/* Infinite loop */
	for(;;)
	{
		/* 合成随机延时时间,模拟三个任务不同时间到达同步点 */
		xDelayTime = (rand() % xMaxDelay) + xMinDelay;
		vTaskDelay(xDelayTime);
		printf("%s reached sync point\r\n", pcTaskGetTaskName(NULL));
		xEventGroupSync(/* 被读的事件组 */
										xEventGroupHandle,
										/* 测试的位 */
										uxThisTasksSyncBit,
										/* 需要等待的所有位 */
										uxAllSyncBits,
										/* 永远等待,不会超时 */
										portMAX_DELAY);
		/* 任务会同时退出同步点,串口输出需要时间,所以通过临界段保护串口输出 */
		taskENTER_CRITICAL();
		printf("%s exited sync point\r\n", pcTaskGetTaskName(NULL));
		taskEXIT_CRITICAL();
	}
	/* USER CODE END AppTask_Syncing */
}

5.4、烧录验证

烧录程序,打开串口助手,通过串口助手输出的信息可以发现,三个任务在不同的(伪随机)时间到达任务点,但是当其中最后一个任务到达同步点之后,三个任务会同时退出同步点,具体的串口输出信息如下图所示

6、注释详解

注释1
:图片来源于
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf