2024年1月

wmproxy

wmproxy
已用
Rust
实现
http/https
代理,
socks5
代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现
websocket
代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

项目设计目标

针对有一些应用场景需要将TCP转成websocket的,就比如旧的客户端或者旧的服务端比较不合适进行改造,但是又需要借助阿里的全站加速DCDN等这类服务或者其它可能需要特定浏览器协议的情况下,需要进行协议的转化而服务。

Tcp转Websocket

流程图

以下展示Tcp转Websocket的流程图,就纯粹的Tcp客户端在不经过任何源码修改的情况下成功连接websocket服务端

flowchart TD
A[tcp客户端] -->|连接服务| B[服务节点]
B -->|服务转化| C[websocket客户端]
C -->|连接服务| D[websocket服务端]

比较适合原生客户端,又不想引入第三方库,又能在需要的时候直接使用websocket来做配合。

源码实现

实现源码在
stream_to_ws

/// 将tcp的流量转化成websocket的流量
pub struct StreamToWs<T: AsyncRead + AsyncWrite + Unpin> {
    url: Url,
    io: T,
}
  • 需要传入的参数为原生的tcp,此处tcp是具备异步读异步写功能的虚拟tcp
  • 传入连接websocket的url地址,可以连接到websocket的服务端地址

定义回调类:

struct Operate {
    /// 将tcp来的数据流转发到websocket
    stream_sender: Sender<Vec<u8>>,
    /// 从websocket那接收信息
    receiver: Option<Receiver<OwnedMessage>>,
}
  • stream_sender
    将数据进行发送到websocket中
  • receiver
    从websocket中获取信息流

核心转发逻辑:

pub async fn copy_bidirectional(self) -> ProtResult<()> {
    let (ws_sender, ws_receiver) = channel::<OwnedMessage>(10);
    let (stream_sender, stream_receiver) = channel::<Vec<u8>>(10);
    let url = self.url;
    tokio::spawn(async move {
        if let Ok(mut client) = Client::builder().url(url).unwrap().connect().await {
            client.set_callback_ws(Box::new(Operate {
                stream_sender,
                receiver: Some(ws_receiver),
            }));
            let _e = client.wait_ws_operate().await;
        }
    });
    Self::bind(self.io, ws_sender, stream_receiver).await?;
    Ok(())
}

创建两对发送接收对分别为
OwnedMessage

Vec<u8>
来进行双向绑定,并在协程中发起对websocket的连接请求。更多的逻辑请查看源码。

测试demo

示例文件
ws_stw
,当下监听
8082
的流量并将其转发到
8081

websocket
服务上,测试借助
websocat
做测试服务端

  • cargo run --example ws_stw
    启动转发监听8082
  • websocat -s 8081
    监听8081
  • telnet 127.0.0.1 8082
    手动建立8082的端口

成功测试转发

Websocket转Tcp

流程图

以下展示Websocket转Tcp的流程图,通常由浏览器环境中发起(因为浏览器的标准全双工就是websocket)。然后服务器这边由TCP的方案

flowchart TD
A[websocket客户端] -->|连接服务| B[服务节点]
B -->|服务转化| C[tcp客户端]
C -->|连接服务| D[tcp服务端]

比较适合原生服务端,又不想引入第三方库,又能兼容TCP及websocket协议,适合在这个做个中间层。

源码实现

实现源码在
ws_to_stream

/// 将websocket的流量转化成的tcp流量
pub struct WsToStream<T: AsyncRead + AsyncWrite + Unpin + Send + 'static, A: ToSocketAddrs> {
    addr: A,
    io: T,
}
  • 需要传入的参数为原生的tcp,此处tcp是具备异步读异步写功能的虚拟tcp,其中
    'static
    表示io为一个类,而不是引用
  • 传入连接tcp的SocketAddr地址,可以连接到Tcp的服务端地址

定义回调类:

struct Operate {
    /// 将tcp来的数据流转发到websocket
    stream_sender: Sender<Vec<u8>>,
    /// 从websocket那接收信息
    receiver: Option<Receiver<OwnedMessage>>,
}
  • stream_sender
    将数据进行发送到websocket中
  • receiver
    从websocket中获取信息流

核心转发逻辑:

