2024年8月

技术背景

前面写过一篇
关于Cython和C语言混合编程的文章
,在Cython中可以使用非常Pythonic的方法去调用C语言中的函数。另外我们也曾在
文章
中介绍过Python中使用CUDA计算的一种方案。其实从Python中去调用CUDA有很多种解决方案,例如直接使用MindSpore、PyTorch、Jax等成熟的框架进行GPU加速,也可以使用Numba的CUDA即时编译JIT来直接对Python函数进行GPU加速,还可以使用PyCuda或者Cupy来实现Python中的GPU加速。但是不论是哪一种方案,其实本质上都绕不开到C语言和Kernel函数的转换。这里提供了另外一个思路,允许我们给出Python的API接口,又可以同时使用熟悉的Python函数和C函数、CUDA函数,以达到不同的目的,这就是Python+Cython+C/CUDA的方案。

构建思路

随着Python语言在机器学习领域的大力推广,Python编程对于众多的科技工作者而言总是一个较为靠前的选项。所以不论使用何种方式构建自己的项目,一般我们都会选择面向用户开放一个Python的API接口。如果做一个项目,项目的本身对于程序性能的要求并不高的情况下,我们可以直接使用Python进行相关的计算。但如果对计算性能要求比较高,那么C/CUDA会是一个更好的选择。那么剩下一个待解决的问题就是,如何构建Python API与C/CUDA之间的交互。其实这里本身有两个方案:1. 直接把C/CUDA代码编译成
*.so
动态链接库,然后在Python中加载动态链接库的函数作为接口。2. 只用C/CUDA执行计算,把C/CUDA代码编译成
*.so
动态链接库,从Cython中对两者函数进行调度和管理。

从用户面的角度来说,第一个方案虽然可能性能还不错,但凡是需要新增一些功能模块,都需要去修改C/CUDA代码。如果只是功能模块还好说,如果涉及到任务调度和接口传输的问题,那门槛就更高了。第二个方案在性能上有可能略有衰减,但是因为接口传输和调度都是在Cython中完成的,即使是只会Python的开发者,也可以以Cython为入口来开发自己需要的模块。

案例演示

这里我们演示一个简单的
Hello World
程序,首先我们可以写一个
hello.cu
的CUDA文件:

#include<stdio.h>
	
__global__ void HelloKernel(void){
    printf("Hello World From GPU!\n");
}

int main(){
    HelloKernel<<<1,3>>>();
    cudaDeviceReset();
    return 1;
}

然后使用
nvcc
对这个CUDA项目进行编译:

$ nvcc -c ./hello.cu -Xcompiler -fPIC -o ./libhello.so

得到了一个
libhello.so
的动态链接文件。然后在
hello.pyx
中引入这个动态链接库:

# cythonize -i hello.pyx
import ctypes
libcuda = ctypes.CDLL('./libhello.so')

cpdef int run_cuda():
    cdef int res
    res = libcuda.main()
    if res:
        print ('Load cuda function successfully!')
    else:
        print ('Failed to load cuda function.')
    return 1

这里我们可以使用
cythonize
指令编译pyx文件,也可以使用python的
setup.py
配置编译指令。为了方便我们就用
cythonize
演示一下这个案例:

$ cythonize -i hello.pyx

那么会得到一个
hello.c
文件和一个
hello.cpython-37m-x86_64-linux-gnu.so
动态链接文件。到这里相当于我们对CUDA文件中的
main函数
封装了一层cython的调用,然后就可以在python文件中调用这个cython的
run_cuda()
函数:

# $ python3 hello.py
from hello import run_cuda
run_cuda()

然后运行这个python文件,输出为:

$ python3 hello.py
Hello World From GPU!
Hello World From GPU!
Hello World From GPU!
Load cuda function successfully!

成功的从Python侧调用了CUDA Kernel函数。

其他调用方法

前面提到,我们也可以在C程序中直接调用这个CUDA函数。例如在上面我们编译好
libhello.so
的CUDA动态链接库之后,用一个C文件去调用动态链接库:

#include <dlfcn.h>

int main()
{
    void* handle = dlopen("./libhello.so", RTLD_LAZY);
    int (*helloFromGPU)();
    helloFromGPU = dlsym(handle, "main");
    helloFromGPU();
    dlclose(handle);
    return 1;
}

然后使用如下指令编译成一个可执行文件:

$ gcc -o hello_c hello_c.c -ldl

再执行这个可执行文件,可以得到跟Python程序类似的打印结果。如果需要将这部分的C代码也编译成动态链接库供Python调用的话,可以使用如下指令分两次编译:

$ gcc -fPIC -c hello_c.c -o hello_c.o
$ gcc -shared hello_c.o -o libhello_c.so

路径问题

如果需要从其他路径(例如动态链接库的公共存储位置)来引入相关的动态链接库的话,可以在Python中获取环境变量。例如,我们先在环境中配置一个
LD_LIBRARY_TEST_PATH

$ export LD_LIBRARY_TEST_PATH=$(pwd)

这里是把当前路径的绝对路径设置为环境变量中的一个地址。然后在Python中可以查询到这个环境变量:

In [1]: import os

In [2]: os.getenv('LD_LIBRARY_TEST_PATH')
Out[2]: '/home/mindsponge/tests/toolkits'

这样在很多时候可以从外部解决库的引用地址的问题,不需要每次都去手动修改或者配置。

总结概要

从Python接口调用GPU进行加速的方案有很多,包括Cupy和PyCuda以及之前介绍过的Numba,还可以使用MindSpore、PyTorch和Jax等成熟的深度学习框架,这里介绍了一种直接写CUDA Kernel函数的方案。为了能够做到CUDA-C和Python编程的分离,这里引入了Cython作为中间接口,这样一来Python开发者和C开发者可以去共同开发相应的高性能方法。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/cython-cuda.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

前言

前面介绍了定时器和输出比较,这一节主要介绍一下输入捕获测量输入频率和PWM占空比,然后介绍一下编码器接口。

一、输入捕获

1.什么是输入捕获

当输入的引脚有指定电平跳变时,会将计数器CNT中的值保存在CCR中,这个就称为输入捕获。

2.输入捕获测频率

我们可以通过获取输入的值来测量频率,这里有两个计算的方法:

2.1 测频法

在一个闸门时间T内,对上升沿进行计数,得到计数值N,频率就为=N/T。

2.2 测周法

在一个标准的频率fc下,对上升沿进行计数,得到计数值N,频率就为:fc / N。

3.输入捕获的内部结构

接下来了解一下输入捕获的内部结构,只有了解了内部结构,我们写代码才会很轻松,下图就是输入捕获的内部结构:

img

可以看到,从GPIO口输入电平,然后通过输入捕获单元,检测到触发电平,如果是设定的电平就会将CNT中的值保存到CCR中,然后通过配置从模式来将CNT中的值清0。

了解了这个结构后我们大概就知道如何配置输入捕获了。

首先配置外部输入,即GPIO口,然后配置定时器,因为输入捕获是定时器的一个工作模式,然后配置从模式,使用从模式的Reset来清空计数器CNT,这样就是配置好输入捕获了。

我们可以通过读取CCR中的值来计算频率。

4.软件实现

前面了解了硬件结构后,我们就可以使用软件来一一实现了,首先要打开时钟,时钟是stm32中最重要的一个部件,如果不打开时钟,就算配置了也没办法运行。

4.1 打开时钟

这里我们从上面的硬件分析可以得到,其实就是只用开启两个时钟,一个是GPIO的时钟,另一个就是TIM的时钟,输入捕获这个功能是在TIM中的,所以打开了TIM的时钟,输入捕获和输出比较也就打开了。

所以这里的代码就比较简单,我这里使用TIM3作为输入捕获的定时器,那么打开的代码如下:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

4.2 配置GPIO口

GPIO口做为一个外部信号的输入口,所以要为其设置输入模式,那选择什么模式最好呢?这里可以翻看一下手册:

img

