2024年1月

空间聚类
算法是数据挖掘和机器学习领域中的一种重要技术。

本篇介绍的
基于密度的空间聚类
算法的概念可以追溯到1990年代初期。
随着数据量的增长和数据维度的增加,基于密度的算法逐渐引起了研究者的关注。
其中,
DBSCAN
(Density-Based Spatial Clustering of Applications with Noise)是最具代表性的算法之一。

基于密度的空间聚类
算法在许多领域都有应用,例如图像处理、生物信息学、社交网络分析等。
在图像处理中,该算法可以用于检测图像中的密集区域,用于识别物体或形状。

1. 算法概述

DBSCAN
算法的基本思想是,对于给定的数据集,基于数据点的
密度
进行聚类。
在密度高的区域,数据点更为集中,而密度低的区域数据点较为稀疏。
基于密度的算法能够发现任意形状的簇,并且对噪声有较好的鲁棒性。

算法的核心在于:

  1. 定义邻域
    :对于数据集中的每个点,其邻域是由距离该点在一定半径(通常称为Eps)内的所有点组成的
  2. 定义密度
    :一个点的密度是其邻域内的点的数量。如果一个点的密度超过某个阈值(通常称为MinPts),则该点被视为核心点
  3. 寻找簇
    :从每个核心点出发,找到所有密度可达的点,即这些点通过一系列核心点可以与该核心点相连,这些点形成一个簇
  4. 标记噪声点
    :不属于任何簇的点被标记为噪声点

2. 创建样本数据

下面,创建三种不同的样本数据,来测试
DBSCAN
的聚类效果。

from sklearn.datasets import make_blobs, make_moons, make_circles

fig, axes = plt.subplots(nrows=1, ncols=3)
fig.set_size_inches((9, 3))

X_moon, y_moon = make_moons(noise=0.05, n_samples=1000)
axes[0].scatter(
    X_moon[:, 0],
    X_moon[:, 1],
    marker="o",
    c=y_moon,
    s=25,
    cmap=plt.cm.spring,
)

X_circle, y_circle = make_circles(noise=0.05, factor=0.5, n_samples=1000)
axes[1].scatter(
    X_circle[:, 0],
    X_circle[:, 1],
    marker="o",
    c=y_circle,
    s=25,
    cmap=plt.cm.winter,
)

X_blob, y_blob = make_blobs(n_samples=1000, centers=3)
axes[2].scatter(
    X_blob[:, 0],
    X_blob[:, 1],
    marker="o",
    c=y_blob,
    s=25,
    cmap=plt.cm.autumn,
)

plt.show()

image.png

3. 模型训练


scikit-learn

DBSCAN
模型来训练,这个模型
主要的参数
有两个:

  1. eps
    (eps):这个参数表示邻域的大小,或者说是邻域的半径。具体来说,对于数据集中的每个点,其
    eps-邻域
    包含了所有与该点的距离小于或等于
    eps
    的点。
  2. min_samples
    (minPts):在给定
    eps-邻域
    内,一个点需要有多少个邻居才能被视为核心点。

通过调节这2个参数,基于上面创建的样本数据,训练效果如下:

from sklearn.cluster import DBSCAN

# 定义
regs = [
    DBSCAN(min_samples=2, eps=0.1),
    DBSCAN(min_samples=2, eps=0.2),
    DBSCAN(min_samples=3, eps=2),
]

# 训练模型
regs[0].fit(X_moon, y_moon)
regs[1].fit(X_circle, y_circle)
regs[2].fit(X_blob, y_blob)

fig, axes = plt.subplots(nrows=1, ncols=3)
fig.set_size_inches((9, 3))

# 绘制聚类之后的结果
axes[0].scatter(
    X_moon[:, 0],
    X_moon[:, 1],
    marker="o",
    c=regs[0].labels_,
    s=25,
    cmap=plt.cm.spring,
)

axes[1].scatter(
    X_circle[:, 0],
    X_circle[:, 1],
    marker="o",
    c=regs[1].labels_,
    s=25,
    cmap=plt.cm.winter,
)

axes[2].scatter(
    X_blob[:, 0],
    X_blob[:, 1],
    marker="o",
    c=regs[2].labels_,
    s=25,
    cmap=plt.cm.autumn,
)

plt.show()

image.png

针对3种不同的样本数据,调节参数之后,聚类的效果还不错。
感兴趣的话,可以试试修改上面代码中的
DBSCAN
定义部分的参数:

# 定义
regs = [
    DBSCAN(min_samples=2, eps=0.1),
    DBSCAN(min_samples=2, eps=0.2),
    DBSCAN(min_samples=3, eps=2),
]

调节不同的
min_sample

eps
,看看不同的聚类效果。

4. 总结

总的来说,
基于密度的空间聚类
算法是一种强大的工具,能够从数据中提取有价值的信息。
但是,如同所有的算法一样,它也有其局限性,需要在合适的应用场景中使用,才能达到最佳的效果。

它的优势主要在于:

  1. 能够发现任意形状的簇
  2. 对噪声和异常值有较好的鲁棒性
  3. 不需要提前知道簇的数量

不足之处则在于:

  1. 对于高维数据,密度计算可能会变得非常复杂和计算量大
  2. 算法的性能高度依赖于密度阈值的选择
  3. 在处理密度变化较大的数据时可能效果不佳

UAC(User Account Control) 是 Windows 平台的用户权限控制。它可以让程序使用管理员权限执行某些操作。

静态 UAC 提权

静态 UAC 提权让程序一直运行在管理员权限下,只要在项目设置里把 "UAC Execution Level" 设置为 "requireAdministrator"。这样生成的 exe 文件图标会自动加上一个小盾牌的角标 Overlay。执行 exe 文件会自动弹出 UAC 对话框。