pub async fn copy_bidirectional(self) -> ProtResult<()> {
    let (ws_sender, ws_receiver) = channel(10);
    let (stream_sender, stream_receiver) = channel::<Vec<u8>>(10);
    let stream = TcpStream::connect(self.addr).await?;
    let io = self.io;
    tokio::spawn(async move {
        let mut server = Server::new(io, None);
        server.set_callback_ws(Box::new(Operate {
            stream_sender,
            receiver: Some(ws_receiver),
        }));
        let e = server.incoming().await;
        println!("close server ==== addr = {:?} e = {:?}", 0, e);
    });
    Self::bind(stream, ws_sender, stream_receiver).await?;
    Ok(())
}

与tcp转websocket类似,但是此时是将io流量转成
Server
的处理函数。

测试demo

示例文件
ws_wts
,当下监听
8082
的流量并将其转发到
8081

websocket
服务上,测试借助
websocat
做测试服务端
新建测试TCP的监听,原样转发的测试代码:

#[tokio::main]
async fn main() -> std::io::Result<()> {
    use tokio::{net::TcpListener, io::{AsyncReadExt, AsyncWriteExt}};
    let tcp_listener = TcpListener::bind(format!("127.0.0.1:{}", 8082)).await?;
    loop {
        let mut stream = tcp_listener.accept().await?;
        tokio::spawn(async move {
            let mut buf = vec![0;20480];
            loop {
                if let Ok(size) = stream.0.read(&mut buf).await {
                    println!("receiver = {:?} size = {:?}", &buf[..size], size);
                    let _ = stream.0.write_all(b"from tcp:").await;
                    let _ = stream.0.write_all(&buf[..size]).await;
                } else {
                    break;
                }
            }
        });
    }
}
  • cargo run --example tcp
    监听8082的端口,收到数据原样转发
  • cargo run --example ws_wts
    启动转发监听8081
  • websocat ws://127.0.0.1:8081
    用websocket的方式连接到8081

成功测试转发

组合方案

当我们现存的网络方案为
Tcp到Tcp
或者为
Websocket到Websocket
而我们在中间的传输过程中如想利用DCDN做源地址保护,而他只支持Websocket,此时我们就可以利用数据的转化,将我们的数据包通过DCDN做转发:

flowchart TD
A[TCP客户端] -->|连接服务| B[服务节点]
B -->|转化成websocket通过加速| C[DCDN全站加速]
C -->|连接服务| E[服务节点]
E -->|转化成Tcp并串连到服务端| F[TCP服务端]

这样子我们就可以利用基础网络中的CDN或者DCDN等服务,又不用对旧的数据进行修改或者无法修改的程序就比如远程服务通过CDN进行加速等。

小结

协议的自由转化可以帮助我们创建更合适的网络环境,可以让运维更自由的构建系统。利用转化可以用好全站加速
DCDN
这类的功能,可以更好的保护源站,防止被DDOS攻击。

点击
[关注]

[在看]

[点赞]
是对作者最大的支持

image

协程的发展流程

再来回顾一下协程的发展流程:
python2.5 为生成器引用.send()、.throw()、.close()方法
python3.3 为引入yield from,可以接收返回值,可以使用yield from定义协程
Python3.4 加入了asyncio模块
Python3.5 增加async、await关键字,在语法层面的提供支持
python3.7 使用 async def + await 的方式定义协程
python3.10 移除 以 yield from 的方式定义协程

旧协程
是指以
yield

yield from
等生成器语法为基础的协程实现
新协程
是指以
asyncio

async

await
等关键字为基础的协程实现
两种协程的实现方式在协程发展史上有一段交集,并且旧协程基于生成器的协程语法让生成器和协程两个概念混淆,所以对学习者会造成一定的困扰。本篇主要说明两种协程的实现方式的差异。

旧协程回顾

旧协程以yield关键字为核心,通过yield关键提供的代码执行暂停、恢复的能力,实现函数交替的执行,cpu的转让等能力。

import time


def consume():
    r = ''
    while True:
        n = yield r
        print(f'[consumer] 开始消费 {n}...')
        time.sleep(1)
        r = f'{n} 消费完成'


def produce(c):
    next(c)
    n = 0
    while n < 5:
        n = n + 1
        print(f'[producer] 生产了 {n}...')
        r = c.send(n)
        print(f'[producer] consumer return: {r}')
    c.close()


if __name__=='__main__':
    c = consume()
    produce(c)

执行结果:

[producer] 生产了 1...
[consumer] 开始消费 1...
[producer] consumer return: 1 消费完成
[producer] 生产了 2...
[consumer] 开始消费 2...
[producer] consumer return: 2 消费完成
[producer] 生产了 3...
[consumer] 开始消费 3...
[producer] consumer return: 3 消费完成
[producer] 生产了 4...
[consumer] 开始消费 4...
[producer] consumer return: 4 消费完成
[producer] 生产了 5...
[consumer] 开始消费 5...
[producer] consumer return: 5 消费完成

