Springboot 结合 MQTT、Redis ,对接硬件以及做消息分发,最佳实践

news2025/1/15 23:28:30

Springboot 结合 mqttredis对接硬件以及做消息分发,最佳实践

一,认识

需要了解EMQX 基本知识原理,不了解的可以查看我之间的博客,以及网上的资料,这里不在过多撰述。

二,开发思路

这里以对接雷达水位计为例:

说一下思路, 这里场景各种设备连接 EMQX ,然后通过 EMQX 上报数据,和接收服务器下发的指令。

我们需要部署一个 EMQX 服务器, 设备配置我们的服务器ip和端口连接到 EMQX 。

那么我们开发EMQX 的思路应该是什么样子的。

  1. mqtt 客户端订阅相关主题;

  2. 数据库保存数据设备产品项目定义主题,存到redis;

  3. 通过主题做出相关数据分析;

三,准备工作

3.1 引入Springboot-mqtt依赖

Springboot 依赖, MQTT依赖

	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
    </parent>

	<dependency>
        <!-- mqtt -->
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-integration</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.integration</groupId>
			<artifactId>spring-integration-stream</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.integration</groupId>
			<artifactId>spring-integration-mqtt</artifactId>
		</dependency>
    </dependency>

其他相关依赖 不在撰写, 数据库依赖以及 工具类依赖 ,自己按需引用

四,编写代码

4.1 编写MQTT配置类

不在过多解释代码,每行都有注释

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

/**
 * 功能描述: 配置类
 *
 * @Author keLe
 * @Date 2022/10/31
 */
@Data
@Component
@Configuration
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "mqtt")
public class MqttProperties {

    /**服务器地址url*/
    private String host;

    /**客户端唯一ID*/
    private String clientId;

    /**用户名*/
    private String userName;

    /**密码*/
    private String passWord;

    /**超时时间*/
    private Integer timeOut;

    /**保活时间*/
    private Integer keepaLive;

    /**是否清除会话*/
    private boolean clearSession;
}

appliction.yml

mqtt:
    host: tcp://xx.xx.xx.xx:1883 #MQTT-服务器连接地址,如果有多个,用逗号隔开,如:tcp://127.0.0.1:61613,tcp://192.168.2.133:61613
    clientId: ${random.int}  #MQTT-连接服务器默认客户端ID
    userName: admin   #MQTT-用户名
    passWord: admin #MQTT-密码
    default-topic: test #MQTT-默认的消息推送主题,实际可在调用接口时指定
    timeOut: 1000 #连接超时
    keepaLive: 30   #设置会话心跳时间
    clearSession: true  #清除会话(设置为false,断开连接,重连后使用原来的会话 保留订阅的主题,能接收离线期间的消息)

4.2 编写MQTT客户端,处理创建,连接,订阅,发布等功能

import com.joygis.mqtt.MqttProperties;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Arrays;

/**
 * 功能描述: mqtt客户端
 *
 * @Author keLe
 * @Date 2022/10/31
 */
@Slf4j
@Component
public class MqttCustomerClient {

    @Resource
    private MqttCallback mqttCallback;

    @Resource
    private MqttProperties mqttProperties;

    /**
     * 连接配置
     */
    private MqttConnectOptions options;

    /**
     * MQTT异步客户端
     */
    public static MqttAsyncClient client;

    /**
     * 功能描述: 客户端连接
     *
     * @Author keLe
     * @Date 2022/10/31
     */
    public void connect() {
        if (mqttProperties == null) {
            log.error("【mqtt异常】:连接失败,配置文件缺失。");
            return;
        }
        //设置配置
        if (options == null) {
            setOptions();
        }
        //创建客户端
        if (client == null) {
            createClient();
        }
        while (!client.isConnected()) {
            try {
                IMqttToken token = client.connect(options);
                token.waitForCompletion();
            } catch (Exception e) {
                log.error("【mqtt异常】:mqtt连接失败,message={}", e.getMessage());
            }
        }
    }