image-20240120115154732

静态 UAC 提权对程序员来说是一种偷懒的办法,只需要修改一个配置就行。但对用户来说非常麻烦,每次打开程序都需要确认 UAC 对话框。比如“小黑盒加速器”,每次打开它都会弹 UAC 对话框。更奇葩的是“小黑盒加速器” 可以设置开机自启,每次开机都会弹一个 UAC 对话框要你确认。

动态 UAC 提权

动态 UAC 提权让程序一直运行在普通用户权限下,并且只有需要管理员权限操作时才会弹出 UAC 对话框。这种做法比静态 UAC 提权更加细致。一个普通的应用程序 99% 的功能都不需要管理员权限,只在极少数情况下才需要。比如“QQ音乐”,它只是一个音乐播放软件。用户大部分的时间都仅使用音乐播放功能。而需要管理员权限的“将QQ音乐设为默认应用”功能很少会被使用。所以动态 UAC 提权很有必要。

按照微软官方文档
Developing Applications that Require Administrator Privilege
,有四种方法可以实现动态 UAC 提权:

可以根据具体需要实现的功能选择合适的方法。比如:“添加防火墙规则”这个功能需要使用 INetFwPolicy2。这是一个 COM 接口,可以直接用第 4 种方法“Administrator COM Object Model”实现。使用这种方法有个前提,就是这个 COM 接口必须在注册表里是配置为可以提权的。比如 INetFwPolicy2 接口,先找到 NetFwPolicy2 的 GUID 为
E2B3C97F-6AE1-41AC-817A-F6F92166D7DD
,再打开 regedit,输入
HKLM\Software\Classes\CLSID\{E2B3C97F-6AE1-41AC-817A-F6F92166D7DD}\Elevation

Enabled
值为
1
就可以用。对于这种方法,
The COM Elevation Moniker
介绍得比较详细,所以直接贴出完整代码:

#include <Windows.h>
#include <netfw.h>
#include <comdef.h>

#include <spdlog/spdlog.h>
#include <memory>
#include <filesystem>
#include <wil/resource.h>
#include <wil/com.h>
#include <iostream>

int add_firewall_rule();

int main()
{
    add_firewall_rule();
}

int add_firewall_rule()
{
    auto couninitialize_call = wil::CoInitializeEx();

    auto pNetFwPolicy2 = wil::CoCreateInstance<INetFwPolicy2>(__uuidof(NetFwPolicy2));

    wil::unique_cotaskmem_string clsid;
    RETURN_IF_FAILED(StringFromCLSID(__uuidof(NetFwPolicy2), &clsid));

    // https://learn.microsoft.com/en-us/windows/win32/secauthz/developing-applications-that-require-administrator-privilege
    // https://learn.microsoft.com/en-us/windows/win32/com/the-com-elevation-moniker
    BIND_OPTS3 bo{};
    bo.cbStruct = sizeof(bo);
    bo.hwnd = GetConsoleWindow();
    bo.dwClassContext = CLSCTX_LOCAL_SERVER;
    auto moniker = fmt::format(L"Elevation:Administrator!new:{}", clsid.get());
    spdlog::info(L"moniker: {}", moniker);
    auto hr = (CoGetObject(moniker.c_str(), &bo, IID_PPV_ARGS(&pNetFwPolicy2)));

    wil::com_ptr<INetFwRules> pNetFwRules;
    RETURN_IF_FAILED(pNetFwPolicy2->get_Rules(&pNetFwRules));

    long count{};
    RETURN_IF_FAILED(pNetFwRules->get_Count(&count));

    spdlog::info("rule count: {}", count);

    wil::com_ptr<IUnknown> pEnumerator;
    pNetFwRules->get__NewEnum(&pEnumerator);

    auto pVariant = pEnumerator.query<IEnumVARIANT>();

    std::vector<wchar_t> buf(1024);
    GetModuleFileNameW(nullptr, buf.data(), (DWORD)buf.size());
    std::filesystem::path exe_path(buf.data());

    // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-enumerating-firewall-rules
    while (true) {
        wil::unique_variant var;
        ULONG cFecthed = 0;
        if (pVariant->Next(1, &var, &cFecthed) != S_OK) {
            break;
        }

        wil::com_ptr<INetFwRule> pNetFwRule;
        var.pdispVal->QueryInterface(__uuidof(INetFwRule), (void**)&pNetFwRule);

        _bstr_t app_name;
        pNetFwRule->get_ApplicationName(app_name.GetAddress());
        std::error_code ec{};
        if (!app_name || !std::filesystem::equivalent(exe_path, (wchar_t*)app_name, ec)) {
            continue;
        }

        pNetFwRule->put_Name(app_name);
        pNetFwRules->Remove(app_name);
        spdlog::info(L"remove firewall rule: {}", (wchar_t*)app_name);
    }

    auto pNetFwRule = wil::CoCreateInstance<INetFwRule3>(__uuidof(NetFwRule));
    pNetFwRule->put_Enabled(VARIANT_TRUE);
    pNetFwRule->put_Action(NET_FW_ACTION_ALLOW);
    pNetFwRule->put_ApplicationName(_bstr_t(exe_path.c_str()));
    pNetFwRule->put_Profiles(NET_FW_PROFILE2_ALL);

    pNetFwRule->put_Name(_bstr_t(L"firewall-test.exe(TCP-In)"));
    pNetFwRule->put_Protocol(NET_FW_IP_PROTOCOL_TCP);
    RETURN_IF_FAILED(pNetFwRules->Add(pNetFwRule.get()));

    pNetFwRule->put_Name(_bstr_t(L"firewall-test.exe(UDP-In)"));
    pNetFwRule->put_Protocol(NET_FW_IP_PROTOCOL_UDP);
    RETURN_IF_FAILED(pNetFwRules->Add(pNetFwRule.get()));

    spdlog::info("success");
    return S_OK;
}