结果分析:
当消费者
consume
执行到
n = yield r
时,流程暂停,将cpu交还给调用方
produce


asyncio初识篇
中提到过,协程最重要的两个因素是
事件循环
+
任务
。用yield实现的协程中,
consume

produce
中的 while循环共同作用下实现了一个事件循环的功能,
yield

send
实现了任务的暂停和继续执行。

总结来说协程需要的两个能力
事件循环

任务暂停和继续
,在旧协程中的实现分别是:

  1. 事件循环通过手动编写while循环代码实现
  2. 代码暂停继续执行通过yield生成器的能力实现

新协程回顾

新协程是
asyncio

async

await
等关键字实现的。新协程是基于事件循环机制实现的,核心能力包括事件循环,任务,回调机制等。三者提供的能力分别是

  1. asyncio 提供了事件循环
  2. async 提供了协程标识
  3. await 提供了流程挂起能力
import asyncio


async def coro1():
    print("start coro1")
    await asyncio.sleep(2)
    print("end coro1")


async def coro2():
    print("start coro2")
    await asyncio.sleep(1)
    print("end coro2")


# 创建事件循环
loop = asyncio.get_event_loop()


# 创建任务
task1 = loop.create_task(coro1())
task2 = loop.create_task(coro2())

# 运行协程
loop.run_until_complete(asyncio.gather(task1, task2))

# 关闭事件循环
loop.close()

结果

start coro1
start coro2
end coro2
end coro1

结果分析:

coro1
执行到
await asyncio.sleep(2)
时,流程挂起,将cpu交还给事件循环,等待事件循环的下一次调度,而事件循环调度到
coro2
继续执行。

协程的两个重要能力
事件循环

任务暂停和继续
,分别的实现者:

  1. 事件循环通过asyncio提供的loop实现
  2. 程序挂起通过 await 关键字实现

新旧协程实现的对比

asyncio

yield
是用于实现异步编程的两种不同的机制。

yield
是一种用于生成器(Generator)函数的关键字,用于创建可暂停和恢复执行的函数。当一个函数中包含
yield
语句时,它会返回一个生成器对象,可以通过调用生成器的
next()
方法或使用
for
循环来逐步迭代生成器函数中的值。

通过使用
yield
,我们可以将一个函数分割成多个代码块,并在每个代码块之间进行切换执行。这使得我们可以在函数执行过程中临时挂起函数的执行,然后再次恢复执行。

asyncio
是 Python 提供的标准库,用于编写异步代码。它基于事件循环(Event Loop)模式,允许我们在单线程中处理多个并发任务,并通过协程(Coroutine)来管理异步操作。

asyncio
使用了
async

await
这两个关键字来定义协程函数。在协程函数中可以使用
await
关键字来暂停当前协程的执行,等待某个异步操作的完成,然后恢复执行。

总结来说:
旧协程:通过
yield关键字的暂停和恢复执行的能力
实现协程
新协程:通过
事件循环机制,await关键字挂起流程能力
实现协程

await 和 yield 的关系

await
关键字和
yield
关键字都可以用于控制流的暂停和恢复,都属于python的关键字,但是它们在协程的实现上有所不同。

相同点:

  1. 控制流暂停和恢复
    :无论是
    await
    还是
    yield
    ,它们都可以使代码在某个点暂停执行,并在稍后的时间点继续执行。
  2. 协程支持

    await

    yield
    都与协程(Coroutine)密切相关。它们都能够用于定义和管理协程,使得异步代码的编写更加简单和易读。

区别:

  1. 语法差异

    await
    是 Python 3.5 引入的关键字,用于异步函数中暂停执行等待异步操作完成。而
    yield
    是早期协程的关键字,主要用于生成器(Generator)函数,用于创建迭代器和实现惰性计算,早期通过生成器的能力来实现协程。
  2. 语义
  • await
    表示当前协程需要等待一个异步操作的完成,并挂起执行,让其他任务有机会执行。

  • yield
    是将执行的控制权交给调用方,同时保存函数的状态,以便在下次迭代时从上一次暂停的位置恢复执行。

      await将程序挂起,让事件循环调度新的任务。yield将程序挂起,等待调用方的下一步指令。
    
  1. 上下文

    await
    必须在异步上下文中使用,例如在异步函数中或者在
    async with
    块中。而
    yield
    可以在普通函数中使用,即使没有使用协程的上下文。
  2. 返回值

    yield
    返回生成器对象,通过调用
    next()
    方法或使用
    for
    循环逐步迭代生成器中的值。而
    await
    返回一个可等待对象(Awaitable),它可以是
    Future

    Task

    Coroutine
    等。