    /**
     * 功能描述: 创建客户端
     *
     * @Author keLe
     * @Date 2022/10/31
     */
    private void createClient() {
        if (client == null) {
            try {
              /*host为主机名,clientId是连接MQTT的客户端ID,MemoryPersistence设置clientId的保存方式
                默认是以内存方式保存*/
                client = new MqttAsyncClient(mqttProperties.getHost(), mqttProperties.getClientId(), new MemoryPersistence());
                //设置回调函数
                client.setCallback(mqttCallback);
                log.info("【mqtt】:mqtt客户端启动成功");
            } catch (MqttException e) {
                log.error("【mqtt异常】:mqtt客户端连接失败,error={}", e.getMessage());
                e.printStackTrace();
            }
        }
    }

    /**
     * 功能描述: 设置连接属性
     *
     * @Author keLe
     * @Date 2022/10/31
     */
    private void setOptions() {
        if (options != null) {
            options = null;
        }
        if (mqttProperties == null) {
            log.error("【mqtt异常】连接失败,失败原因:配置文件缺失。");
            return;
        }
        options = new MqttConnectOptions();
        options.setCleanSession(true);
        options.setUserName(mqttProperties.getUserName());
        options.setPassword(mqttProperties.getPassWord().toCharArray());
        options.setConnectionTimeout(mqttProperties.getTimeOut());
        options.setKeepAliveInterval(mqttProperties.getKeepaLive());
        //设置自动重新连接
        options.setAutomaticReconnect(true);
        options.setCleanSession(mqttProperties.isClearSession());
    }

    /**
     * 功能描述: 断开与mqtt的连接
     *
     * @Author keLe
     * @Date 2022/10/31
     */
    public synchronized void disconnect() {
        //判断客户端是否null 是否连接
        if (client != null && client.isConnected()) {
            try {
                IMqttToken token = client.disconnect();
                token.waitForCompletion();
            } catch (MqttException e) {
                log.error("【mqtt异常】: 断开mqtt连接发生错误,message={}", e.getMessage());
            }
        }
        client = null;
    }

    /**
     * 功能描述: 重新连接MQTT
     *
     * @Author keLe
     * @Date 2022/10/31
     */
    public synchronized void refresh() {
        disconnect();
        setOptions();
        createClient();
        connect();
    }

    /**
     * 功能描述: 发布
     * @param qos         连接方式
     * @param retained    是否保留
     * @param topic       主题
     * @param pushMessage 消息体
     * @Author keLe
     * @Date 2022/10/31
     */
    public void publish(int qos, boolean retained, String topic, String pushMessage) {
        log.info("【mqtt】:发布主题" + topic);
        MqttMessage message = new MqttMessage();
        message.setQos(qos);
        message.setRetained(retained);
        message.setPayload(pushMessage.getBytes());

        try {
            IMqttDeliveryToken token = client.publish(topic,message);
            token.waitForCompletion();
        } catch (MqttPersistenceException e) {
            e.printStackTrace();
        } catch (MqttException e) {
            log.error("【mqtt异常】: 发布主题时发生错误 topic={},message={}",topic,e.getMessage());
        }
    }

    /**
     * 功能描述: 订阅某个主题
     * @param topic 主题
     * @param qos   消息质量
     *              Qos1:消息发送一次,不确保
     *              Qos2:至少分发一次,服务器确保接收消息进行确认
     *              Qos3:只分发一次,确保消息送达和只传递一次
     * @Author keLe
     * @Date 2022/10/31
     */
    public void subscribe(String topic, int qos){
        log.info("【mqtt】:订阅了主题 topic={}",topic);
        try {
            IMqttToken token = client.subscribe(topic, qos);
            token.waitForCompletion();
        }catch (MqttException e){
            log.error("【mqtt异常】:订阅主题 topic={} 失败 message={}",topic,e.getMessage());
        }
    }

