搜索
bottom↓
回复: 1

《DMF407电机控制专题教程_V1.0》第7章 直流有刷电机编码器测速

[复制链接]

出0入234汤圆

发表于 2022-8-12 15:39:16 | 显示全部楼层 |阅读模式
本帖最后由 正点原子 于 2022-8-12 15:39 编辑

电机开发板:
1)实验平台:正点原子DMF407电机开发板
2)平台购买地址: https://detail.tmall.com/item.htm?&id=677230699323
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/stm32dj/ATK-DMF407.html
4)对正点原子电机开发板感兴趣的同学可以加群讨论: 592929122 lQLPJxaFi2zaB4UWWrDAMgIsFEW2pwLb3abnwDMA_90_22.png
lQDPJxaFi2nfFizMjM0CbLCPlxn_FVheIQLb3aGrwFQA_620_140.jpg

lQLPJxaFi2nfFhLMkM0BXrDNvOUyeU_FPgLb3aGvQNIA_350_144.png


第7章 直流有刷电机编码器测速

本章我们主要来学习编码器测速的原理,并实现直流有刷电机的速度检测。在实际的电机控制中,我们时常需要关注电机的转速,并以此来了解电机的运行状态,因此,如何获得一个准确的电机速度至关重要。
本章分为如下几个小节:
7.1 编码器简介
7.2 编码器测速原理
7.3 编码器测速实现
7.1 编码器简介

        1)概念:编码器是一种能将直线位移、角位移数据转换为脉冲信号、二进制编码的设备。它本质上就是一个传感器,可以把角位移或直线位移转换成电信号,并反馈给控制器,使控制器知道当前机械运动的位置、角度等信息。编码器的实物图如图7.1.1所示:
image002.jpg
图7.1.1 编码器实物

2)特点:精度高、结构简单、体积小、使用可靠、性价比高,等等。
        3)应用场景:数控机床、机器人、雷达、光电经纬仪、地面指挥仪、高精度闭环调速系统、伺服系统等诸多领域。
        4)编码器的分类:编码器的分类方式有很多,我们这里列举两种分类方式:按检测原理和编码类型,详见下图:
image004.jpg
图7.1.2 编码器分类

从图7.1.2中可以看到,编码器按照检测原理可以分为光电式和磁电式;按照编码类型可分为增量式和绝对式。在实际的应用中,这四类编码器并不是相对独立的,它们经过组合后,就变成了光电绝对式、光电增量式、磁电绝对式和磁电增量式这四种编码器。
5)编码器原理:我们这里主要介绍磁电增量式、光电增量式以及光电绝对式这3种常用编码器的工作原理。
        ① 磁电增量式
        原理:利用霍尔效应,将位移转换成计数脉冲,用脉冲个数计算位移和速度。
        磁电增量式编码器的具体工作原理如图7.1.3所示:
image006.jpg
图7.1.3 磁电增量式编码器工作原理

        图7.1.3中,磁电增量式编码器的结构包含:磁盘、霍尔传感器以及信号转换电路3个部分,其中,磁盘是由一个个交替排布的S极和N极磁极组成;霍尔传感器可以把磁场的变化转换成电信号的变化,它通常有A、B两相(有的还有Z相),这两相的安装位置形成一定的夹角,这使得输出的A、B两相信号有90°的相位差;信号转换电路可以把电信号转换成脉冲信号。
        在实际应用中,磁盘会装在电机的转轴上,它会随着电机的转轴旋转,而磁盘上面的S极和N极就会交替地经过霍尔传感器的A、B两相,霍尔传感器就可以把磁盘上的磁场变化转换为电信号的变化,输入到信号转换电路中,经过信号的转换之后,我们就可以得到A、B两相脉冲信号了。从上图中可以看到,A、B两相脉冲信号存在90°的相位差,而磁盘的正反转方向就决定了是A相信号在前还是B相在前。
② 光电增量式
        原理:利用光电系统,将位移转换成计数脉冲,用脉冲个数计算位移和速度。
        光电增量式编码器的具体工作原理如图7.1.4所示:
image008.jpg
图7.1.4 光电增量式编码器工作原理

图7.1.4中,光电增量式编码器的结构包含:光电码盘、光源、透镜、受光元件以及信号转换电路5个部分,其中,光电码盘上有一个个均匀排布的透光孔;光源和透镜形成一个聚光系统;受光元件可以把光线的变化转换成电信号的变化,它通常有A、B两相(有的还有Z相),这两相的安装位置形成一定的夹角,这使得输出的A、B两相信号有90°的相位差;信号转换电路可以把电信号转换成脉冲信号。
        在实际应用中,光电码盘会装在电机的转轴上,它会随着电机的转轴旋转,而码盘上面的透光孔会间歇性地经过A、B两相,受光元件就可以把光线变化转换为电信号的变化,输入到信号转换电路中,经过信号的转换之后,我们就可以得到A、B两相脉冲信号了。从上图中可以看到,A、B两相脉冲信号存在90°的相位差,而码盘的正反转方向就决定了是A相信号在前还是B相在前。
        ③ 光电绝对式
原理:当码盘处于不同位置(角度)时,光敏元件根据受光与否转换出相应的电平信号,最后转换成二进制数输出。
        光电绝对式编码器的结构总体来说和光电增量式很类似,都是由光电码盘、光源、透镜、受光元件以及信号转换电路5个部分,但是它们码盘结构和输出信号含义不同。光电绝对式编码器的自然二进制码盘如图7.1.5所示:
image010.png
图7.1.5 自然二进制码盘

        图7.1.5中,光电绝对式编码器的二进制码盘上有很多圈线槽,我们称为码道。上图中的二进制码盘有4个码道,按照自然二进制的方式排列,这个码盘一共被分为2^4 = 16个区域,这些区域中,黑块不透光,代表1;白块透光,代表0。当码盘随着电机转轴旋转,光线会照射到不同的区域,受光元件就能感受到不同的光线情况,最后经过信号的处理,就可以直接输出该区域对应的二进制码了,而我们通过这个二进制码即可得出码盘(电机转轴)的当前位置(角度)。大家需要注意:二进制码盘的每一个位置对应一个确定的二进制码,因此这一类编码器常被应用于位置以及角度测量。