关于动态 UAC 提权的代码只有第 30-36 行。需要注意的点有:

  • 第 32 行的
    bo.hwnd
    设置 UAC 对话框的 owner。如果代码是控制台程序,应该设置为
    GetConsoleWindow()
    ,如果是 GUI 程序可以直接填
    nullptr
  • 第 33 行的
    bo.dwClassContext
    应填
    CLSCTX_LOCAL_SERVER
    ,而不是
    CLSCTX_INPROC_SERVER
    。否则提权失败,
    CoGetObject
    仍然返回
    S_OK
  • UAC 对话框出现在
    CoGetObject
    调用时,而不是执行需要管理员权限的操作(如
    pNetFwRules->Add
    )时。
  • 每次执行
    add_firewall_rule()
    都会弹出 UAC 对话框。

很多动态 UAC 提权的程序都会在需要管理员权限的操作按钮上显示一个小盾牌图标
sheild
,表示点击它会请求管理员权限,比如:

image-20240120141602183

可以直接调用
SHGetStockIconInfo
方法直接得到它的 HICON 句柄。

SHSTOCKICONINFO sii{};
sii.cbSize = sizeof(sii);
HRESULT hr = SHGetStockIconInfo(SIID_SHIELD, SHGSI_ICON | SHGSI_SMALLICON, &sii);
m_buttonRepairFirewall.SetIcon(sii.hIcon);
DestroyIcon(sii.hIcon);


进入新的内容,深度学习啦
万事万物的产生不是一下子就变出来的,学术上也是,一点点的进步才催生出一门新的学科或者技术,神经网络用于机器学习也不例外,
前面的机器学习的内容,线性回归,逻辑回归,多分类,决策树,以及各种集成学习,再到概率图等等,其实隐约有了深度学习的影子,
只不过是只有一层输入一层输出的浅层神经网络,下面正式进入神经网络!

1. 神经网络算法是有监督的学习算法,


一个神经元 有很多树突dentrities(接收信号输入) 细胞体body经过分析综合汇总(激活函数) 通过轴突axon传递很长的距离,作为下一个神经元的输入 输出信号。

生物神经网络与人工神经元

  1. 激活函数:将神经元的净输入信号转换成单一的输出信号,以便进一步在网络中传播。(隐藏层如果没有进行非线性变换,那无非就是信号的放大与缩小,只是做“透传”,不进行汇总 分析 综合,神经元的意义何在) 隐藏层激活函数必须是非线性的
  2. 网络拓扑:描述了模型中神经元的数量以及层数和它们连接的方式。
  3. 训练算法:指定如何设置连接权重,以便抑制或增加神经元在输入信号中的比重。

常见的激活函数: (本质上是为了进行非线性的变换 如果都是线性变换 那一层就能搞定 要其他的层干嘛, 这也就是与之前的线性回归的本质区别)

  1. sigmoid
    输出正例的概率 隐射到 0-1之间

  2. tanh
    hyperbolic tangent
    隐射到 -1--1 之间


  3. relu
    只有信号强到一定的程度才会输出

2. 分类

例如三分类 用ovr的方式训练三个二分类 逻辑回归

使用softmax变换

3. 训练

一次迭代:
前向传播计算一次输出loss
根据loss 以及梯度 更新w
这样来回一次 完成一次迭代

4. 代码

from sklearn.neural_network import MLPRegressor
from sklearn.neural_network import MLPClassifier

X = [[0, 0],
     [1, 2]]
y = [0,
     1]

clf = MLPClassifier(solver="sgd", alpha=0.00001, activation="relu", hidden_layer_sizes=(5, 2), max_iter=2000,
                    tol=0.00001)
clf.fit(X, y)

predict_value = clf.predict([[2, 2], [-1, -2]])
print(predict_value)

predict_prob = clf.predict_proba([[2, 2], [-1, -2]])
print(predict_prob)

print([coef.shape for coef in clf.coefs_])
print([coef for coef in clf.coefs_])

前言

ELK 是指 Elasticsearch、Logstash 和 Kibana 这三个开源软件的组合。

Elasticsearch 是一个分布式的搜索和分析引擎,用于日志的存储,搜索,分析,查询。

Logstash 是一个数据收集、转换和传输工具,用于收集过滤和转换数据,然后将其发送到 Elasticsearch 或其他目标存储中。

Kibana 是一个数据可视化平台,通过与 Elasticsearch 的集成,提供了强大的数据分析和仪表盘功能。

Filebeat 是 Elastic Stack(ELK)中的一个组件,用于轻量级的日志文件收集和转发。它能够实时监控指定的日志文件,并将其发送到 Elasticsearch 或 Logstash 进行处理和分析。

ELK的架构有多种,本篇分享使用的架构如图所示: Beats(Filebeat) -> -> Elasticsearch -> Kibana,目前生产环境一天几千万的日志,内存占用大概 10G 左右

特点

  • 开源免费
  • 灵活性和可扩展性,高可用性,易扩展,支持集群
  • 高效的搜索和分析功能
  • 实时性
  • 易于使用

使用情况

  • 目前微服务项目使用,ELK单机部署,处理千万级日志无压力
  • 使用 Kibana 做了面板,根据面板监控系统情况
  • 使用 Docker 部署,方便快捷
  • 上手用还算简单,但是弄懂这一套,就不是那么容易了
  • 提炼出 docker compose 配置,分分钟部署好