    /**
     * 功能描述: 订阅某些主题
     * @param topic 主题
     * @param qos   消息质量
     *              Qos1:消息发送一次,不确保
     *              Qos2:至少分发一次,服务器确保接收消息进行确认
     *              Qos3:只分发一次,确保消息送达和只传递一次
     * @Author keLe
     * @Date 2022/10/31
     */
    public void subscribe(String[] topic,int[] qos){
        log.info("【mqtt】:订阅了主题 topic={}", Arrays.toString(topic));
        try {
            IMqttToken token = client.subscribe(topic,qos);
            token.waitForCompletion();
        }catch (MqttException e){
            log.error("【mqtt异常】:订阅主题 topic={} 失败 message={}",topic,e.getMessage());
        }
    }

    /**是否处于连接状态*/
    public boolean isConnected(){
        return client != null && client.isConnected();
    }
}

4.3 编写MQTT 回调监听器

/**
 * 功能描述: 消费监听
 *
 * @Author keLe
 * @Date 2022/10/31
 */
@Slf4j
@Component
public class MqttCallback implements MqttCallbackExtended {

    @Resource
    private MqttService mqttService;

    @Override
    public void connectionLost(Throwable throwable) {
        log.error("【mqtt异常】:断开连接....");
    }

    @Override
    public void messageArrived(String topic, MqttMessage message) throws Exception {
        mqttService.subscribeCallback(topic,message);
    }

    /**
     * 功能描述: 发布消息后,到达MQTT服务器,服务器回调消息接收
     * @param token  Mqtt传递令牌
     * @Author keLe
     * @Date 2022/10/31
     */
    @Override
    public void deliveryComplete(IMqttDeliveryToken token) {
        log.info("【mqtt】交付完成:{}",token.isComplete());
    }

    /**
     * 功能描述: 监听mqtt连接消息
     * @param reconnect 是否重连
     * @param serverUrl 服务地址
     * @Author keLe
     * @Date 2022/10/31
     */
    @Override
    public void connectComplete(boolean reconnect, String serverUrl) {
        log.info("mqtt已经连接!!");
        //连接后,可以在此做初始化事件,或订阅
        try {
            mqttService.subscribe(MqttCustomerClient.client);
        } catch (MqttException e) {
            log.error("======>>>>>订阅主题失败 error={}",e.getMessage());
        }
    }
}

4.4 编写MQTT 业务接口,处理订阅发布

import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.joygis.common.constant.Constants;
import com.joygis.common.core.redis.RedisCache;
import com.joygis.mqtt.client.MqttCustomerClient;
import com.joygis.mqtt.domian.SubscribeConfig;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Slf4j
@Service
public class MqttService {

    @Resource
    private MqttCustomerClient mqttCustomerClient;

    @Autowired
    private RedisCache redisCache;

    /**
     * 功能描述: 订阅主题
     * @param client MQTT异步客户端
     * @Author keLe
     * @Date 2022/10/31
     */
    public void subscribe(MqttAsyncClient client) throws MqttException {
        //获取主题
        List<String> cacheList = redisCache.getCacheList(Constants.SUB_CONFIG_KEY + "topic");
        if(CollectionUtil.isEmpty(cacheList)){
            log.error("【mqtt异常】:redis缓存中,无法获取主题相关信息!");
            return;
        }
        String[] topicFilters = cacheList.toArray(new String[cacheList.size()]);
        int[] qos = new int[cacheList.size()];
        for(int i = 0 ; i<cacheList.size() ; i++){
            qos[i] = 1;
        }
        // 订阅
        client.subscribe(topicFilters, qos);
        log.info("mqtt订阅了设备信息和物模型主题");
    }

    /**
     * 功能描述: 消息回调方法
     * @param topic  主题
     * @param mqttMessage 消息体
     * @Author keLe
     * @Date 2022/10/31
     */
    @Async
    public void subscribeCallback(String topic, MqttMessage mqttMessage) throws InterruptedException {
        /**测试线程池使用*/
        log.info("====>>>>线程名--{}",Thread.currentThread().getName());
        /**模拟耗时操作*/
        // Thread.sleep(1000);
        // subscribe后得到的消息会执行到这里面
        String message = new String(mqttMessage.getPayload());
        log.info("接收消息主题 : " + topic);
        log.info("接收消息Qos : " + mqttMessage.getQos());
        log.info("接收消息内容 : " + message);
        String key = Constants.SUB_CONFIG_KEY+topic;
        SubscribeConfig subscribeConfig = redisCache.getCacheObject(key);
        //TODO 这里使用通过数据取到相应的bean 动态去调用接口解析数据
    }