可以看到GPIO配置为浮空输入,但是其实这里也可以配置为上拉输入,浮空输入就是能很好的捕捉电平的变化,但是没有上拉或者下拉导致不是很稳定,这里因为都是一个电平的跳变,就算是使用上拉,当输入的是高电平后会跳变为低电平,然后再变成高电平,所以这里可以选上拉或者下拉。

如果你不清楚这个跳变到底是什么,你可以选择浮空输入,这样获得的要好一点。

这里就是正常的配置GPIO口即可:

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;   // 这里使用上拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

这里是使用的是TIM3的通道1。

4.3 配置TIM

现在需要配置一下TIM了,在配置GPIO那配置的是TIM3的通道1,所以这要配置一下TIM3。

如果你选择的是TIM2,那上面要配置TIM2对应的引脚。

这里的配置方法和之前配置定时器那一样:

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};

TIM_InternalClockConfig(TIM3);    // 使用内部时钟
    
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;       // 不分频
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;     // 向上计数
TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1;     // 这里需要注意
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;      // 频率
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;       // 重复计数器,高级定时器才有的功能
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);

这里的自动重装寄存器是填了一个最大值,是因为这里的输入捕获是用CNT对变化电平进行计数,而CNT中的值和自动重装寄存器ARR中的值一致时,就会使得计数器清0然后发送一个信号,如果这里到了ARR中的值后,还有变化的电平,是不是就会被舍弃。

所以这里为了防止这个问题,我们将ARR中的值设置成最大值,防止出现到了自动重装值时还有变化,所以用个最大的值,防止溢出。

4.4 配置输入捕获

这个也是使用一个结构体来进行配置的,结构体类型为:

typedef struct
{

  uint16_t TIM_Channel;      /*!< 指定 TIM 通道。
  此参数的值可以是 @ref TIM_Channel */

  uint16_t TIM_ICPolarity;   /*!< 指定输入信号的有效边沿。
  此参数的值可以是 @ref TIM_Input_Capture_Polarity */

  uint16_t TIM_ICSelection;  /*!< 指定输入。
  此参数的值可以是 @ref TIM_Input_Capture_Selection */

  uint16_t TIM_ICPrescaler;  /*!< 指定输入捕获预分频器。
  此参数的值可以是 @ref TIM_Input_Capture_Prescaler */

  uint16_t TIM_ICFilter;     /*!< 指定输入捕获筛选器。
  此参数可以是介于 0x0 和 0xF 之间的数字 */
} TIM_ICInitTypeDef;

第一个参数
TIM_Channel
是选择通道,这个是通过之前说的引脚来进行设定的:

img

img

img

前面比如是设置了TIM3_CH1,那这里就填写
TIM_Channel_1

第二个参数
TIM_ICPolarity
,这个参数是选择有效信号的,可以选择
TIM_ICPolarity_Rising
高电平触发,
TIM_ICPolarity_Falling
低电平触发。

第三个参数
TIM_ICSelection
,指定输入,有信号直连
TIM_ICSelection_DirectTI
和交叉
TIM_ICSelection_IndirectTI
。这个需要拿个图来看看:

img

从这可以看到,我们通过选择
TI1FP1
它可以连到两个输入通道,一个是连到输入通道1,另一个连到通道2,直接配置到输入通道1的就叫做直连模式,如果连到输入通道2就叫做交叉模式。

第四个参数
TIM_ICPrescaler
就是分配器。

第五个参数
TIM_ICFilter
这个参数是选择过滤器的分辨率。

了解了结构体后我们就可以配置一下结构体中的内容了,这里我直接使用定时器通道1和输入通道1,所以模式就是直连:

TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;      // 有效电平高电平
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;     // 直连
TIM_ICInit(TIM3, &TIM_ICInitStruct);

这里再举一个例子,我这还是定时器通道1,输入通道变成了通道2,那这就选择交叉模式,代码如下:

TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;      // 有效电平高电平
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI;     // 交叉
TIM_ICInit(TIM3, &TIM_ICInitStruct);