总结:
await 不是通过 yield 来实现的程序暂停和执行,两者有相似的能力,但完全没有调用关系,都是属于python关键字。

  • await
    适用于异步编程场景,用于等待异步操作的完成,同时支持更灵活的协程管理。
  • yield
    则主要用于生成器函数,用于实现迭代器和惰性计算。

它们在应用场景和语法上存在一些差异,但都为我们提供了控制流的暂停和恢复的能力。

以上就是新旧协程的实现方法,对比了两种协程的实现方法,比较了yield关键字既作为生成器又实现协程有点混淆的用法,比较了都可以暂停恢复的关键字yield和await。这些内容是协程原理的核心知识,理解有难度。

转载至我的博客
https://www.infrastack.cn
,公众号:架构成长指南

大家好,我是蜗牛哥,跨系统转账网上教程很多,但是都是讲的比较浅,这个功能看似简单,但是细节很多,要做好没那么容易,因为涉及到分布式事务、交易安全性等方面,做不好就出现资损,本文讲一下如何设计一个高可靠跨系统转账,以及要关注的重点

示例说明

假设有用户 A在 银行A 转账给B银行的用户B 100 元

这里只是为了便于理解,所以才把系统命名为银行A/B,具体可能与银行的流程有点细微区别

会遇到哪些问题?

转账失败,不能直接回滚

要根据返回的异常来判断,如果接收到的异常是一个业务异常,并且异常码是双方约定好的,那么可以进行回滚,如果返回的不是一个明确的异常,,那么不能擅自回滚,因为可能是网络超时异常,而网络超时,又分为响应超时和请求超时,如果是响应超时,对方系统可能已经入账了,所以要进行重试操作确认

面试题:超时异常,有哪几种情况,怎么处理?

系统重试要保持幂等

假如网络超时进行重试,入金方的接口需要支持幂等,否则会出现可能重复入金,而幂等条件是根据出金方的
业务流水号+渠道号
进行查询判断

  1. 如果有记录,并根据状态,来决定响应结果
  2. 如果没有记录则进行入金,在返回对应的响应结果

如果失败,那么出金方需要进行解冻回滚操作,如果成功,那么需要进行解冻出金操作。

同时入金方还要设置此组合字段为
唯一索引
,这样可以避免重复插入的问题,比如:未查询到数据,则进行插入,正好前面一笔请求事务未提交,如果不设置唯一索引就会导致出现重复插入的问题。

交易安全性

由于这种资产操作非常敏感,稍有失误影响非常大,所以交易安全性是非常重要的,比如:有攻击者知道B银行的入金接口,那么直接调用,他的账户就会加钱。。。,所以要进行以下安全措施

要进行签名调用

在转账前用私钥对账户进行签名,然后给B银行颁发一个公钥,进行入金的签名验签操作,来保证此请求是正常请求。

要对交易的时效性进行校验

为了进一步保证交易的安全性,双方要约定好一个交易的时效性,比如5 分钟,在进行接口调用时携带请求时间,如果这个请求时间是5分钟之前的进行拒绝,等待重新发起。

要进行系统对账

除了签名,双方系统还要进行对账,而对账又分为总账对账和明细对账

总账对账

比如查看银行A出金总额是否等于B银行的入金总额,对账频率有小时、天不等,计算公式如下

转账给银行B总额==接收到银行A的入金总额 ?
明细对账

除了总账要进行核对,明细账也要进行核对,因为总账不平后,要确保那一个账户出现问题,为了实现明细对账双方系统要保留对方系统流水号,这样才能对应起来,对账频率一般是天

要考虑并发扣款

在进行账户操作时,要考虑并发问题,进行加锁处理,否则会出现资损,例如

  1. 订单a和订单 b同一时间都查询到了,账户余额为1000
  2. 订单a扣款200,订单b扣款 100
  3. 假如订单 a先执行,那么账户余额为800,订单 b 修改为账户余额为900,最终为 900,反正则为 800,都不对

具体可以查看
并发扣款,如何保证结果一致性

涉及到表可能有哪些?

出金方

转账流水表

此表可以进行对账,也可以进行定时任务重新发起重试

- 主键
- 流水号
- 用户 ID
- 方向:转出转入
- 金额
- 目标方流水号
- 时间
- 状态 (等待调用、调用成功、调用失败)
账户表