    /**
     * 功能描述: 发布设备状态
     * @Author keLe
     * @Date 2022/10/31
     * @param  productId 产品id
     * @param  deviceNum 设备编号
     * @param  deviceStatus 设备状态
     * @param  isShadow 影子模式
     * @param  rssi 编号
     */
    public void publishStatus(Long productId, String deviceNum, int deviceStatus, int isShadow,int rssi) {
        String message = "{\"status\":" + deviceStatus + ",\"isShadow\":" + isShadow + ",\"rssi\":" + rssi + "}";
        mqttCustomerClient.publish(1, false, "/" + productId + "/" + deviceNum + "", message);
    }

    /**
     * 功能描述: 发布设备状态
     * @Author keLe
     * @Date 2022/10/31
     * @param  productId 产品id
     * @param  deviceNum 设备编号
     */
    public void publishInfo(Long productId, String deviceNum) {
        mqttCustomerClient.publish(1, false, "/" + productId + "/" + deviceNum + "", "");
    }

    /**
     * 功能描述: 发布设备状态
     * @Author keLe
     * @Date 2022/10/31
     * @param  productId 产品id
     * @param  deviceNum 设备编号
     */
    public void publishFunction(Long productId, String deviceNum, List<String> thingsList) {
        if (thingsList == null) {
            mqttCustomerClient.publish(1, true, "/" + productId + "/" + deviceNum + "", "");
        } else {
            mqttCustomerClient.publish(1, true, "/" + productId + "/" + deviceNum + "", JSON.toJSONString(thingsList));
        }

    }
}

4.5 Redis初始化,加载数据库 topic

@Service
public class SubscribeConfigServiceImpl implements ISubscribeConfigService {
    @Autowired
    private RedisCache redisCache;

    @Resource
    private SubscribeConfigMapper subscribeConfigMapper;

    @PostConstruct
    public void init() {
        loadingConfigCache();
    }
    
    @Override
    public void loadingConfigCache() {
        List<SubscribeConfig> configsList = subscribeConfigMapper.selectSubscribeConfigList(new SubscribeConfig());
        if(CollectionUtil.isNotEmpty(configsList)){
            for (SubscribeConfig config : configsList) {
                redisCache.setCacheObject(getCacheKey(config.getTopic()), config);
            }
            List<String> topicList = configsList.stream().map(SubscribeConfig::getTopic).collect(Collectors.toList());
            redisCache.deleteObject(getCacheKey("topic"));
            redisCache.setCacheList(getCacheKey("topic"), topicList);
        }
    }

    /**
     * 设置cache key
     *
     * @param configKey 参数键
     * @return 缓存键key
     */
    private String getCacheKey(String configKey) {
        return Constants.SUB_CONFIG_KEY + configKey;
    }
    
}

4.6 Springboot 启动,MQTT也自启动,任务定时器池也启动

package com.joygis.iot.config;
import com.joygis.mqtt.client.MqttCustomerClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 功能描述: spring 容器创建完成之后,开始创建mqtt客户端
 *
 * @Author keLe
 * @Date 2022/10/31
 */
@Order(value = 1 )
@Component
public class MqttStart implements ApplicationRunner {

    @Resource
    private MqttCustomerClient mqttCustomerClient;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        mqttCustomerClient.connect();
    }
}

五 ,测试

启动服务

在这里插入图片描述

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

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

相关文章

【最优化理论】03-无约束优化

无约束优化无约束优化问题无约束优化问题的应用无约束优化问题的最优性条件无约束-凸函数-最优性条件&#xff08;充要&#xff09;无约束-一般函数-最优性条件必要条件一阶必要条件&#xff1a;梯度为0二阶必要条件&#xff1a;hessian矩阵半正定充分条件二阶充分条件&#xf…