上述的自然二进制码盘读数很方便直观,但是它在实际应用中容易造成读数偏差很大,例如:当码盘停止旋转时,光线照射在0000和1111这两个相邻的区域之间,此时输出的二进制数可能是0000~1111中的任何一个,此时的读数和码盘的实际位置可能就相差很远了。为了避免读数和实际位置出现巨大偏差,我们可以改进一下二进制码的排列方式,使用格雷码形式,如图7.1.6所示:
   image012.png
图7.1.6 格雷码码盘

        图7.1.6中,格雷码盘有4个码道,同样的也能表示16个二进制数,但是任意相邻的两个区域之间的二进制码只有一位不同。当我们采用格雷码盘时,如果码盘停止旋转,光线照射到码盘相邻两个区域之间,其最终输出的二进制数最多只会相差一位,此时位置的偏差范围就很小了。
        6)编码器基本参数:
        ① 分辨率:编码器每个计数单位之间产生的距离,它是编码器可以测量到的最小的距离。对于增量式编码器,分辨率表示为编码器的转轴每旋转一圈所输出的脉冲数(PPR),也称为多少线,直流有刷电机教程中所使用的编码器是11线的。
        ② 精度:编码器分辨率和精度是两个独立的概念,精度是指编码器输出的信号数据与实际位置之间的误差,常用角分′、角秒″表示。
        ③ 最大响应频率:编码器每秒能输出的最大脉冲数,单位Hz,也称为PPS。
        ④ 最大转速:指编码器机械系统所能承受的最高转速。
7.2 编码器测速原理
本章节中我们所使用的编码器是磁电增量式编码器,它安装在直流有刷电机的尾部,实物图如图7.2.1所示:
image014.jpg
图7.2.1 直流有刷电机编码器实物

        图7.2.1中,直流有刷电机的编码器有A、B两相,它们会输出两个相位差为90°的脉冲。当电机正转时,A相脉冲在前;当电机反转时,则是B相脉冲在前。
        有了A、B两相脉冲信号之后,我们应该如何去处理这些信号,把它们转换成电机的转速呢?这里就涉及到一个非常重要的功能:定时器编码器接口模式。
        STM32定时器的编码器接口模式就相当于带有方向选择的外部时钟,也就是说,在此模式下,外部输入的脉冲信号可以作为计数器的时钟,而计数的方向则是由脉冲相位的先后所决定的。定时器编码器接口模式的原理如图7.2.2所示:
image016.jpg
图7.2.2 定时器编码器接口原理

        图7.2.2中,当电机(编码器)正转时,输出两相脉冲信号,A相脉冲在前,此时编码器接口把脉冲信号作为计数器的脉冲,计数方式为递增计数;当电机(编码器)反转时,计数方式就变成了递减计数。
        上述的内容只是对编码器接口的原理进行简单的介绍,让大家有一个感性的认识,接下来我们深入研究一下编码器接口的原理:
1)编码器接口框图
我们首先看编码器的接口框图部分,了解一下脉冲信号进入编码器接口的途径,这里以通用定时器为例,具体框图如图7.2.3所示:
image018.jpg
图7.2.3 通用定时器编码器接口框图

        图7.2.3中,A、B两相脉冲信号从TIMx_CH1和TIMx_CH2这两个通道输入,经过滤波器和边沿检测器(可以设置滤波和反相)的处理,进入到编码器接口控制器中。大家需要注意,TIMx_CH3和TIMx_CH4是不支持编码器接口模式的。
        2)编码器接口计数原理
        编码器接口可以利用输入脉冲的边沿进行计数,我们通过计数值的变化量,就可以算出输入脉冲信号的变化量,也就可以进一步计算出电机的转速了。接下来我们看一下编码器接口是如何根据脉冲边沿计数的,它的计数方向与输入脉冲信号的关系如表7.2.1所示:
lQLPJxaVaJ4xa4HMzs0CN7DOyqh4EGSHRwL124JPgEIA_567_206.png
表7.2.1 计数方向与输入脉冲信号的关系

        表7.2.1来自于《STM32F4xx参考手册_V4(中文版).pdf》中的表75(第415页),上表中的有效边沿我们可以通过代码去配置,一共有3种边沿检测方式,其中仅在TI1处计数表示只检测TI1上的脉冲边沿变化,这时候的计数方向需要结合TI2FP2上的电平情况来确定,其他的两种边沿检测方式原理也是一样的,下面我们以一个实例来理解这个表格的内容:
        假设我们把A相接在CH1(TI1),B相接在CH2(TI2),选择仅在TI1处计数(仅检测A相边沿)。此时编码器接口计数方向和输入脉冲信号的关系如下表:
lQLPJxaVaKe605VfzQIBsFWZNA_Q02m9AvXbke9AogA_513_95.png
表7.2.2 仅在A相边沿计数

编码器输出的A、B两相脉冲信号如图7.2.4所示:
image020.jpg
图7.2.4 A、B相脉冲信号

        图7.2.4中,A、B两相输出的脉冲信号有两种情况:当编码器正转,A相在前;当编码器反转,B相在前,我们选择仅在TI1处计数,也就是只检测A相的边沿。接下来我们分别介绍这两种情况下的计数方向:
        正转:当A相上升沿到来时(图中①处),我们需要关注B相的电平高低,从图7.2.4中可看到B相此时是低电平,结合表7.2.2,可以得知此时计数方向为递增计数;当A相下降沿到来时(图中②处),从图中可以看到B相此时是高电平,结合表7.2.2,可以得知此时计数方向为递增计数;当A相上升沿再次到来时(图中③处),同理可得此时计数方向为递增计数。综上所得,我们可以知道此时编码器正转对应的计数方向就是递增计数。