此表的作用不用多说,主要说下冻结资金密度,防止真正扣款时账户上没钱,导致交易失败,所以一般都是先进行冻结,如果失败则进行解冻

- 用户 id
- 总金额
- 冻结资金
- 账户状态(正常 冻结)
- 时间
冻结记录表

记录冻结流水,防止出问题没法追溯

- 主键
- 流水号
- 用户 Id
- 金额
- 类型:冻结、解冻
- 关联的业务流水号
- 时间

入金方

以下表为最核心的表,但不是最全的表,比如应该还有账账务流水表、账务订单、热点账户表等

渠道转账流水表

此表可以进行对账,也可以进行定时任务重新发起重试

- 主键
- 流水号
- 渠道
- 业务方流水号 //后期幂等要根据此字段进行判断,所以此字段+渠道号为唯一索引
- 用户 ID
- 方向:转出转入
- 金额
- 时间
- 状态  (1成功 2失败)
账务表
- 用户 id
- 总金额
- 冻结资金
- 账户状态(正常 冻结)
- 时间

最终流程应该是什么样的?

流程有4个,分别为

  • 正常的转账流程
  • 补偿转账流程
  • 总账对账流程
  • 明细对账流程

其实这也是分布式事务最通用的实现方式,失败就重试,直到最终成功,不管你是 tcc、还是其他的实现方式,只要出现异常,系统最终都要通过定时去重试,直到最终 一致,感兴趣可以去看看 SEATA 源码,遇到异常也是通过定时任务进行重试。

转账流程

转账补偿流程

这个流程是定时任务定时发起的,
查询小于等于当前时间-指定时间,状态为等待调用的转账记录
并重新发起转账

select * from transfer_list where   update_time <= #{queryEndDate}

总账对账流程

明细对账流程

明细对账,如果数量不大,一天天对没问题,现在银行大多数是基于这种做法,如果文件比较大,可以考虑使用Merkle树,这里就说传统的方式

直接查询对比

这种方式最快,数据不大可以这样搞,同时也需要对方系统提供接口支持

基于文件对比

这种方式也是比较常用的方式,适合数据量大的对比,一般银行会这么做

总结

以上我们介绍了如何设计一个高可靠的系统转账,可以看到还是比较复杂的,细节很多,主要要考虑补偿、安全、并发扣款几方面,这几方面做好才能设计一个高可靠的系统转账。

扫描下面的二维码关注我的微信公众帐号,在微信公众帐号中回复◉加群◉即可加入到我们的技术讨论群里面共同学习。

前言

超链接是指在网页或电子文档中常见的元素,它的主要作用是将一个文本或图像与另一网页、文件或资源链接起来,从而使用户能够通过点击该链接跳转到目标资源、超链接可以起到导航以及引用的作用。超链接通常有以下几种用途:

  • 网页链接
  • 文档链接
  • 内部定位链接
  • 电子邮件链接

在Java中设置超链接

下面小编将为大家介绍如何使用Java实现超链接的添加、删除和带形状的超链接。在下面的例子中我们使用到了GcExcel产品,具体的例子如下。

添加超链接

下面的代码,添加了四个链接,分别是外部文件,网页链接,定位链接及邮件链接。

Workbook wb = new Workbook();
 IWorksheet sheet = wb.getActiveSheet();
 // 添加外部文件链接
 sheet.getRange("A1:B2").getHyperlinks().add(sheet.getRange("A1"),
     "C:/Documents/GcExcel/GrapeCityDocumentsExcel/Project/Hyperlink/SampleFile.xlsx",
     null, "链接至SampleFile.xlsx文件.", "SampleFile.xlsx");
 // 添加网页链接
 sheet.getRange("A3:B4").getHyperlinks().add(sheet.getRange("A3"),
     "http://www.grapecity.com.cn/", null,
     "打开葡萄城官网.", "葡萄城");
 // 添加内部定位链接
 sheet.getRange("A5:B6").getHyperlinks().add(sheet.getRange("A5"),
     null, "Sheet1!$C$3:$E$4", "跳转至 sheet1 C3:E4", "");
 // 添加邮件链接
 sheet.getRange("A7:B8").getHyperlinks().add(sheet.getRange("A7"),
     "mailto:sales.xa@grapecity.com", null,
     "发送至销售部门", "发送至中国销售");
 wb.save("output/hyperlinks.xlsx");

实现效果如下图:

删除超链接

通过 delete 可以删除对应单元格上的超链接,下面代码删除了 "A5:B6" 单元格中的超链接。