实践

准备

  • 一台 linxu 服务器,内存 8G+
  • 安装 docker,docker compose
  • 新机器搭建后的运行情况,限制了Elasticsearch的jvm参数 4g

安装

本篇 ELK 的版本为 v7.8.1,本篇使用的容器网络为 devopsnetwork ,需创建
docker network create devopsnetwork

Elasticsearch 使用 docker compose 安装

  • 官方使用 Docker 安装文档

  • compose.yml


    • 指定了jvm参数:4g
    • 暴露端口 9200:该端口是Elasticsearch REST API的默认端口。
    • 暴露端口 9300:该端口是Elasticsearch节点之间的内部通信端口,默认用于节点之间的集群通信
    • 挂载数据目录
      ./data
      及配置文件
      ./config/elasticsearch.yml
    • 需要对两个目录进行授权,这里直接用了777,也可以根据官网使用对应es的用户id 1000
version: '3.1'
services:
  elk_elasticsearch:
    image: elasticsearch:7.8.1
    container_name: elk_elasticsearch
    restart: always
    environment:
      - discovery.type=single-node
      - ES_JAVA_OPTS=-Xms4096m -Xmx4096m
    ports:
      - 9200:9200 
      - 9300:9300 
    volumes:
      # 授权 chmod 777 ./config/ && chmod 777 ./data/
      - ./data:/usr/share/elasticsearch/data
      - ./config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
    networks:
      - devopsnetwork

networks:
  devopsnetwork:
    external: true
  • config/elasticsearch.yml
network.host: 0.0.0.0
xpack:
  ml.enabled: false
  monitoring.enabled: false
  security.enabled: false
  watcher.enabled: false

将其拷贝到服务器执行启动即可

#cd /app/elasticsearch
mkdir ./config
mkdir ./data
chmod 777 ./config/ && chmod 777 ./data/
docker compose up -d

验证访问

Logstash 使用 docker compose 安装

  • 官方使用 Docker 安装 文档

  • compose.yml


    • 暴露端口5044:用于接收来自其他主机的日志数据、
    • 挂载的
      ./pipeline

      ./config
      目录可以运行容器复制出来
    • 需要将./config/logstash.yml 和 ./pipeline/logstash.conf 改成es地址,
      参考
version: '3.1'
services:
  elk_logstash:
    image: logstash:7.17.16
    container_name: elk_logstash
    restart: always
    ports:
      - 5044:5044 
    volumes:
     # 授权 chmod 777 ./logs/ && chmod 777 ./data/ && chmod 777 ./pipeline/ && chmod 777 ./config/ 
      - /etc/timezone:/etc/timezone
      - /etc/localtime:/etc/localtime:ro
      - ./logs:/usr/share/logstash/logs
      - ./data:/usr/share/logstash/data
      - ./pipeline:/usr/share/logstash/pipeline
      - ./config:/usr/share/logstash/config
    networks:
      - devopsnetwork

networks:
  devopsnetwork:
    external: true
  • pipeline/logstash.conf


input {
    beats {
        port => 5044
        codec => json {
            charset => "UTF-8"
        }
    }

}

filter {  
 
}

output {
    elasticsearch { 
      hosts => ["http://192.168.123.102:9200"]
      index => "%{[app]}-%{+YYYY.MM.dd}" 
  }    
  stdout { 
    codec => rubydebug 
  }
}

将其拷贝到服务器执行启动即可

mkdir ./data
mkdir ./logs
chmod 777 ./logs/ && chmod 777 ./data/ && chmod 777 ./pipeline/ && chmod 777 ./config/ 
docker compose up -d

Kibana 使用 docker compose 安装

  • 官方使用 Docker 安装文档

  • compose.yml


    • 指定es节点是单节点,多节点使用zen
    • 挂载配置文件
      ./config/kibana.yml
    • 暴露端口 5601:面板访问端口
version: '3.1'
services:
  elk_kibana:
    image: kibana:7.8.1
    container_name: elk_kibana
    restart: always
    environment:
      - discovery.type=single-node
    ports:
      - 5601:5601 
    volumes:
      - ./config/kibana.yml:/usr/share/kibana/config/kibana.yml
    networks:
      - devopsnetwork

networks:
  devopsnetwork:
    external: true
  • config/kibana.yml
server.host: "0.0.0.0"
elasticsearch.hosts: ["http://192.168.123.102:9200/"]
i18n.locale: "zh-CN"
xpack:
  apm.ui.enabled: false
  graph.enabled: false
  ml.enabled: false
  monitoring.enabled: false
  reporting.enabled: false
  security.enabled: false
  grokdebugger.enabled: false
  searchprofiler.enabled: false

将其拷贝到服务器执行启动即可

docker compose up -d

Filebeat 使用 docker compose 安装

  • compose.yml


    • 挂载filebeat的配置文件,数据目录及日志目录,需要设置权限
    • 挂载容器外的日志到容器内的日志采集目录
version: '3.1'
services:
  elk_filebeat:
    image: elastic/filebeat:7.8.1
    container_name: elk_filebeat
    restart: always
    volumes:
      # 授权 chmod 777 ./config/ && chmod 777 ./data/ && chmod 777 ./logs/ && chmod 777 /app/logs
      - ./config/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - ./data:/usr/share/filebeat/data 
      - ./logs:/usr/share/filebeat/logs
      - /app/logs:/app/logs
    networks:
      - devopsnetwork

networks:
  devopsnetwork:
    external: true
  • config/filebeat.yml


    • hosts 节点需要配置 logstash 地址
    • paths 指定日志目录

output.logstash:
  #logstash hosts
  hosts: ["192.168.123.102:5044"]