反转:当A相上升沿到来时(图中④处),我们需要关注B相的电平高低,从图7.2.4中可看到B相此时是高电平,结合表7.2.2,可以得知此时计数方向为递减计数;当A相下降沿到来时(图中⑤处),从图中可以看到B相此时是低电平,结合表7.2.2,可以得知此时计数方向为递减计数;当A相上升沿再次到来时(图中⑥处),同理可得此时计数方向为递减计数。综上所得,我们可以知道此时编码器反转对应的计数方向就是递减计数。
上述的就是仅在TI1处计数的实例分析,其他两种边沿计数方式的原理是一样的,大家可以举一反三,我们这里就不再展开分析。
注意:1、选择仅在TI1或者TI2处计数,就相当于对脉冲信号进行了2倍频(两个边沿),此时如果编码器输出10个脉冲信号,那么就会计数20次。2、选择的是在TI1和TI2处均计数,就相当于对脉冲信号进行了4倍频,此时如果编码器输出10个脉冲信号,那么就会计数40次。因此,我们通过计数次数来计算电机速度的时候,需要除以相应的倍频系数。
至此,A、B两相脉冲信号的变化就转换成了定时器的计数变化。接下来我们就可以通过一分钟内计数的变化量来计算电机的速度,具体公式如下:
电机转速 = 一分钟内计数变化量 / 倍频系数 / 编码器线数 / 减速比
        到这里,编码器的测速原理就介绍完了,接下来我们开始实现直流有刷电机的测速实验。
7.3 编码器测速实现
本小节我们来学习使用直流有刷电机的编码器测速功能,关于编码器测速的原理大家可以回顾上一小节的内容。我们这里选择通用定时器的编码器接口来实现测速功能,而高级定时器则用于电机的基础驱动。
7.3.1 TIM2/TIM3/TIM4/TIM5寄存器
        下面介绍TIM2/TIM3/TIM4/TIM5的几个与编码器测速相关且重要的寄存器,相关内容可以参考《STM32F4xx参考手册_V4(中文版).pdf》定时器相关章节。
        控制寄存器 1(TIMx_CR1)
TIM2/TIM3/TIM4/TIM5的控制寄存器1描述如图7.3.1.1所示:
image022.jpg
图7.3.1.1 TIMx_CR1寄存器

上图中我们只列出了本实验需要用的位:CEN位,此位用于使能计数器的工作,必须要设置该位为1,才可以开始计数;DIR位,在编码器模式下可通过读取该位来判断计数方向。
        捕获/比较模式寄存器1(TIMx_CCMR1)
该寄存器在编码器接口模式中作为输入功能,其描述如图7.3.1.2所示:
image024.jpg
图7.3.1.2 TIMx_CCMR1寄存器

大家需要注意:TIMx_CCMR1寄存器对应于通道1和通道2的设置,CCMR2寄存器对应通道3和通道4,而通道3和通道4是不支持编码器接口模式的,所以这里只用到TIMx_CCMR1。
上图中的CC1S[1:0]这两个位用于CCR1的通道配置,这里我们设置IC1S[1:0]=01,也就是配置IC1映射在TI1上。
输入捕获1预分频器IC1PSC[1:0],这个比较好理解。我们是1次边沿就触发1次计数,所以选择00就是了。
输入捕获1滤波器IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中,fCK_INT是定时器时钟源频率,按照例程的配置为84Mhz,而fDTS则是根据TIMx_CR1的CKD[1:0]的设置来确定的,如果CKD[1:0]设置为00,那么fDTS=fCK_INT。N值采样次数,举个简单的例子:假设IC1F[3:0]=0011,并设置IC1映射到TI1上。表示以fCK_INT为采样频率,当连续8次都是采样到TI1为高电平或者低电平,滤波器才输出一个有效输出边沿。当8次采样中有高有低,那就保持原来的输出,这样可以滤除高频干扰信号,从而达到滤波的效果。
        捕获/比较使能寄存器(TIMx_CCER)
TIM2/TIM3/TIM4/TIM5的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER寄存器描述如图7.3.1.3所示:
image026.jpg
图7.3.1.3 TIMx_CCER寄存器

我们要用到这个寄存器的4个位,分别是CC1E、CC1P、CC2E和CC2P位,上图中只列出了CCIE和CCIP位,其他两个位同理。要使能编码器接口模式,必须设置CC1E和CC2E为1,而CC1P和CC2P设置的是边沿触发的方向。
        从模式控制寄存器(TIMx_SMCR)
TIM2/TIM3/TIM4/TIM5的从模式控制寄存器描述如图7.3.1.4所示:
   image028.jpg
图7.3.1.4 TIMx_SMCR寄存器

        该寄存器的SMS[2:0]位,用于从模式选择,其实就是选择计数器输入时钟的来源。在编码器模式中,如果需要仅在 TI1边沿处计数,则设置SMS[2:0]=010;如果需要仅在 TI2边沿处计数,则设置SMS[2:0]=001;如果需要在 TI1、TI2边沿处都计数,则设置SMS[2:0]=011。本实验我们在TI1和TI2边沿处均计数,也就是对脉冲信号进行4倍频。
7.3.2 硬件设计
1. 例程功能
1、本实验以电机开发板的直流有刷电机驱动接口1为例,基于电压温度电流采集实验,加入编码器测速功能,利用TIM3_CH1(由PC6复用)和TIM3_CH2(由PC7复用)作为编码器脉冲信号输入接口。
2、当按键0按下,就增大PWM的比较值变量,电机将加速,当按键1按下,就减小PWM的比较值变量,电机将减速,当比较值变量为正数时电机正转,反之电机反转,按下按键2则马上停止电机。
3、定时器6中断里面计算电机速度。
4、屏幕显示按键功能信息以及电机转速。
5、串口打印驱动板电压、电流、温度和转速信息。
6、LED0闪烁指示程序运行。
2. 硬件资源
1)LED灯
LED0 – PE0
2)独立按键
KEY0 – PE2
KEY1 – PE3
KEY2 – PE4
3)定时器1、3、6
TIM1正常输出通道 PA8
    TIM1互补输出通道 PB13
        TIM3 编码器A相输入通道 PC6