元宇宙-漫游世界后与Cocos一起看湖南卫视直播

使用参考资源 CocosCreator v3.6.2 cocomat 腾讯开源公共组件框架 Cocos Creator 3D特制 Video MeshRender 播放器&#xff08;Cocos商店购买&#xff09; TcPlayer 腾讯开源 Web 播放器 视频流 hls 库 正文 场景漫游引发的思考 元宇宙&#xff0c;虚拟世界。OK&#xff0c;…

【UI编程】将Java awt/swing应用移植到JavaFX纪实

1. 背景 最近想做一个实用的小工具&#xff0c;能屏幕截图&#xff0c;录屏和录制课件&#xff0c;简单的图像处理&#xff0c;和制作gif表情包。翻出了很久以前用Java awt/swing写的一个屏幕截图小程序&#xff0c;能运行&#xff0c;但是屏幕截图到剪贴板后&#xff0c;发现…

深入理解JavaScript-this关键字

先说结论&#xff1a;谁调用它&#xff0c;this 就指向谁 前言 在讲 Function、作用域 时&#xff0c;我们都讲到了 this&#xff0c;因为 JavaScript 中的作用域是词法作用域&#xff0c;在哪里定义&#xff0c;就在哪里形成作用域。而与词法作用域相对应的还有一个作用域叫…

MP157-0-遇见的问题及解决办法

MP157-0-遇见的问题及解决办法1.Win11运行VMware15虚拟机崩溃死机&#xff0c;蓝屏。1.Win11运行VMware15虚拟机崩溃死机&#xff0c;蓝屏。 时间&#xff1a;2022.11.15 解决办法&#xff1a; Hyper-V方案。 打开控制面板-程序-启用或关闭Windows功能&#xff0c;可能你的电…

【JavaScript高级】03-JavaScript内存管理和闭包

JavaScript内存管理和闭包JavaScript内存管理垃圾回收机制算法常见的GC算法-标记清除闭包闭包的概念理解闭包的形成过程闭包的内存泄露JavaScript内存管理 JavaScript会在定义数据时为我们分配内存&#xff1a; JS对于原始数据类型内存的分配会在执行时&#xff0c;直接在栈空…

Sentinel使用教程

文章目录一、Sentinel简介1.sentinel介绍2.sentinel应用场景3.sentinel与hystrix4.sentinel组件介绍二、Sentinel使用说明1.控制台Dashboard2.Sentinel 流量控制和熔断降级3.常见报错解决一、Sentinel简介 1.sentinel介绍 Sentinel 是由阿里巴巴中间件团队开发的开源项目&…

Java三大特性篇之——继承篇(超详解的好吧!)

&#x1f60d;&#x1f60d;&#x1f60d;欢迎欢迎欢迎欢迎&#xff0c;我的朋友&#xff0c;答应我&#xff0c;看完好吗&#xff1f;&#x1f974; 文章目录前言&#xff1a;何为继承&#xff1f;不谈钱的继承实现&#xff01;嘘&#xff1a;偷偷访问父类的私密成员&#xff…

OkHttp相关知识(二)

okhttp中一次网络请求的大致过程&#xff1a; Call对象对请求的封装 dispatcher对请求的分发 getResponseWithInterceptors()方法 一、OkHttp同步方法总结&#xff1a; 创建OkHttpClient和构建了携带请求信息的Request对象将Request封装成Call对象调用Call的execute()发送…

【11.16】Codeforces 刷题

DP\text{DP}DP &#xff1a;&#xff08;今天做的这两道都没啥 DP 相关来着 D. Match & Catch 题意&#xff1a; 给定两个字符串 1≤∣s1∣,∣s2∣≤50001\leq |s_1|,|s_2|\leq 50001≤∣s1​∣,∣s2​∣≤5000 &#xff0c;求最短的满足各只出现一次的连续公共字串。 思…

实验27:红外遥控三级控速风扇实验