Workbook wb = new Workbook();
 wb.open("output/hyperlinks.xlsx");
 IWorksheet sheet = wb.getActiveSheet();
 sheet.getRange("A5:B6").getHyperlinks().delete();
 wb.save("output/removeHyperlinks.xlsx");

实现效果如下图:

带有超链接的形状

除了普通的超链接,GcExcel 还支持带有超链接的形状,通过下面的代码可以创建带有超链接的形状。

// 创建workbook
 Workbook workbook = new Workbook();
 IWorksheet worksheet = workbook.getActiveSheet();

 // 添加形状
 IShape shape1 = worksheet.getShapes().addShape(AutoShapeType.Rectangle, 10, 0, 100, 100);
 shape1.getTextFrame().getTextRange().add("点击访问GcExcel官网");
 IShape shape2 = worksheet.getShapes().addShape(AutoShapeType.RightArrow, 10, 120, 100, 100);
 shape2.getTextFrame().getTextRange().add("跳转至 sheet1 C3:E4");
 IShape shape3 = worksheet.getShapes().addShape(AutoShapeType.Oval, 10, 240, 100, 100);
 shape3.getTextFrame().getTextRange().add("发送邮件");
 IShape shape4 = worksheet.getShapes().addShape(AutoShapeType.LeftArrow, 10, 360, 100, 100);
 shape4.getTextFrame().getTextRange().add("链接external.xlsx文件");

 //给shape1添加网址链接
 worksheet.getHyperlinks().add(shape1,
     "https://www.grapecity.com.cn/developer/grapecitydocuments/excel-java",
     null, "点击访问GcExcel官网", "GcExcel");

 //给shape2添加内部定位链接
 worksheet.getHyperlinks().add(shape2, null, "Sheet1!$C$3:$E$4", "跳转至 sheet1 C3:E4", null);

 //给shape3添加邮件链接
 worksheet.getHyperlinks().add(shape3, "mailto:sales.xa@grapecity.com",
     null, "发送邮件", "发送邮件");

 //给shape4添加文档链接
 String path = "external.xlsx";
 worksheet.getHyperlinks().add(shape4, path, null,
     "链接external.xlsx文件", "External.xlsx");

 //保存到Excel
 workbook.save("output/shapeHyperlink.xlsx");

实现效果如下图:

总结

综上所述,超链接是网页和电子文档中常见的元素,它将文本或图像与其他资源相关联,实现了导航和引用的功能。无论是在网页中还是在Java编程中,我们都可以灵活运用超链接来连接不同的内容和资源。通过添加、删除和带形状的超链接,我们可以实现更加丰富和个性化的用户交互体验。超链接的应用范围广泛,包括网页链接、文档链接、内部定位链接和电子邮件链接等。相信随着技术的不断发展,超链接将在互联网和数字化领域中扮演越来越重要的角色,为用户提供更加便捷和丰富的资源访问方式。

扩展链接:

从表单驱动到模型驱动,解读低代码开发平台的发展趋势

低代码开发平台是什么?

基于分支的版本管理,帮助低代码从项目交付走向定制化产品开发

1、准备材料

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

STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

野火DAP仿真器

XCOM V2.6串口助手

3个滑动变阻器

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的
ADC实现多重ADC采集
,具体为使用ADC1_IN5、ADC2_IN6实现二重ADC采集,使用ADC1_IN5、ADC2_IN6和ADC3_IN5实现三重ADC采集

3、二重ADC转换

3.0、前提知识

STM32F407的三个ADC可以组合实现多重ADC采集
,当仅仅开启一个ADC时,该ADC只能工作在独立模式;当同时启动ADC1和ADC2,则以ADC1为主器件,ADC2为从器件可以工作在双重ADC采集模式下;当同时启动ADC1/2/3,则以ADC1为主器件,ADC2/3为从器件可以工作在三重ADC采集模式下;

在多重 ADC 模式下可实现以下6种模式

  1. 二/三重注入同时模式 + 规则同时模式
  2. 二/三重规则同时模式 + 交替触发模式
  3. 二/三重注入同时模式
  4. 二/三重规则同时模式
  5. 二/三重交替模式
  6. 二/三重交替触发模式

本实验我们只介绍二/三重规则同时模式
,如下图所示为CubeMX配置中可选的所有模式

工作在多重ADC模式下的DMA请求拥有三种DMA模式,这里只介绍DMA access mode 1/2,不会涉及DMA access mode 3

二重规则同时模式ADC采集时只能选择DMA access mode 2,三重规则同时模式ADC采集时只能选择DMA access mode 1