这样就可以通过读取输入通道2中的CCR值来获取有效电平的次数了。

我们可以通过这个来计算PWM的频率和PWM的占空比,一个输入通道来获取PWM频率,另一个通道获取PWM的占空比。

一个通道两个设置的方法如下:

TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;      // 有效电平高电平
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;     // 直连
TIM_ICInit(TIM3, &TIM_ICInitStruct);

TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling;      // 有效电平低电平
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI;     // 交叉
TIM_ICInit(TIM3, &TIM_ICInitStruct);

一个通道分别有直连和交叉,然后一个是高电平有效,一个是低电平有效。

4.5 配置从模式

我们在前面知道,需要配置个从模式让从模式去执行
Reset
操作来讲CNT中的值清0,这里只需要使用俩个函数就可以实现,首先是第一个
TIM_SelectInputTrigger
,这个函数是选择触发器源,在这个图中:

img

可以看到在边缘检测电路后有四个信号,分别是是
TI1FP1

TI2FP2

TI3FP3

TI4FP4
,这几个信号可以触发从模式,我们先用这个定时器的输入通道后,然后通过
TIM_SelectSlaveMode
函数来设置从模式。

比如说我们上面设置的是TIM3定时器,而且是使用的是TIM3_CH1,那这的设置函数就为:

TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

再比如说,这里是TIM2的CH2,那设置函数为:

TIM_SelectInputTrigger(TIM2, TIM_TS_TI2FP2);
TIM_SelectSlaveMode(TIM2, TIM_SlaveMode_Reset);

这里的信号
TIxFPx
和定时器是什么无关,只和通道有关。stm32f103c8t6只能配置4个输入捕获,所以这里的x是从1到4。

4.6 使能定时器

这个就不详细说了,就是使用
TIM_Cmd()
函数就可以使能了,代码如下:

TIM_Cmd(TIMx, ENABLE);

5.输入捕获测频率和占空比

上面介绍了代码的实现,这里来介绍一下,如果使用输入捕获来获取PWM频率和PWM占空比。

这个的代码实现比较简单,就是设置为输入捕获模式,然后写两个转换函数即可:

#include "ic.h"

void IC_Init()
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0};
    TIM_ICInitTypeDef TIM_ICInitStruct = {0};
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;  // 这里使用的是TIM3_CH2
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    TIM_InternalClockConfig(TIM3);
    
    TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1;
    TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;
    TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
    
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;       // 直连模式要选择通道2
    TIM_ICInitStruct.TIM_ICFilter = 0xF;
    TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
    TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;        // 直连模式
    TIM_ICInit(TIM3, &TIM_ICInitStruct);
    
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;          // 交叉模式要选择通道1
    TIM_ICInitStruct.TIM_ICFilter = 0xF;
    TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling;   // 这里需要和上面设置的有效电平分割开,要不然会出现运行不了的问题,我试过,必须要分开,要不然测试不了。
    TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_IndirectTI;      // 交叉模式
    TIM_ICInit(TIM3, &TIM_ICInitStruct);
    
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI2FP2);
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
    
    TIM_Cmd(TIM3, ENABLE);
}

uint32_t IC_Get_Freq()
{
    // 转换为频率
    return 1000000 / (TIM_GetCapture2(TIM3) + 1);      // 用输入捕获设置的频率除以标准时钟72MHz后得到1000000,然后用这个频率除以获取的N即可得到频率。
}

uint32_t IC_Get_Pulse()
{
    // 转换为占空比
    return ((TIM_GetCapture1(TIM3) + 1) * 100) / IC_Get_Freq();      // 这个算法是在PWM占空比那说过的。这里不重复
}

这样就可以获取得到PWM的频率和占空比了。

二、编码器接口

1.什么是编码器接口

编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减。

下图是一个正交编码器的图:

img

可以看到,编码器的这个波形,A项和B项的波形是相差90度的,所以这两个波形在一起就可以分正和反了。

2.编码器接口的硬件结构