fields_under_root: true    
filebeat.inputs: 
 - type: log
   enabled: true
   paths:
       - /app/logs/*/*.log    
   close_older: 24h
   ignore_older: 24h   
   json.keys_under_root: true
   json.overwrite_keys: true
   encoding: utf-8 
filebeat.config.modules: 
  path: ${path.config}/modules.d/*.yml 
  reload.enabled: false
setup.template.settings:
  index.number_of_shards: 3  
processors:
  - add_host_metadata: ~
  - add_cloud_metadata: ~  
  - drop_fields:    
      fields: ["log","@version","ecs","agent","beat","host","beat.hostname","beat.version","beat.name","prospector.type","input.type","host.id","host.name","host.os.build","host.os.family","host.os.name","host.os.platform","host.os.platform","log.file.path","tags","offset","host.architecture","host.os.version"]

安全使用

配置 nginx 域名转发

server {

    listen 80;
    listen       443 ssl;
    server_name kibana.devops.test.com;  # 自行修改成你的域名

    ssl_certificate      /certs/kibana.devops.test.com/server.crt;
    ssl_certificate_key  /certs/kibana.devops.test.com/server.key;
    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    location / {
            proxy_pass http://192.168.123.102:5601;
            proxy_http_version 1.1;
            proxy_buffering off;
            proxy_request_buffering off;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $remote_addr;
    }
}

配置完成,即可使用域名访问

配置 nginx 基本认证

在Nginx配置文件中添加以下内容

location / {
    auth_basic "Restricted Content";
    auth_basic_user_file /certs/kibana.devops.test.com/passwd;
    ...
}

添加对应的passwd文件,使用 htpasswd 生成,如账号密码是 root devops666 的配置文件

root:WvesKBTr22.wY

可以使用我
metools
工具的
密码生成器
生成

配置完成,重载配置后刷新页面就提示输入账号密码了

配置 nginx IP白名单

location / {
    allow 192.168.123.201;   # 允许的IP地址
    deny all;              # 拒绝所有其他IP地址
}

后语

本篇只针对 ELK 的安装进行了介绍及整理对应的 Docker Compose 配置文件,后续即可快速安装配置 ELK 环境,如何与项目结合使用后续再分享

1、准备材料

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

STM32CubeMX软件(
Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

XCOM V2.6串口助手

逻辑分析仪
nanoDLA

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的
I2C1与MPU6050芯片通信,读取MPU6050的三轴加速度和陀螺仪数据并通过串口打印出来

3、实验流程

3.0、前提知识

本实验重点是理解I2C通信协议
,而STM32CubeMX的配置则相对简单,这里不会过于详细全面的介绍I2C通信协议,但是会对所有需要知道的知识做介绍

在我们的开发板上有一颗三轴加速度计和陀螺仪传感器MPU6050,
单片机通过I2C1的PB8和PB9两个引脚与MPU6050进行通信
,MPU6050还有一个中断引脚,这里为3D_INT引脚,但是本实验仅仅轮询读取加速度计和陀螺仪的数据,并没有用到该引脚中断功能,我们使用的开发板上的MPU6050芯片硬件原理图如下图所示

I2C通信仅需要时钟线SCLK和数据线SDA两根线就可以让主机与挂载在I2C上的从机进行通信和数据交换,一个I2C理论上最多可挂载127个设备(从机地址用7位二进制表示)

为了让主机准确的与众多从机中的一个进行通信,每个从机都会有一个地址,I2C通信时会在通信数据中先发送从机地址,然后对应地址的从机才会响应

而且I2C通信使用的时钟线SCLK和数据线SDA两根线必须要做上拉设置,因为I2C的两个引脚被配置为开漏输出,因此无法输出高电平
,I2C总线连线图如下图所示
(注释1)

MPU6050的从机地址由芯片上的AD0引脚确定,当AD0引脚接地时,从机地址为0X68;

当AD0引脚接VCC时,从机地址为0X69;

根据上面MPU6050芯片硬件原理图可知此时MPU6050的从机地址为0X68

根据MPU6050 datasheet 9.3 I2C Communications Protocol 小节可知
(注释2)
,主机要通过I2C写入/读取MPU6050某一个寄存器一字节的数据,其通信步骤序列应该如下图所示

我们以读取内部寄存器单个字节数据为例做详细介绍
,首先确定通信的目的为主机Master从从机Slave内部某个寄存器internal register中读取一个字节数据,以下为详细通信步骤

  1. 主机时钟线SCLK和数据线SDA两根线产生起始信号(当 SCL 线是高电平时 SDA 线从高电平向低电平切换)
  2. 接下来时钟线SCLK的8个时钟节拍中,由数据线SDA发送一字节8位的数据,其中高7位为从机的地址,最后一位为0(1表示读操作,0表示写操作)
  3. 接下来1个时钟节拍,从机应该产生应答信号ACK
  4. 接下来8个时钟节拍,主机发送一字节8位的数据,该数据为要读取的从机内部寄存器地址
  5. 接下来1个时钟节拍,从机应该产生应答信号ACK
  6. 主机重新产生一个起始信号
  7. 接下来8个时钟节拍,重新发送一字节8位的数据,其中高7位为从机的地址,最后一位为1,表示这次要读
  8. 接下来1个时钟节拍,从机应该产生应答信号ACK
  9. 接下来8个时钟节拍,主机读取一字节8位数据
  10. 接下来1个时钟节拍,主机应该产生应答信号NACK
  11. 主机时钟线SCLK和数据线SDA两根线产生停止信号(当 SCL 线是高电平时 SDA 线由低电平向高电平切换)

接下来我们用逻辑分析仪捕获下主机使用I2C1读取MPU6050寄存器WHO_AM_I(0X75)时,时钟线SCLK和数据线SDA的逻辑电平变化
,如下图所示,从图上可知I2C读取读取MPU6050内部寄存器的时序与上面我们所描述的一致

用逻辑分析仪捕获主机使用I2C1向MPU6050寄存器PWR_MGMT_1(0X6B)写入一字节0X00数据时,时钟线SCLK和数据线SDA的逻辑电平变化
,如下图所示

为什么上图中从机地址从0X68变为0XD0了?

HAL库中的I2C写入函数HAL_I2C_Mem_Write()和读取函数HAL_I2C_Mem_Read()对传入从机地址DevAddress参数做了要求,该地址必须将数据手册中提到的地址左移才可以调用该接口

0X68(0110 1000)向左移动直到遇到1即为0XD0(1101 0000),在I2C通信中使用上述两个API,从机地址传入0XD0表示对从机地址为0X68的从机进行写操作,传入0XD1表示对从机地址为0X68的从机进行读操作

如下图为HAL库HAL_I2C_Mem_Read()函数说明

上面提到,
当启用I2C之后,其I2C_SCL和I2C_SDA两个引脚被配置为了复用功能开漏输出,而开漏输出无法输出高电平,因此I2C_SCL和I2C_SDA两个引脚需要外部上拉
,一般的开发板都会考虑到这一点,在设计原理图的时候将使用的I2C两根线给外部上拉到3.3V,如果你使用的是自己设计的板子,请务必记住I2C需要上拉

细心的同学可能又发现了,你上面给出的MPU6050硬件原理图I2C两根线并没有外部上拉呀?

虽然MPU6050硬件原理图I2C两根线没有上拉,但是在开发板的其他I2C通信的芯片上进行了外部上拉,如下图所示

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

实验需要通过串口来输出与MPU6050进行I2C通信读取的陀螺仪和加速度计数据,因此外设需要初始化USART1和I2C1

单击Pinout & Configuration页面左边Connectivity/USART1选项,然后按照“
STM32CubeMX教程9 USART/UART 异步通信
”实验中将USART1配置为异步通信模式,无需开启中断,如下图所示

在Pinout & Configuration页面右边芯片引脚预览Pinout view中找到与开发板上MPU6050芯片连接的I2C的两个通信引脚PB8和PB9,左边单击将其分别配置为I2C1_SCL和I2C1_SDA

然后单击Connectivity/I2C1选项,在其模式中选择I2C,在下方Master Features中将
I2C Speed Mode
根据用户需求选择快速模式(400kkHz)或者标准模式(100kHz),这两种模式仅仅影响通信速率,对于本实验两个模式均可随意选择

其他参数保持默认即可,具体配置如下图所示

3.1.3、外设中断配置

本实验无需启用中断,如果需要启用I2C1的中断,请单击System Core/NVIC,然后根据需求勾选I2C1的事件或错误中断,并选择合适的中断优先级即可,具体配置如下图所示

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

还是一模一样的流程,与该系列教程所有的外设初始化一致

在生成的工程代码主函数中新增了MX_I2C1_Init()函数,在该函数中实现了对I2C1的模式及参数配置

在MX_I2C1_Init()函数中调用了HAL_I2C_Init()函数使用配置的参数对I2C1进行了初始化

在HAL_I2C_Init()函数中又调用了HAL_I2C_MspInit()函数对I2C1引脚复用设置,I2C1时钟使能,如果开启了中断该函数中还会有中断相关设置及使能

如下图所示为I2C1初始化调用流程

3.2.2、外设中断调用流程

本实验无需中断,因此未启动任何I2C1的中断

3.2.3、添加其他必要代码

需要添加MPU6050的驱动文件,文件中至少应该包括

  1. MPU6050初始化函数
  2. MPU6050获取三轴加速度计原始数据函数
  3. MPU6050获取三轴陀螺仪原始数据函数

注意本实验只使用而不会介绍MPU6050具体驱动文件的原理,具体源代码如下所示
(注释3)

mpu6050.c文件

#include "mpu6050.h"
 
/**
* @brief 		MPU6050初始化函数
* @alter		无
* @param		无
* @retval 		成功返回0,失败返回1
*/
uint8_t MPU6050_Init(I2C_HandleTypeDef *I2Cx)
{
	uint8_t check;
	uint8_t Data;
 
	// check device ID WHO_AM_I
	HAL_I2C_Mem_Read(I2Cx, MPU6050_ADDR, MPU_DEVICE_ID_REG, 1, &check, 1, I2C_TimeOut);
	// 0x68 will be returned by the sensor if everything goes well
	if (check == 104) 
	{
			// power management register 0X6B we should write all 0's to wake the sensor up
			Data = 0;
			HAL_I2C_Mem_Write(I2Cx, MPU6050_ADDR, MPU_PWR_MGMT1_REG, 1, &Data, 1, I2C_TimeOut);
 
			// Set DATA RATE of 1KHz by writing SMPLRT_DIV register
			Data = 0x07;
			HAL_I2C_Mem_Write(I2Cx, MPU6050_ADDR, MPU_SAMPLE_RATE_REG, 1, &Data, 1, I2C_TimeOut);
 
			// Set accelerometer configuration in ACCEL_CONFIG Register
			// XA_ST=0,YA_ST=0,ZA_ST=0, FS_SEL=0 ->   2g
			Data = 0x00;
			HAL_I2C_Mem_Write(I2Cx, MPU6050_ADDR, MPU_ACCEL_CFG_REG, 1, &Data, 1, I2C_TimeOut);
 
			// Set Gyroscopic configuration in GYRO_CONFIG Register
			// XG_ST=0,YG_ST=0,ZG_ST=0, FS_SEL=0 ->   250  /s
			Data = 0x00;
			HAL_I2C_Mem_Write(I2Cx, MPU6050_ADDR, MPU_GYRO_CFG_REG, 1, &Data, 1, I2C_TimeOut);
			return 0;
	}
	return 1;
}
 
 
/**
* @brief 		MPU6050温度值获取函数
* @alter		无
* @param		无
* @retval 		温度值
*/
float MPU_Get_Temperature(void)
{
	uint8_t buf[2]; 
	short raw;
	float temp;
	HAL_I2C_Mem_Read(&MPU6050_I2C, MPU6050_ADDR, MPU_TEMP_OUTH_REG, 1, buf, 2, I2C_TimeOut);
	raw=((int16_t)buf[0]<<8)|buf[1];  
	temp=36.53+((double)raw)/340;  
	return temp;;
}
 