下面请读者重点理解采集完成的数据是如何通过DMA存入用户定义好的数组中的!

当ADC工作在二重规则同时模式下
,此时DMA模式为DMA access mode 2,在ADC1或ADC2转换事件结束时,会生成一个32位DMA传输请求,此请求会将存储在 ADC_CDR 32 位寄存器高位半字中的 ADC2 转换数据传输到SRAM,然后将存储在ADC_CCR低位半字中的ADC1转换数据传输到 SRAM,
也就是说我们只需定义一个包含一个元素的uint32_t DataBuffer[1]数组,以DMA方式启动ADC转换后,只需每次从高16位读取ADC2采集的数据,从低16位读取ADC1采集的数据即可

当ADC工作在三重规则同时模式下
,此时DMA模式为DMA access mode 1,在ADC1、ADC2或ADC3转换事件结束时,会生成三个32位DMA传输请求,之后会发生三次从 ADC_CDR 32 位寄存器到SRAM的传输:首先传输 ADC1 转换数据,然后是 ADC2 转换数据,最后是 ADC3 转换数据,
也就是说我们需定义一个包含三个元素的uint32_t DataBuffer[3]数组,其中第一个元素DataBuffer[0]表示ADC1采集的数据,第二个元素DataBuffer[1]表示ADC2采集的数据,第三个元素DataBuffer[2]表示ADC3采集的数据

上述描述如下图所示
(注释1)

如下图所示为多重ADC框图,当工作在二重ADC时不存在ADC3,ADC1/2/3三个ADC只有ADC1为主ADC,
当以多重ADC工作时,只需要配置主ADC的DMA传输,从ADC无需设置,在启动多重ADC采集时也只能以DMA方式启动主ADC,从ADC以普通方式启动即可
,不能将从ADC也以DMA方式启动,多重ADC采集的数据均会存入32位的通用规则数据寄存器中

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、外设参数配置

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

设置TIM3通用定时器溢出时间100ms,外部触发事件选择更新事件,参数详解请阅读“
STM32CubeMX教程6 TIM 通用定时器 - 生成PWM波
”实验,具体配置如下图所示

在Pinout & Configuration页面左边功能分类栏目
Analog中单击其中ADC1,勾选IN5通道

Mode
(ADC模式):修改为Dual regular simultaneous mode only(需要启用ADC2通道才可以选择二重ADC采集模式)

DMA Access Mode
(DMA模式):选择DMA access mode 2

DMA Continuous Requests
(DMA连续转化请求):使能(需要先增加DMA请求才可以使能)

其他参数与“
STM32CubeMX教程13 ADC - 单通道转换
”实验均保持一致,具体配置如下图所示

单击Configuration中的DMA Settings选项卡对ADC1的DMA请求进行设置,所有配置均与“
STM32CubeMX教程14 ADC - 多通道DMA转换
”实验保持一致,具体配置如下图所示

在Pinout & Configuration页面左边功能分类栏目
Analog中单击其中ADC2,勾选IN6通道
,注意除
Rank

DMA Continuous Requests
参数外所有参数配置必须与ADC1保持一致,否则ADC采集将出现错误,具体配置如下图所示

3.1.3、外设中断配置

在Pinout & Configuration页面左边System Core/NVIC中
勾选DMA2 Stream0 全局中断
,然后选择合适的中断优先级即可,具体配置如下图所示

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、外设初始化调用流程

请阅读“
STM32CubeMX教程14 ADC - 多通道DMA转换
”实验“3.2.1、外设初始化调用流程”小节

3.2.2、外设中断调用流程

请阅读“
STM32CubeMX教程14 ADC - 多通道DMA转换
”实验“3.2.2、外设中断调用流程”小节

3.2.3、添加其他必要代码

在adc.c中重新实现ADC转换完成回调函数HAL_ADC_ConvCpltCallback()
,具体代码如下所示

源代码如下

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /*定时器中断启动DMA二重ADC转换*/
    uint32_t Volt1,Volt2;
    uint32_t adcValue=DataBuffer[0];	

    /*从低16位取出ADC1采集数据*/
    uint32_t ADC1_val=adcValue & 0x0000FFFF;	
    Volt1=3300*ADC1_val;	
    Volt1=Volt1>>12;		
    
    /*从高16位取出ADC2采集数据*/
    uint32_t ADC2_val=adcValue & 0xFFFF0000;
    ADC2_val= ADC2_val>>16;
    Volt2=3300*ADC2_val;
    Volt2=Volt2>>12;		
    
    printf("Volt1:%d, Volt2:%d\r\n",Volt1,Volt2);
}