我们可以用前面学过的输入捕获或者是中断来实现编码器的实现,但是,在stm32f103c8t6中设置了一个编码器接口,我们可以直接使用硬件来实现编码器的测试。

硬件结构如下:

img

可以看到非常的简单,只需要在前面的基础上增加个编译器接口的配置即可。

使用的函数是:

void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode,uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);

这个函数就可以设置编码器接口了,首先设置GPIO口,因为我们的这个编码器是需要两个输入的,所以需要设置两个引脚作为输入引脚,然后配置定时器,配置为定时器后需要配置输入捕获,之后就使用上面的函数进行配置编码器接口。

第一个参数
TIMx
是定时器的编号。

第二个参数
TIM_EncoderMode
是使用模式,可以选下面这几个模式:

模式 解释
TIM_EncoderMode_TI1 使用TI1FP1作为编码器设置
TIM_EncoderMode_TI2 使用TI2FP2作为编码器设置
TIM_EncoderMode_TI12 使用TI1FP1和TI2FP2作为编码器设置

第三个参数
TIM_IC1Polarity
是选择TI1FP1是正项还是反向,第四个参数
TIM_IC2Polarity
和第三个参数一样,是选择TI2FP2。

当两个都选择正向或者是反向时,就如下图的时许一样:

img

当TI1有上升电平,TI2还是低电平时,计数器就会自增,当TI2有上升电平,TI1是高电平时,计数器也会自增。

当TI1FP1或者TI2FP2有一个是反项时就会变成下面的时许:

img

在正向时TI1有上升电平,TI2还是低电平时,计数器就会自增,但反向后就是变成自减了。

也就是说当正向的自增转到反向后就会变成自减。

3.软件实现

硬件结构讲完成后,就可以用代码来对其中的功能进行一个实现了。

3.1 时钟开启

这里的开启时钟也就只需要开启GPIO的时钟和定时器的时钟,因为编码器接口也是定时器的一个附加功能,所以只用开启GPIO和定时器的时钟即可,不需要额外的时钟开启。

3.2 GPIO口配置

这里还是需要配置GPIO口,因为需要外部的编码器的输入,这里选择的引脚和定时器的输入捕获和输出比较一样,需要查看引脚的复用功能是不是在TIM定时器的通道上,如果不在那就不能使用。

这里选择的是定时器3的通道1和通道2,对应的引脚是PA6和PA7。

配置代码如下:

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

3.3 配置定时器

配置的方法也是和上面一样,打开TIM3定时器,然后配置一系列操作,但这里需要改变一下,预分频器PSC这里就不需要分频,只需要填写1-1即可,1就是不分频,这里是又编码器接口硬件来进行操作即可,然后自动重装寄存器ARR只需要配置最大即可,也不需要TIM指定使用内部时钟,这个时钟也是由编码器接口来提供的。

TIM_TimeBaseTypeDef TIM_TimeBaseStruct = {0};
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStruct.TIM_Period = 65536 - 1;
TIM_TimeBaseStruct.TIM_Prescaler = 1 - 1;
TIM_TimeBaseStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);

3.4 配置输入捕获

这里需要注意一下,有些东西在这配置时不需要配置,只需要配置两个参数即可,需要配置的有通道
TIM_Channel
和过滤器的分辨率
TIM_ICFilter
然后其他的不需要配置。

所以这里需要使用一个函数来为结构体赋一个默认值:

TIM_ICInitTypeDef TIM_ICInitStruct = {0};
TIM_ICStructInit(&TIM_ICInitStruct);

TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0x0F;
TIM_Init(TIM3, &TIM_ICInitStruct);

TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
TIM_ICInitStruct.TIM_ICFilter = 0x0F;
TIM_Init(TIM3, &TIM_ICInitStruct);

这里可能会比较好奇为什么那些都不配置,比如说预分频器和有效电平和反向,这是因为在配置编码器接口那就已经配置好了的,不需要我们再进行配置,编码器接口会为这个输入捕获一个时钟,所以就不需要分频了,然后反向也是,也是那一个函数就可以配置好了的。