TIM3 编码器B相输入通道 PC7
4)SD(刹车)信号输出 PF10
5)ADC
        ADC1通道9  PB1(电压)
ADC1通道0  PA0(温度)
ADC1通道8  PB0(电流)
6)串口1
        USART1_TX  PB6(发送)
    USART1_RX  PB7(接收)
3. 原理图
image030.jpg
图7.3.2.1 直流有刷电机接口原理图

        图7.3.2.1就是我们DMF407电机开发板的直流有刷电机接口1原理图,本实验我们除了用到基础驱动以及电压温度电流采集所需的引脚,还需要用到PM1_ENCA(PC6)、PM1_ENCB(PC7)这两个引脚,它们分别用于连接编码器的A、B两相。
本实验的硬件接线部分和上一章节一模一样,这里不再赘述,大家可以回顾上一章节的实验内容。
7.3.3 程序设计
7.3.3.1. 定时器的HAL库驱动

定时器在HAL库中的驱动代码在前面已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1. HAL_TIM_Encoder_Init函数
定时器的编码器接口初始化函数。其声明如下:
HAL_StatusTypeDef HAL_TIM_Encoder_Init(TIM_HandleTypeDef *htim,                                                                                                               TIM_Encoder_InitTypeDef *sConfig);
        函数描述:
用于初始化编码器接口模式。
        函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量,用于配置定时器基本参数。
形参2是TIM_Encoder_InitTypeDef结构体类型指针变量,用于配置编码器模式参数。
重点了解一下TIM_Encoder_InitTypeDef结构体指针类型,其定义如下:
typedef struct
{
  uint32_t EncoderMode;           /* 编码器模式 */
  uint32_t IC1Polarity;           /* IC1输入信号的极性 */
  uint32_t IC1Selection;          /* 输入通道映射 */
  uint32_t IC1Prescaler;          /* 输入捕获预分频器 */
  uint32_t IC1Filter;             /* 输入捕获滤波器 */
  uint32_t IC2Polarity;           /* IC2输入信号的极性 */
  uint32_t IC2Selection;          /* 输入通道映射 */
  uint32_t IC2Prescaler;          /* 输入捕获预分频器 */
  uint32_t IC2Filter;             /* 输入捕获滤波器 */
} TIM_Encoder_InitTypeDef;
该结构体成员用于配置编码器接口的参数。成员变量EncoderMode用来设置编码器模式,我们可以选择仅在TI1 / TI2处计数或者在TI1、TI2处均计数。本实验中我们选择在TI1、TI2处均计数,也就是4倍频。
成员变量IC1Polarity在编码器模式下用于设置输入信号是否反相,它设定的是 TIMx_CCER 寄存器的 CCxNP 位和 CCxP 位。本实验我们不需要输入信号反相。
成员变量ICSelection用来设置通道映射关系,它设定 TIMx_CCMRx 寄存器的 CCxS[1:0] 位的值。在编码器接口模式下,此成员设置为输入模式即可(其他型号芯片不一定),本实验我们选择TIM_ICSELECTION_DIRECTTI。
成员变量ICPrescaler用来设置输入捕获的时钟分频系数,本实验不需要分频,所以选择TIM_ICPSC_DIV1。
成员变量ICFilter用来设置滤波器长度,可选设置 0x0 至 0x0F。它设定 TIMx_CCMRx 寄存器ICxF[3:0] 位的值。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
2. HAL_TIM_Encoder_Start函数
定时器的编码器接口模式启动函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim,
uint32_t Channel);
        函数描述:
用于启动定时器的编码器接口模式。
        函数形参:
形参1是TIM_HandleTypeDef结构体类型指针变量。
形参2是定时器通道,范围:TIM_CHANNEL_1到TIM_CHANNEL_4。
        函数返回值:
HAL_StatusTypeDef枚举类型的值。
定时器的编码器模式配置步骤
1)开启TIMx和输入通道的GPIO时钟,配置该IO口的复用功能输入
首先开启TIMx的时钟,然后配置GPIO为复用功能。本实验我们默认用到定时器3通道1和通道2,对应IO是PC6,PC7,它们的时钟开启方法如下:
__HAL_RCC_TIM3_CLK_ENABLE();                    /* 使能定时器3 */
__HAL_RCC_GPIOC_CLK_ENABLE();                   /* 开启GPIOC时钟 */
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
2)初始化TIMx,设置TIMx的ARR和PSC等参数
使用定时器的输入捕获功能时,通过HAL_TIM_Encoder_Init函数来初始化定时器ARR和PSC等参数。
注意:该函数会调用:HAL_TIM_Encoder_MspInit函数,我们可以通过后者存放定时器和GPIO时钟使能、GPIO初始化、中断使能以及优先级设置等代码。
3)设置TIMx_CHy的编码器模式,开启编码器通道
在HAL库中,定时器的编码器接口模式是通过HAL_TIM_Encoder_Init函数来设置定时器的编码器通道,包括映射关系,输入滤波和输入分频等。
4)使能定时器更新中断,配置定时器中断优先级
通过__HAL_TIM_ENABLE_IT函数使能定时器更新中断。
通过HAL_NVIC_EnableIRQ函数使能定时器中断。
通过HAL_NVIC_SetPriority函数设置中断优先级。
编码器在电机运行时会一直旋转并输出脉冲信号,如果时间较长,那么定时器计数就会溢出,我们必须对溢出必须做处理,否则电机速度的计算结果就不准了。
5)编写中断服务函数
定时器3中断服务函数为:TIM3_IRQHandler,当发生中断的时候,程序就会执行中断服务函数。HAL库为了使用方便,提供了一个定时器中断通用处理函数HAL_TIM_IRQHandler,该函数会调用一些定时器相关的回调函数,用于给用户处理定时器中断到了之后,需要处理的程序。本实验我们用到更新(溢出)中断回调函数HAL_TIM_PeriodElapsedCallback。详见本例程源码。
7.3.3.2 程序流程图
image032.png
图7.3.3.2.1 编码器测速程序流程图