/**
* @brief 		MPU6050陀螺仪值获取函数(三轴原始值)
* @alter		无
* @param		gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号)
* @retval 		正常:0,错误:其他
*/
uint8_t MPU_Get_RAW_Gyroscope(int16_t *gx,int16_t *gy,int16_t *gz)
{
	uint8_t buf[6],res;  
	res = HAL_I2C_Mem_Read(&MPU6050_I2C, MPU6050_ADDR, MPU_GYRO_XOUTH_REG, 1, buf, 6, I2C_TimeOut);
	if(res==0)
	{
		*gx=((int16_t)buf[0]<<8)|buf[1];  
		*gy=((int16_t)buf[2]<<8)|buf[3];  
		*gz=((int16_t)buf[4]<<8)|buf[5];
	} 	
    return res;
}
 
/**
* @brief 		MPU6050加速度值获取函数(三轴原始值)
* @alter		无
* @param		ax,ay,az:加速度计x,y,z轴的原始读数(带符号)
* @retval 		正常:0,错误:其他
*/
uint8_t MPU_Get_RAW_Accelerometer(int16_t *ax,int16_t *ay,int16_t *az)
{
	uint8_t buf[6],res;  
	res = HAL_I2C_Mem_Read(&MPU6050_I2C, MPU6050_ADDR, MPU_ACCEL_XOUTH_REG, 1, buf, 6, I2C_TimeOut);
	if(res==0)
	{
		*ax=((int16_t)buf[0]<<8)|buf[1];  
		*ay=((int16_t)buf[2]<<8)|buf[3];  
		*az=((int16_t)buf[4]<<8)|buf[5];
	} 	
    return res;
}