3.5配置编码器接口

这里就使用上面介绍的那个函数即可配置:

TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

这里我都设置的是不反向,是为了方便。

3.6 使能定时器

最后使能一下定时器就可以了,使能方法之前介绍了很多遍,这里就不再拉出代码了,下面完整代码中有。

3.7 完整代码

这里为了方便,我把完整代码贴出来:

void Encoder_Init()
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct = {0};
    TIM_ICInitTypeDef TIM_ICInitStruct = {0};
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
    
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseStruct.TIM_Period = 65536 - 1;
    TIM_TimeBaseStruct.TIM_Prescaler = 1 - 1;
    TIM_TimeBaseStruct.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStruct);
    
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStruct.TIM_ICFilter = 0x0F;
    TIM_ICInit(TIM3, &TIM_ICInitStruct);
    
    TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
    TIM_ICInitStruct.TIM_ICFilter = 0x0F;
    TIM_ICInit(TIM3, &TIM_ICInitStruct);
    
    TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
    
    TIM_Cmd(TIM3, ENABLE);
}

总结

通用定时器的相关操作就介绍完成了,后面有机会的话给大家介绍一下高级定时器,高级定时器可以操作三项无刷电机,等后面有时间我做一个无人机会使用到高级定时器。

本文节选自清华大学出版社出版的图书《数据资产管理核心技术与应用》,作者为张永清等著。

从Spark 执行计划中获取数据血缘

因为数据处理任务会涉及到数据的转换和处理,所以从数据任务中解析血缘也是获取数据血缘的渠道之一,Spark 是大数据中数据处理最常用的一个技术组件,既可以做实时任务的处理,也可以做离线任务的处理。Spark在执行每一条SQL语句的时候,都会生成一个执行计划,这一点和很多数据库的做法很类似,都是SQL语句在执行时,先生成执行计划。如下图3-1-10所示,在Spark的官方文档链接https://spark.apache.org/docs/latest/sql-ref-syntax-qry-explain.html#content中,有明确提到,可以根据EXPLAIN关键字来获取执行计划,这和很多数据库查看执行计划的方式很类似。

图3-1-10

Spark底层生成执行计划以及处理执行计划的过程如下图3-1-11所示。本文节选自清华大学出版社出版的图书《数据资产管理核心技术与应用》,作者为张永清等著。

图3-1-11

从图中可以看到,

1、 执行SQL语句或者Data Frame时,会先生成一个Unresolved Logical Plan,就是没有做过任何处理和分析的逻辑执行计划,仅仅会从SQL语法的角度做一些基础性的校验。

2、 之后通过获取Catalog的数据,对需要执行的SQL语句做表名、列名的进一步分析校验,从而生成一个可以直接运行的逻辑执行计划。

3、 但是Spark底层会有个优化器来生成一个最优的执行操作方式,从而生成一个优化后的最佳逻辑执行计划。

4、 将最终确定下来的逻辑执行计划转换为物理执行计划,转换为最终的代码进行执行。

Spark的执行计划其实就是数据处理的过程计划,会将SQL语句或者DataFrame 做解析,并且结合Catalog一起,生成最终数据转换和处理的代码。所以可以从Spark的执行计划中,获取到数据的转换逻辑,从而解析到数据的血缘。但是spark的执行计划都是在spark底层内部自动处理的,如何获取到每次Spark任务的执行计划的信息呢?其实在Spark底层有一套Listener的架构设计,可以通过Spark Listener 来获取到spark 底层很多执行的数据信息。

在spark的源码中,以Scala的形式提供了一个org.apache.spark.sql.util.QueryExecutionListener  trait (类似Java 语言的接口),来作为Spark SQL等任务执行的监听器。在org.apache.spark.sql.util.QueryExecutionListener  中提供了如下表3-1-2所示的两个方法。

表3-1-2

方法名

描述

def onSuccess(funcName: String, qe: QueryExecution, durationNs: Long): Unit

执行成功时,调用的方法,其中包括了执行计划参数,这里的执行计划可以是逻辑计划或者物理计划