在主函数main中启动二重ADC转化,一些全局变量定义及启动源代码如下

/*main.c全局变量定义*/
uint32_t DataBuffer[BATCH_DATA_LEN];

/*main.h变量声明*/
#define BATCH_DATA_LEN 1
extern uint32_t DataBuffer[BATCH_DATA_LEN];

/*ADC启动代码*/
HAL_ADC_Start(&hadc2);
HAL_ADCEx_MultiModeStart_DMA(&hadc1,DataBuffer,BATCH_DATA_LEN);
HAL_TIM_Base_Start(&htim3);

为什么二重ADC转化下DMA要将数据传输到uint32 DataBuffer[1]?

二重ADC转化下DMA模式为DMA access mode 2,在该模式下ADC1转换完成的数据会传输到32位的 DataBuffer[0] 的低16位,而ADC2转换完成的数据会传输到32位的 DataBuffer[0] 的高16位

4、常用函数

/*多重ADC以DMA方式启动*/
HAL_StatusTypeDef HAL_ADCEx_MultiModeStart_DMA(ADC_HandleTypeDef *hadc, uint32_t *pData, uint32_t Length)

5、烧录验证

烧录程序,单片机上电之后,串口不断的输出ADC1_IN5、ADC2_IN6的采集值转化为的电压值,笔者将两个滑动变阻器按照ADC1_IN5、ADC2_IN6的顺序,分别从一端缓慢拧到另一端,可以从串口输出的数据看到,三个通道采集到的电压值从最小值0慢慢变到最大3300

6、三重ADC转换

6.1、CubeMX相关配置

在Pinout & Configuration页面左边功能分类栏目
Analog中单击其中ADC3,勾选IN5通道,所有参数与二重ADC转换ADC2参数一致
,在配置ADC1为三重ADC规则通道采集时ADC3的触发源参数会消失,因此无需理会,具体ADC3参数配置如下图所示

在Pinout & Configuration页面左边功能分类栏目
Analog中单击其中ADC1,将其模式修改为Triple regular simultaneous mode only,DMA模式修改为DMA access mode 1

ADC1的其他参数与二重ADC转换时的参数一致,ADC2的配置、ADC1 DMA的配置和NVIC的设置均与二重ADC采集一致,具体ADC1参数配置如下图所示

6.2、添加其他必要代码

/*main.c全局变量定义*/
uint32_t DataBuffer[BATCH_DATA_LEN];

/*main.h变量声明*/
#define BATCH_DATA_LEN 3
extern uint32_t DataBuffer[BATCH_DATA_LEN];

/*主函数中ADC启动代码*/
HAL_ADC_Start(&hadc2);
HAL_ADC_Start(&hadc3);
HAL_ADCEx_MultiModeStart_DMA(&hadc1,DataBuffer,BATCH_DATA_LEN);
HAL_TIM_Base_Start(&htim3);

/*adc.c中重新实现转换完成中断回调*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    /*定时器中断启动DMA三重ADC转换*/
    uint32_t val=0,Volt=0;
    for(uint8_t i=0;i<BATCH_DATA_LEN;i++)
    {
        val=DataBuffer[i];
        Volt=(3300*val)>>12;
        printf("ADC%d, val:%d, Volt:%d\r\n",i,val,Volt);
    }
    printf("\r\n");
}

为什么三重ADC转化下DMA要将数据传输到uint32 DataBuffer[3]?

二重ADC转化下DMA模式为DMA access mode 1,在该模式下ADC1转换完成的数据会传输到32位的 DataBuffer[0],ADC2转换完成的数据会传输到32位的 DataBuffer[1],ADC3转换完成的数据会传输到32位的 DataBuffer[2]

6.3、实验现象

烧录程序,单片机上电之后,串口不断的输出ADC1_IN5、ADC2_IN6和ADC3_IN5的采集值,笔者将三个滑动变阻器按照ADC1_IN5、ADC2_IN6和ADC3_IN5的顺序,分别从一端缓慢拧到另一端,可以从串口输出的数据看到,三个通道采集到的ADC数据从最小值0慢慢变到最大4095

7、注释详解

注释1
:图片来源STM32F4xx中文参考手册

8、参考资料

主要参考
STM32Cube高效开发教程(基础篇)
320页14.6小节实验

笔者认为该章节提到一个BUG其实是错误的,从ADC不应该以DMA方式启动,也无需在STM32CubeMX生成的工程代码中手动修改DMAContinuousRequests为ENABLE

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