mpu6050.h文件

#ifndef __MPU6050_H
#define __MPU6050_H
#include "main.h"
#include "i2c.h"
#endif
 
 
#define MPU6050_I2C      		hi2c1
#define MPU6050_ADDR     		0xD0   
#define I2C_TimeOut  			100
 
/*MPU6050内部寄存器地址*/
#define MPU_SAMPLE_RATE_REG		0X19	//采样频率分频器
#define MPU_GYRO_CFG_REG		0X1B	//陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG		0X1C	//加速度计配置寄存器
#define MPU_ACCEL_XOUTH_REG		0X3B	//加速度值,X轴高8位寄存器
#define MPU_TEMP_OUTH_REG		0X41	//温度值高八位寄存器
#define MPU_GYRO_XOUTH_REG		0X43	//陀螺仪值,X轴高8位寄存器
#define MPU_PWR_MGMT1_REG		0X6B	//电源管理寄存器1
#define MPU_DEVICE_ID_REG		0X75	//器件ID寄存器
 
uint8_t MPU6050_Init(I2C_HandleTypeDef *I2Cx);
float MPU_Get_Temperature(void);
uint8_t MPU_Get_RAW_Gyroscope(int16_t *gx,int16_t *gy,int16_t *gz);
uint8_t MPU_Get_RAW_Accelerometer(int16_t *ax,int16_t *ay,int16_t *az);
 