7.3.3.3 程序解析
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。关于基础驱动和电压温度电流采集的代码我们不再赘述,大家可以回顾相应的章节。
注意:本实验需要用到TIM3的编码器模式和TIM6的更新中断,相关的定时器驱动源码包括两个文件:dcmotor_tim.c和dcmotor_tim.h。
首先看dcmotor_tim.h头文件的几个宏定义:
/* 通用定时器 定义 */
#define GTIM_TIMX_ENCODER_CH1_GPIO_PORT         GPIOC
#define GTIM_TIMX_ENCODER_CH1_GPIO_PIN          GPIO_PIN_6
#define GTIM_TIMX_ENCODER_CH1_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)  /* PC口时钟使能 */

#define GTIM_TIMX_ENCODER_CH2_GPIO_PORT         GPIOC
#define GTIM_TIMX_ENCODER_CH2_GPIO_PIN          GPIO_PIN_7
#define GTIM_TIMX_ENCODER_CH2_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)  /* PC口时钟使能 */

/* TIMX 引脚复用设置
* 因为PC6/PC7, 默认并不是TIM3的功能脚, 必须开启复用, 才可以用作TIM3的CH1/CH2功能
*/
#define GTIM_TIMX_ENCODERCH1_GPIO_AF           GPIO_AF2_TIM3
#define GTIM_TIMX_ENCODERCH2_GPIO_AF           GPIO_AF2_TIM3

#define GTIM_TIMX_ENCODER                        TIM3
#define GTIM_TIMX_ENCODER_INT_IRQn             TIM3_IRQn
#define GTIM_TIMX_ENCODER_INT_IRQHandler      TIM3_IRQHandler

#define GTIM_TIMX_ENCODER_CH1                   TIM_CHANNEL_1 /* 通道1 */
#define GTIM_TIMX_ENCODER_CH1_CLK_ENABLE()      
do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0)          /* TIM3 时钟使能 */

#define GTIM_TIMX_ENCODER_CH2                   TIM_CHANNEL_2 /* 通道2 */
#define GTIM_TIMX_ENCODER_CH2_CLK_ENABLE()      
do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0)          /* TIM3 时钟使能 */

/* 基本定时器 定义
* 捕获编码器值,用于计算速度
*/
#define BTIM_TIMX_INT                             TIM6
#define BTIM_TIMX_INT_IRQn                       TIM6_DAC_IRQn
#define BTIM_TIMX_INT_IRQHandler                           TIM6_DAC_IRQHandler
#define BTIM_TIMX_INT_CLK_ENABLE()              
do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0)          /* TIM6 时钟使能 */

/* 编码器参数结构体 */
typedef struct
{
  int encode_old;                          /* 上一次计数值 */
  int encode_now;                          /* 当前计数值 */
  float speed;                             /* 编码器速度 */
} ENCODE_TypeDef;

extern ENCODE_TypeDef g_encode;    /* 编码器参数变量 */
        可以把上面的宏定义分成四部分:
第一部分是通用定时器3编码器模式输入通道对应的IO口宏定义;
第二部分是通用定时器3编码器模式输入通道的相应宏定义;
第三部分是定时器6更新中断的相应宏定义;
第四部分是编码器参数的结构体,这个结构体用于管理编码器的计数值和速度。
下面看dcmotor_tim.c的程序,首先是通用定时器的编码器接口模式初始化函数。
/**
* @brief       通用定时器TIMX 通道Y 编码器接口模式 初始化函数
* @note
*               通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
*               通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz
*               定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
*               Ft=定时器工作频率,单位:Mhz
*
* @param       arr: 自动重装值。
* @param       psc: 时钟预分频数
* @retval      无
*/
void gtim_timx_encoder_chy_init(uint16_t arr, uint16_t psc)
{        
    g_timx_encode_chy_handle.Instance = GTIM_TIMX_ENCODER;           /* 定时器x */
    g_timx_encode_chy_handle.Init.Prescaler = psc;                    /* 定时器分频 */
    g_timx_encode_chy_handle.Init.Period = arr;                         /* 自动重装载值 */
g_timx_encode_chy_handle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;  
/* TI1、TI2都检测,4倍频 */
g_timx_encoder_chy_handle.EncoderMode = TIM_ENCODERMODE_TI12;     
/* 输入极性,非反向 */  
g_timx_encoder_chy_handle.IC1Polarity = TIM_ICPOLARITY_RISING;     
/* 输入通道选择 */
g_timx_encoder_chy_handle.IC1Selection = TIM_ICSELECTION_DIRECTTI;  
    g_timx_encoder_chy_handle.IC1Prescaler = TIM_ICPSC_DIV1;         /* 不分频 */
    g_timx_encoder_chy_handle.IC1Filter = 10;                             /* 滤波器设置 */

/* 输入极性,非反向 */
g_timx_encoder_chy_handle.IC2Polarity = TIM_ICPOLARITY_RISING;
/* 输入通道选择 */
g_timx_encoder_chy_handle.IC2Selection = TIM_ICSELECTION_DIRECTTI;  
    g_timx_encoder_chy_handle.IC2Prescaler = TIM_ICPSC_DIV1;          /* 一分频 */
    g_timx_encoder_chy_handle.IC2Filter = 10;                         /* 滤波器设置 */
    HAL_TIM_Encoder_Init(&g_timx_encode_chy_handle,
&g_timx_encoder_chy_handle); /* 初始化定时器x编码器 */
    /* 使能编码器通道1、2 */
    HAL_TIM_Encoder_Start(&g_timx_encode_chy_handle,GTIM_TIMX_ENCODER_CH1);
    HAL_TIM_Encoder_Start(&g_timx_encode_chy_handle,GTIM_TIMX_ENCODER_CH2);
        /* 使能更新中断、清除中断标志位 */
    __HAL_TIM_ENABLE_IT(&g_timx_encode_chy_handle,TIM_IT_UPDATE);
    __HAL_TIM_CLEAR_FLAG(&g_timx_encode_chy_handle,TIM_IT_UPDATE);
}
        HAL_TIM_Encoder_Init初始化定时器3的基础工作参数和编码器接口,如:ARR、PSC、编码器模式、滤波器等,第二部分则是调用HAL_TIM_Encoder_Start函数开启编码器通道。最后是使能更新中断和清除中断标志位。通道对应的IO、时钟开启和NVIC的初始化都在HAL_TIM_Encoder_MspInit函数里编写,其定义如下:
/**
* @brief           定时器底层驱动,时钟使能,引脚配置
                 此函数会被HAL_TIM_Encoder_Init()调用
* @param           htim:定时器句柄
* @retval           无
*/
void HAL_TIM_Encoder_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == GTIM_TIMX_ENCODER)
    {
        GPIO_InitTypeDef gpio_init_struct;
        GTIM_TIMX_ENCODER_CH1_GPIO_CLK_ENABLE();                        /* 开启IO时钟 */
        GTIM_TIMX_ENCODER_CH2_GPIO_CLK_ENABLE();
        GTIM_TIMX_ENCODER_CH1_CLK_ENABLE();                                         /*开启定时器时钟*/
        GTIM_TIMX_ENCODER_CH2_CLK_ENABLE();

        gpio_init_struct.Pin = GTIM_TIMX_ENCODER_CH1_GPIO_PIN;        /* 通道1的IO */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                           /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_NOPULL;                           /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;               /* 高速 */
        gpio_init_struct.Alternate = GTIM_TIMX_ENCODERCH1_GPIO_AF; /* 端口复用 */
        HAL_GPIO_Init(GTIM_TIMX_ENCODER_CH1_GPIO_PORT, &gpio_init_struct);  
        
        gpio_init_struct.Pin = GTIM_TIMX_ENCODER_CH2_GPIO_PIN;        /* 通道2的IO */
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;                         /* 复用推挽输出 */
        gpio_init_struct.Pull = GPIO_NOPULL;                             /* 上拉 */
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;                 /* 高速 */
        gpio_init_struct.Alternate = GTIM_TIMX_ENCODERCH2_GPIO_AF;
        HAL_GPIO_Init(GTIM_TIMX_ENCODER_CH2_GPIO_PORT, &gpio_init_struct);
      
        HAL_NVIC_SetPriority(GTIM_TIMX_INT_IRQn, 2, 0);         /* 优先级设置 */
        HAL_NVIC_EnableIRQ(GTIM_TIMX_INT_IRQn);                                        /* 开启中断 */
    }
}
该函数调用HAL_GPIO_Init函数初始化定时器输入通道对应的IO,并且开启GPIO时钟,使能定时器。其中要注意IO口复用功能的选择一定要选对了。最后配置中断抢占优先级和响应优先级,以及打开定时器中断。
接着我们看基本定时器6的中断初始化函数及其底层初始化函数。
/**
* @brief      基本定时器TIMX定时中断初始化函数
* @note
*              基本定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候
*              基本定时器的时钟为APB1时钟的2倍, 而APB1为42M, 所以定时器时钟 = 84Mhz
*              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
*              Ft=定时器工作频率,单位:Mhz
*
* @param      arr: 自动重装值。
* @param      psc: 时钟预分频数
* @retval     无
*/
void btim_timx_int_init(uint16_t arr, uint16_t psc)
{
    timx_handler.Instance = BTIM_TIMX_INT;                              /* 基本定时器X */
    timx_handler.Init.Prescaler = psc;                             /* 设置预分频器  */
    timx_handler.Init.CounterMode = TIM_COUNTERMODE_UP;           /* 向上计数器 */
    timx_handler.Init.Period = arr;                                 /* 自动装载值 */
    timx_handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;  
    HAL_TIM_Base_Init(&timx_handler);
   
    HAL_TIM_Base_Start_IT(&timx_handler);            /* 使能基本定时器x及其更新中断 */
    __HAL_TIM_CLEAR_IT(&timx_handler,TIM_IT_UPDATE);             /* 清除中断标志位 */
}

/**
* @brief       定时器底册驱动,开启时钟,设置中断优先级
                  此函数会被HAL_TIM_Base_Init()函数调用
* @param       无
* @retval      无
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == BTIM_TIMX_INT)
    {
        BTIM_TIMX_INT_CLK_ENABLE();                                     /*使能TIM时钟*/
        HAL_NVIC_SetPriority(BTIM_TIMX_INT_IRQn, 1, 3);
        HAL_NVIC_EnableIRQ(BTIM_TIMX_INT_IRQn);                         /*开启ITM3中断*/
    }
}
        这一部分函数和基本定时器中断实验是一样的,这里不再赘述,我们使用定时器6来定时计算电机速度,本实验中配置溢出时间为1ms,也就是1ms进入一次定时器6的更新中断。
下面开始看中断服务函数的逻辑程序,HAL_TIM_IRQHandler函数会调用下面的回调函数,我们的逻辑代码就是放在回调函数里,函数定义如下:
/**
* @brief       定时器更新中断回调函数
* @param       htim:定时器句柄指针
* @note        此函数会被定时器中断函数共同调用的
* @retval      无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM3)
    {
            /* 判断CR1的DIR位 */
        if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&g_timx_encode_chy_handle))
        {
            g_timx_encode_count--;                         /* DIR位为1,也就是递减计数 */
        }
        else
        {
            g_timx_encode_count++;                        /* DIR位为0,也就是递增计数 */
        }
    }
    else if (htim->Instance == TIM6)
    {
        int Encode_now = gtim_get_encode(); /* 获取编码器值,用于计算速度 */
        /* 中位平均值滤除编码器抖动数据,50ms计算一次速度 */
        speed_computer(Encode_now, 50);
    }
}
        进入更新中断回调函数后,先判断是不是定时器3的寄存器基地址,如果是则读取CR1寄存器的DIR位,判断计数溢出的方向,如果DIR位为1,就是递减计数,说明是向下溢出,我们让变量g_timx_encode_count自减1;如果DIR位为0,就是递增计数,说明是向上溢出,我们让变量g_timx_encode_count自增1,变量g_timx_encode_count将用于计算总的计数值。