今天介绍一个稍微复杂点的实验,复杂在设计和代码 ——OK,受了抖音西湖大学教授刺激,任何人都可以做研究 ——实验:红外遥控三级风速小电扇 ——每按一下CH-,风速从1-2-3-1-2-3-1循环 ——按下CH+,风扇停止 ——没有背景音乐目的是听风扇声音大小判断风速 OK实验介绍完了…

五个可以永远相信的神仙网站推荐

早八的我们是不是偶尔会处在焦虑中呢&#xff1f;一方面年轻人工作压力大&#xff0c;另一方面我们偶尔会感慨我们的碌碌无为&#xff0c;不知道怎样提升自己。今天为大家推荐五个焦虑时可以随手打开看&#xff0c;不知不觉悄悄提升自己的软件。 1.全历史 全历史是一个把历史以…

《元宇宙2086》亮相金鸡奖中国首部元宇宙概念院线电影启动

2022年中国金鸡百花电影节暨第35届中国电影金鸡奖于11月10日至12日在福建厦门举办&#xff0c;中国动漫集团控股子公司北京中文发文化发展有限公司与《元宇宙2086》作者高泽龙在金鸡奖创投论坛正式签约&#xff0c;宣布将共同启动筹拍中国首部元宇宙概念的院线电影。 当日下午&…

如何在Docker中安装MySQL数据库

1、Docker环境 视频教程&#xff1a;https://www.bilibili.com/video/BV1xv4y1S7kA 2、搜索镜像 https://hub.docker.com/网站搜索MySQL&#xff0c;确定其安装版本&#xff0c;这里安装8.0.31版&#xff1b; 3、拉取镜像 [rootlocalhost ~]# docker pull mysql:8.0.31 8.…

市级专精特新的申报条件

一、基本条件&#xff1a;&#xff08;各市政策不同具体情况也不同&#xff0c;下面为济南市企业的申报条件&#xff09; 1、连续经营3年以上&#xff0c;上年度企业营业收入在800万元以上&#xff1b; 2、近两年营业收入复合增长率不低于8%&#xff08;2021年参照国 家级调…

Nginx 反向代理

title: Nginx 反向代理 date: 2022-11-16 10:24 tags: [Nginx,反向代理,正向代理,代理] 文章目录〇、问题一、前言二、正向代理&反向代理2.1 正向代理2.2 反向代理三、Nginx配置反向代理参考更新〇、问题 什么是正向代理&#xff1f;什么是反向代理&#xff1f;Nginx如何配…

Mysql之视图、索引【第五篇】

大纲&#xff1a; 一、视图 1、什么是视图&#xff1f; 1) MySQL 视图(View)是一种虚拟的表&#xff0c;是从数据库中一个或多个表中导出来的表。视图由列和行构成&#xff0c;行和列的数据来自于定义视图的查询中所使用的表&#xff0c;并且还是在使用视图时动态生成的。 …

【蓝桥杯物联网赛项学习日志】Day3 关于IIC

经过昨天的学习&#xff0c;已经了解和初步学会配置CubeMax进行初始化配置。今天就开始下一章节的学习&#xff0c;关于IIC。 关键词&#xff1a;I2C OLED SSD1306 理论基础 串行通信接口通讯方式分&#xff0c;可以分为两种&#xff0c;分别是同步和异步。按照数据的传输方…

组成目标货币的最少张数

1、题目 arr 是货币数组&#xff0c;其中的值都是正数。再给定一个正数 aim。 每个值都认为是一张货币&#xff0c;返回组成 aim 的最少张数。 注意&#xff1a;因为是求张数&#xff0c;所以每张货币认为是相同或不同就不重要了。 2、思路 假设 arr [3&#xff0c;1&…

GD32F450的时钟笔记

GD32F450 标称 200MHz&#xff0c;但是在手册中又说 它是 240MHz。本文以 手册中的 240MHz 进行举例&#xff0c;我保险起见&#xff0c;产品中使用还是在 200MHz 下使用。 时钟树 手册上的时钟树图如下 GD32F450的 外部时钟源 有2个 LXTAL 外部低速时钟源 32.768 kHzHXTAL …