/*
MPU6050内部所有寄存器地址
#define MPU_SELF_TESTX_REG		0X0D	//自检寄存器X
#define MPU_SELF_TESTY_REG		0X0E	//自检寄存器Y
#define MPU_SELF_TESTZ_REG		0X0F	//自检寄存器Z
#define MPU_SELF_TESTA_REG		0X10	//自检寄存器A
#define MPU_SAMPLE_RATE_REG		0X19	//采样频率分频器
#define MPU_CFG_REG				0X1A	//配置寄存器
#define MPU_GYRO_CFG_REG		0X1B	//陀螺仪配置寄存器
#define MPU_ACCEL_CFG_REG		0X1C	//加速度计配置寄存器
#define MPU_MOTION_DET_REG		0X1F	//运动检测阀值设置寄存器
#define MPU_FIFO_EN_REG			0X23	//FIFO使能寄存器
#define MPU_I2CMST_CTRL_REG		0X24	//IIC主机控制寄存器
#define MPU_I2CSLV0_ADDR_REG	0X25	//IIC从机0器件地址寄存器
#define MPU_I2CSLV0_REG			0X26	//IIC从机0数据地址寄存器
#define MPU_I2CSLV0_CTRL_REG	0X27	//IIC从机0控制寄存器
#define MPU_I2CSLV1_ADDR_REG	0X28	//IIC从机1器件地址寄存器
#define MPU_I2CSLV1_REG			0X29	//IIC从机1数据地址寄存器
#define MPU_I2CSLV1_CTRL_REG	0X2A	//IIC从机1控制寄存器
#define MPU_I2CSLV2_ADDR_REG	0X2B	//IIC从机2器件地址寄存器
#define MPU_I2CSLV2_REG			0X2C	//IIC从机2数据地址寄存器
#define MPU_I2CSLV2_CTRL_REG	0X2D	//IIC从机2控制寄存器
#define MPU_I2CSLV3_ADDR_REG	0X2E	//IIC从机3器件地址寄存器
#define MPU_I2CSLV3_REG			0X2F	//IIC从机3数据地址寄存器
#define MPU_I2CSLV3_CTRL_REG	0X30	//IIC从机3控制寄存器
#define MPU_I2CSLV4_ADDR_REG	0X31	//IIC从机4器件地址寄存器
#define MPU_I2CSLV4_REG			0X32	//IIC从机4数据地址寄存器
#define MPU_I2CSLV4_DO_REG		0X33	//IIC从机4写数据寄存器
#define MPU_I2CSLV4_CTRL_REG	0X34	//IIC从机4控制寄存器
#define MPU_I2CSLV4_DI_REG		0X35	//IIC从机4读数据寄存器
#define MPU_I2CMST_STA_REG		0X36	//IIC主机状态寄存器
#define MPU_INTBP_CFG_REG		0X37	//中断/旁路设置寄存器
#define MPU_INT_EN_REG			0X38	//中断使能寄存器
#define MPU_INT_STA_REG			0X3A	//中断状态寄存器
#define MPU_ACCEL_XOUTH_REG		0X3B	//加速度值,X轴高8位寄存器
#define MPU_ACCEL_XOUTL_REG		0X3C	//加速度值,X轴低8位寄存器
#define MPU_ACCEL_YOUTH_REG		0X3D	//加速度值,Y轴高8位寄存器
#define MPU_ACCEL_YOUTL_REG		0X3E	//加速度值,Y轴低8位寄存器
#define MPU_ACCEL_ZOUTH_REG		0X3F	//加速度值,Z轴高8位寄存器
#define MPU_ACCEL_ZOUTL_REG		0X40	//加速度值,Z轴低8位寄存器
#define MPU_TEMP_OUTH_REG		0X41	//温度值高八位寄存器
#define MPU_TEMP_OUTL_REG		0X42	//温度值低8位寄存器
#define MPU_GYRO_XOUTH_REG		0X43	//陀螺仪值,X轴高8位寄存器
#define MPU_GYRO_XOUTL_REG		0X44	//陀螺仪值,X轴低8位寄存器
#define MPU_GYRO_YOUTH_REG		0X45	//陀螺仪值,Y轴高8位寄存器
#define MPU_GYRO_YOUTL_REG		0X46	//陀螺仪值,Y轴低8位寄存器
#define MPU_GYRO_ZOUTH_REG		0X47	//陀螺仪值,Z轴高8位寄存器
#define MPU_GYRO_ZOUTL_REG		0X48	//陀螺仪值,Z轴低8位寄存器
#define MPU_I2CSLV0_DO_REG		0X63	//IIC从机0数据寄存器
#define MPU_I2CSLV1_DO_REG		0X64	//IIC从机1数据寄存器
#define MPU_I2CSLV2_DO_REG		0X65	//IIC从机2数据寄存器
#define MPU_I2CSLV3_DO_REG		0X66	//IIC从机3数据寄存器
#define MPU_I2CMST_DELAY_REG	0X67	//IIC主机延时管理寄存器
#define MPU_SIGPATH_RST_REG		0X68	//信号通道复位寄存器
#define MPU_MDETECT_CTRL_REG	0X69	//运动检测控制寄存器
#define MPU_USER_CTRL_REG		0X6A	//用户控制寄存器
#define MPU_PWR_MGMT1_REG		0X6B	//电源管理寄存器1
#define MPU_PWR_MGMT2_REG		0X6C	//电源管理寄存器2 
#define MPU_FIFO_CNTH_REG		0X72	//FIFO计数寄存器高八位
#define MPU_FIFO_CNTL_REG		0X73	//FIFO计数寄存器低八位
#define MPU_FIFO_RW_REG			0X74	//FIFO读写寄存器
#define MPU_DEVICE_ID_REG		0X75	//器件ID寄存器
*/

在keil中添加.c/.h文件步骤如下图所示

然后将源代码复制到新建的.c/.h文件中,最后将.c文件添加到工程即可,如下图所示

在主函数中初始化MPU6050,主循环中获取原始的三轴加速度数据和三轴陀螺仪数据,并通过串口打印信息

源代码如下

/*main.c中定义的变量*/
int16_t ax,ay,az,gx,gy,gz;
 
/*初始化代码*/
while(MPU6050_Init(&hi2c1) != HAL_OK){HAL_Delay(1);}
 
/*主循环中代码*/
MPU_Get_RAW_Gyroscope(&ax,&ay,&az);
MPU_Get_RAW_Accelerometer(&gx,&gy,&gz);
printf("ax:%d,ay:%d,az:%d,",ax,ay,az);
printf("gx:%d,gy:%d,gz:%d\r\n",gx,gy,gz);
HAL_Delay(100);

4、常用函数

/*I2C读函数*/
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)
 
/*I2C写函数*/
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout)

5、烧录验证

烧录程序,上电后开启串口,可以看到源源不断的输出当前MPU6050三轴加速度数据和三轴陀螺仪数据,移动开发板会发现数据有规律改变,如下图所示为串口输出的数据

6、注释详解

注释1
:图片来源
I2C通信协议介绍 - 知乎

注释2
:MPU6050 Datasheet
https://invensense.tdk.com/wp-content/uploads/2015/02/MPU-6000-Datasheet1.pdf

注释3
:MPU6050驱动库参考
https://github.com/leech001/MPU6050

更多内容请浏览
STM32CubeMX+STM32F4系列教程文章汇总贴