如果是定时器6的寄存器基地址,就先获取编码器的计数总值,然后每隔50ms计算一次电机速度,其中所涉及到的两个函数我们后续再介绍。
        接下来看编码器计数总值的计算函数。
/**
* @brief       获取编码器的值
* @param       无
* @retval      编码器值
*/
int gtim_get_encode(void)
{
        /* 当前计数值+之前累计编码器的值=总的编码器值 */
return (int32_t)__HAL_TIM_GET_COUNTER(&g_timx_encode_chy_handle) +
g_timx_encode_count * 65536;   
}
代码中通过调用__HAL_TIM_GET_COUNTER函数来获取当前的计数值,然后进行编码器计数总值的计算,这里需要用到变量g_timx_encode_count,具体的计算公式如下:
计数总值 = 当前计数值 + g_timx_encode_count * 65536
获取到计数总值之后,我们就可以计算电机的速度了,本实验中电机速度计算相关源码包括两个文件:dc_motor.c和dc_motor.h。
首先看dc_motor.h头文件的几个宏定义:
#define ROTO_RATIO       44  /* 线数*倍频系数,即11*4=44 */
#define REDUCTION_RATIO         30 /* 减速比30:1 */

/* 电机参数结构体 */
typedef struct
{
  uint8_t state;                  /*电机状态*/
  float current;                  /*电机电流*/
  float volatage;                 /*电机电压*/
  float power;                    /*电机功率*/
  float speed;                    /*电机实际速度*/
  int32_t motor_pwm;              /*设置比较值大小 */
} Motor_TypeDef;

extern Motor_TypeDef  g_motor_data;  /*电机参数变量*/
        ROTO_RATIO和REDUCTION_RATIO是用于速度计算的系数,ROTO_RATIO等于编码器的线数*倍频系数,教程中采用的直流有刷电机编码器线数为11,也就是说编码器每旋转一圈就输出11个脉冲信号;REDUCTION_RATIO代表电机的减速比,我们算出编码器的速度之后,就可以根据减速比计算电机的速度了。Motor_TypeDef这个结构体是用于管理电机的参数的,我们本实验中只需要用到speed(实际速度)这个成员。