def onFailure(funcName: String, qe: QueryExecution, exception: Exception): Unit

执行失败时,调用的方法,其中同样也包括了执行计划参数,这里的执行计划可以是逻辑计划或者物理计划

因此可以借用QueryExecutionListener  来主动让Spark在执行任务时,将执行计划信息推送到自己的系统或者数据库中,然后再做进一步的解析,如下图3-1-12所示。本文节选自清华大学出版社出版的图书《数据资产管理核心技术与应用》,作者为张永清等著。

图3-1-12

importorg.apache.spark.internal.Loggingimportorg.apache.spark.sql.SparkSessionimportorg.apache.spark.sql.execution.QueryExecutionimportorg.apache.spark.sql.util.QueryExecutionListenercase class PlanExecutionListener(sparkSession: SparkSession) extendsQueryExecutionListener with Logging{

override def onSuccess(funcName: String, qe: QueryExecution, durationNs: Long): Unit
=withErrorHandling(qe) {//执行成功时,调用解析执行计划的方法 planParser(qe)
}

override def onFailure(funcName: String, qe: QueryExecution, exception: Exception): Unit
=withErrorHandling(qe) {

}
private def withErrorHandling(qe: QueryExecution)(body: => Unit): Unit ={trybodycatch{case NonFatal(e) =>val ctx=qe.sparkSession.sparkContext
logError(s
"Unexpected error occurred during lineage processing for application: ${ctx.appName} #${ctx.applicationId}", e)
}
}


def planParser(qe: QueryExecution): Unit
={
logInfo(
"----------- start to get spark analyzed LogicPlan--------")//解析执行计划,并且将执行计划的数据发送到自有的系统或者数据库中 ......
}
}

上面的代码中,实现了QueryExecutionListener 这个trait中的onSuccess和onFailure这两个方法,只有在onSuccess时,才需要获取执行计划的数据,因为只有onSuccess时的血缘才是有效的。

实现好了自定义的QueryExecutionListener后,可以通过sparkSession.listenerManager.register来将自己实现的PlanExecutionListener 注册到Spark会话中,listenerManager是Spark中Listener的管理器。

在获取到执行计划时,需要再结合Catalog一起,来进一步解析血缘的数据,如下图3-1-13所示

图3-1-13

Spark 中常见的执行计划实现类如下表3-1-3所示,获取数据血缘时,就是需要从如下的这些执行计划中解析血缘关系。本文节选自清华大学出版社出版的图书《数据资产管理核心技术与应用》,作者为张永清等著。

表3-1-3

执行计划实现类

描述

org.apache.spark.sql.execution.datasources.LogicalRelation

一般用于解析字段级的关联关系

org.apache.spark.sql.catalyst.catalog.HiveTableRelation

Hive 表关联关系的执行计划,一般用于SQL执行时,存在关联查询的情况会出现该执行计划。

org.apache.spark.sql.hive.execution.InsertIntoHiveTable

一般是在执行insert into 的SQL 语句时才会产生的执行计划,例如insert into xxx_table(colum1,column2) values("4","zhangsan")

org.apache.spark.sql.execution.datasources

.InsertIntoHadoopFsRelationCommand

一般用于执行类似    sparkSession

.read

.table("xx_source_table ")

.limit(10)

.write

.mode(SaveMode.Append)

.insertInto("xx_target_table ")产生的执行计划。

org.apache.spark.sql.hive.execution.

CreateHiveTableAsSelectCommand

一般是在执行create table xxx_table as的SQL 语句时才会产生的执行计划,例如create table xx_target_table as select * from xx_source_table

org.apache.spark.sql.execution.command

.CreateDataSourceTableAsSelectCommand

一般用于执行类似sparkSession

.read

.table("xx_source_table")

.limit(10)

.write

.mode(SaveMode.Append)

.saveAsTable("xx_target_table")产生的执行计划。

org.apache.spark.sql.execution.datasources

.InsertIntoDataSourceCommand

一般用于将SQL查询结果写入到一张表中,比如insert into xxx_target_table select * from xxx_source_table