下面看dcmotor_tim.c的程序,我们只看电机速度计算函数。
/**
* @brief            电机速度计算
* @param             encode_now:当前编码器总的计数值
*                ms:计算速度的间隔,中断1ms进入一次,例如ms = 5即5ms计算一次速度
* @retval      无
*/
void speed_computer(int32_t encode_now, uint8_t ms)
{
    uint8_t i = 0, j = 0;
    float temp = 0.0;
    static uint8_t sp_count = 0, k = 0;
    static float speed_arr[10] = {0.0};         /* 存储速度进行滤波运算 */

    if (sp_count == ms)                                             /* 计算一次速度 */
    {
        /* 计算电机转速
           第一步 :计算ms毫秒内计数变化量
           第二步 ;计算1min内计数变化量:g_encode.speed * ((1000 / ms) * 60 ,
           第三步 :除以编码器旋转一圈的计数次数(倍频倍数 * 编码器分辨率)
           第四步 :除以减速比即可得出电机转速
        */
        g_encode.encode_now = encode_now;        /* 取出编码器当前计数值 */
           /* 计算编码器计数值的变化量 */
        g_encode.speed = (g_encode.encode_now - g_encode.encode_old);   
/* 保存一次电机转速 */
        speed_arr[k++] = (float)(g_encode.speed * ((1000 / ms) * 60.0) /
(uint16_t)(REDUCTION_RATIO * ROTO_RATIO));
           /* 保存当前编码器的值 */
        g_encode.encode_old = g_encode.encode_now;

        if (k >= 10)                                            /* 累计10次速度值,后续进行滤波*/
        {
            for (i = 10; i >= 1; i--)         /* 冒泡排序*/
            {
                for (j = 0; j < (i - 1); j++)
                {
                    if (speed_arr[j] > speed_arr[j + 1])        /* 数值比较 */
                    {
                        temp = speed_arr[j];                        /* 数值换位 */
                        speed_arr[j] = speed_arr[j + 1];
                        speed_arr[j + 1] = temp;
                    }
                }
            }
            temp = 0.0;
            for (i = 2; i < 8; i++)             /* 去除两边高低数据 */
            {
                temp += speed_arr;           /* 将中间数值累加 */
}
            temp = (float)(temp / 6);        /* 求速度平均值 */
            /* 一阶低通滤波
             * 公式为:Y(n)= qX(n) + (1-q)Y(n-1)
             * 其中X(n)为本次采样值;Y(n-1)为上次滤波输出值;Y(n)为本次滤波输出值,
q为滤波系数
             * q值越小则上一次输出对本次输出影响越大,整体曲线越平稳,
但是对于速度变化的响应也会越慢
             */
            g_motor_data.speed = (float)((g_motor_data.speed * (float)0.52) +
((float)0.48 * temp));
            k = 0;
        }
        sp_count = 0;
    }
    sp_count ++;
}
        电机的速度计算步骤如下:
        第一步,计算50毫秒内计数变化量;
    第二步,计算1min内计数变化量:g_encode.speed * ((1000 / ms) * 60;
    第三步,除以编码器旋转一圈的计数次数(REDUCTION_RATIO);
    第四步,除以减速比(ROTO_RATIO)即可得出电机转速;
我们累计计算10次电机速度,然后进行冒泡排序,把10次电机速度值从小到大排序,接着将中间的6次速度值累加求平均值,最后再进行一阶低通滤波。由于冒泡排序和一阶低通滤波的详细介绍篇幅过长,我们这里不做展开,大家感兴趣的话可以去网上搜索相关的内容。
        在main.c里面编写如下代码:
int main(void)
{     
    uint8_t key;
    uint16_t t;
    int32_t motor_pwm = 0;
    uint16_t init_adc_val;   
    HAL_Init();                                             /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7);             /* 设置时钟,168Mhz */
    delay_init(168);                                /* 延时初始化 */
    usart_init(115200);                             /* 串口初始化为115200 */
    led_init();                                             /* 初始化LED */
    lcd_init();                                             /* 初始化LCD */
    key_init();                                             /* 初始化按键 */
    atim_timx_cplm_pwm_init(8400 - 1, 0);   /* 168Mhz的计数频率 */
    dcmotor_init();                                                        /* 初始化电机 */
    adc_nch_dma_init();                                                /* ADC DMA传输初始化 */
    gtim_timx_encoder_chy_init(0XFFFF, 0);  /* 不分频直接84M的计数频率 */
    btim_timx_int_init(1000 - 1 , 84 - 1);  /* 基本定时器初始化,1ms计数周期 */

    g_point_color = WHITE;
    g_back_color  = BLACK;
    lcd_show_string(10, 10, 200, 16, 16, "DcMotor Test", g_point_color);
    lcd_show_string(10, 30, 200, 16, 16, "KEY0:Start forward", g_point_color);
    lcd_show_string(10, 50, 200, 16, 16, "KEY1:Start backward", g_point_color);
    lcd_show_string(10, 70, 200, 16, 16, "KEY2:Stop", g_point_color);

    delay_ms(20);

    /* init_adc_val存储电流测量对应的参考电压ADC值,这里进行滤波 */
    init_adc_val = g_adc_val [2];                        /* 取出第一次得到的值 */
    for(t=0;t<1000;t++)
    {
        init_adc_val += g_adc_val [2];                /* 现在的值和上一次存储的值相加 */
        init_adc_val /= 2;                                        /* 取平均值 */
        delay_ms(1);
    }

    while (1)
    {
        key = key_scan(0);                          /* 按键扫描 */
        if(key == KEY0_PRES)                        /* 当key0按下 */
        {
                   /* 因为不同的电机最小启动电压不同,可能在第一次增加的时候电机还不能转起来 */
            motor_pwm += 400;
            if (motor_pwm == 0)
            {
                dcmotor_stop();                     /* 停止则立刻响应 */
                motor_pwm = 0;
            }
            else
            {
                dcmotor_start();                    /* 开启电机 */
                if (motor_pwm >= 8400)              /* 限速 */
                {
                    motor_pwm = 8400;
                }               
            }
            motor_pwm_set(motor_pwm);               /* 设置电机方向、转速 */
        }
        
        else if(key == KEY1_PRES)                     /* 当key1按下 */
        {
            motor_pwm -= 400;
            if (motor_pwm == 0)
            {
                dcmotor_stop();                     /* 停止则立刻响应 */
                motor_pwm = 0;
            }
            else
            {
                dcmotor_start();                    /* 开启电机 */
                if (motor_pwm <= -8400)             /* 限速 */
                {
                    motor_pwm = -8400;
                }               
            }
            motor_pwm_set(motor_pwm);               /* 设置电机方向、转速 */
        }
        
        else if(key == KEY2_PRES)             /* 当key2按下 */
        {
            LED1_TOGGLE();
            dcmotor_stop();                         /* 关闭电机 */
            motor_pwm = 0;
            motor_pwm_set(motor_pwm);               /*设置电机方向、转速 */
        }
        
        delay_ms(10);
        t++;
        if(t % 20 == 0)
        {        
            lcd_dis();                                                /* 显示电机速度信息 */
            LED0_TOGGLE();                              /* LED0(红灯) 翻转 */

            printf("KEY0:增加比较值,KEY1:减小比较值,KEY2:停止电机\r\n\r\n");
            printf("Valtage:%.1fV \r\n",g_adc_val[0]*ADC2VBUS);          /* 打印电压值 */
            printf("Temp:%.1fC \r\n",get_temp(g_adc_val[1]));      /* 打印温度值 */
            printf("Current:%.1fmA \r\n",abs(g_adc_val[2]-
init_adc_val)*ADC2CURT);      /* 打印电流值 */
            printf("电机速度:%.1f RPM\r\n\r\n",g_motor_data.speed);/* 打印速度值 */
        }
    }
}
我们只看本实验新增的内容,先看gtim_timx_encoder_chy_init(0XFFFF, 0)这个语句,这两个形参分别设置自动重载寄存器的值为65535,以及预分频器寄存器的值为0。定时器3是16位的计数器,这里设置为最大值65535。预分频系数,我们设置为不分频,定时器3的计数频率是84MHz。接着看btim_timx_int_init(1000 - 1 , 84 - 1)这个语句,这里设置了定时器6的计数频率为84M/84=1M Hz,计数周期为1ms,也就是1ms进入一次更新中断。最后就是在串口以及屏幕上输出电机速度值了。
7.3.4 下载验证
下载代码后,可以看到LED0在闪烁,说明程序已经正常在跑了,LCD上显示按键功能以及电机速度信息,当我们按下KEY0,比较值变量motor_pwm将增大;按下KEY1,比较值变量motor_pwm将减小;按下KEY2,电机将停止。比较值变量motor_pwm为正数时,电机正转,反之电机反转,其绝对值越大,电机的速度越快。我们再打开串口调试助手,选择对应的串口端口,我这边是COM3,可以看到串口打印的按键信息、电压、温度、电流以及电机速度值,如图7.3.4.1所示:
image034.jpg
图7.3.4.1打印按键信息、电压、温度、电流以及速度值



阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入0汤圆

发表于 2022-8-12 22:14:51 | 显示全部楼层
买了1个开发板,2个驱动板,3个电机还没空玩
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-24 00:31

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表