如下是以org.apache.spark.sql.execution.datasources

.InsertIntoHadoopFsRelationCommand 为例的spark 执行计划的数据,如下数据已经将原始的执行计划转换为了json格式的数据,方便做展示。

.................更多内容,请参考清华大学出版社出版的图书《数据资产管理核心技术与应用》,作者为张永清等著

Label Studio是Heartex公司开发的一款在线数据标注工具,分为社区版(开源)和企业版(云服务,收费),企业版提供了增强的安全性(单点登录、角色基于访问控制、SOC2)、团队管理、分析和报告,以及正常运行时间和支持服务水平协议。即便是免费的开源版本,也足以支持广泛的标注类型,包括图像分类、目标检测、语义分割等。也支持多种数据类型,如文本、图像、音频和视频等。它还支持集成机器学习模型,可以满足各种复杂的数据标注需求。

安装Label Studio

Label Studio的安装方法有多种,主流的有pip、conda安装,也支持docker安装,这些安装方法,网上可以搜索到很多,这里不做介绍了。作为开发人员,更习惯于通过源码安装,本文就介绍如何从github上clone源码安装。

下载源码

安装poetry

Poetry 是一个 Python 打包和依赖管理工具,旨在简化 Python 包的创建、发布和依赖管理。与传统的 setuptools、pip 和 requirements.txt 的组合相比,Poetry 提供了一个统一和简化的工具和工作流程。
cd label-studio
pip install poetry

安装后,执行以下命令

  • poetry config list
cache-dir = "/Users/oheroj/Library/Caches/pypoetry"
experimental.system-git-client = false
installer.max-workers = null
installer.modern-installation = true
installer.no-binary = null
installer.parallel = true
keyring.enabled = true
solver.lazy-wheel = true
virtualenvs.create = true
virtualenvs.in-project = null
virtualenvs.options.always-copy = false
virtualenvs.options.no-pip = false
virtualenvs.options.no-setuptools = false
virtualenvs.options.system-site-packages = false
virtualenvs.path = "{cache-dir}/virtualenvs"  # /Users/laijihua/Library/Caches/pypoetry/virtualenvs
virtualenvs.prefer-active-python = false
virtualenvs.prompt = "{project_name}-py{python_version}"
warnings.export = true

其中
virtualenvs.create = true
若改为 false,则可以停止 poetry 在检查不到虚拟环境是自动创建的行为模式,但是建议不要改动。


virtualenvs.in-project = false
就是我们要修改的目标,使用指令:

poetry config virtualenvs.in-project true

虚拟环境将创建在项目根目录下,而不是在缓存目录下。

安装依赖

执行以下指令,安装依赖:

poetry install

如果出现以下错误

 - Installing label-studio-sdk (1.0.4 https://github.com/HumanSignal/label-studio-sdk/archive/0b7ece0554de291d05d446ea5240e56724e384e8.zip): Failed

  SSLCertVerificationError

  [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1129)

则执行以下指令

poetry add label-studio-sdk@latest

依赖包安装完成后,执行以下命令:

poetry run python label_studio/manage.py migrate
poetry run python label_studio/manage.py collectstatic

启动服务

poetry run python label_studio/manage.py runserver

stable diffusion 实践与测试

放大

原图高清放大

原始图片


当不满意图片质量的时候

使用stable diffusion进行二次处理
选择适合图片风格的模型,再次根据图片写出提示词

输入原图像1024尺寸之后调整重绘幅度

采样器automatic在这里会选择karras

原图异变放大

a dog,orange_overalls,hard hat,white dog,take spade,in mountain,rain,gloves,

将cat 改为 dog,并且增加重绘幅度到0.4

重绘幅度过高,则sd会直接按照提示词生成图片,不参考用户的图片,会超乎想象。建议高清放大重绘幅度控制在0.35

若想不好提示词可以用GPTS来生成

风格转换

动漫转真人



真人不带Anime6B更佳
真人转动漫


线稿上色





不同模型风格不一样