平台ST
下位机实现功能:
motor,实现控制2轮驱动,光码盘测速,产生4路不同PWM信号,方便调节周期,分频,通道方式 ,定时中断功能,光电编码器实现角度信号测量;
超声波测距,收发过程中实现避障;
实现串口通信,显示MPU6050温度,俯仰角(pitch),横滚角(roll),航向角(yaw)的数值;
I2C读写MPU6050;
Adc规则通道采样超声波控制端发射数据;
平衡运行:PD角度控制,PD转向控制,PD直立控制,PD速度控制,PD方向控制;
MPU6050调用标准库可获取小车角速度,加速度,重力传感,姿态数据;配合上位机可以实现此传感器的姿态曲线;
上位机实现功能:
蓝牙连接:实现80种环境下联网,蓝牙遥控控制小车
APP联网:具有蓝牙开关功能,重力显示x,y,z轴,超声波测距显示,电池电压检测,左右电机速度监视,摇杆控制,重力感应,可调节角度环参数P,D;可调节速度环参数P,I;具有加速度波形显示,陀螺仪波形显示,电池电压波形显示;
上位机通信:实现键盘控制小车方向,左右旋转,电机PID调节,查询当前PID功能,恢复PID功能,波形显示加速度,陀螺仪,电池电压等功能;
微信远程连接(WIFI模块):2.4G网段实现左右前后控制小车,旋转,点灯灭灯,鸣笛
卡尔曼滤波运动稳定性:实现陀螺仪数据与加速度计数据通过滤波融合,融合角度与角速度;
目录
- 多路径小车机器人
- 二、角度(物理分析 PD算法)
- ``F = mg θ-mk1 θ -mk2 θ'``
- 按照上述倒立摆的模型,可得出控制小车车轮加速度的算法:
- `a =k1θ+k2θ'` 式中θ为小车角度,θ`为角速度。k1 k2都是比例系数
- 根据上述内容,建立速度的比例微分负反馈控制,根据基本控制理论讨论小车通过闭环控制保持稳定的条件(这里需要对控制理论有基本了解)。假设外力干扰引起车模产生角加速度x(t)。沿着垂直于车模地盘方向进行受力分析,可以得到车模倾角与车轮运动加速度以及外力干扰加速度a(t)x(t)之间的运动方程。如图3所示。
- `在角度反馈控制中,与角度成正比例的控制量是称为比例控制`
- `与角速度比例的控制量称为微分控制(角速度是角度的微分)。`
- 因此上面的系数k1,k2分别称为比例和微分控制参数。其中微分参数相当于阻尼力,可以有效抑制车模震荡。通过微分抑制控制震荡的思想在后面的速度和方向控制中也同样适用。
- 总结控制车模直立稳定的条件如下:
- (1)`能够精确测量车模倾角θ的大小和角速度θ‘的大小`
- (2)`可以控制车轮的加速度。`
- 上述控制实际结果是小车与地面不是严格垂直,而是存在一个对应的倾角。在重力的作用下,小车朝着一个方向加速前进。为了保持小车的静止或者均速运动需要消除这个安装误差。在实际小车制作过程中需要进行机械调整和软件参数设置。另外需要通过软件中的速度控制来实现速度的稳定性。在小车角度控制中出现的小车倾角偏差,使得小车在倾斜的方向上产生加速。这个结果可以用来进行小车的速度控制。下面利用这个原理来调节小车的速度
- 三,测速(物理模型 建立数学模型 传递函数PD算法)
- 假设小车在上面直立控制调节下已经能够保持平衡了,但是由于安装误差,传感器实际测试的角度与车模角度有偏差,因此小车实际不是保持与地面垂直,而是存在一个倾角。在重力的作用下,小车就会朝倾斜的方向加速前进。控制速度只要通过控制小车的倾角就可以实现了。
- `(1)如何测量小车的速度?`·
- `(2)如何通过小车直立控制实现小车倾角的改变?`
- `(3)如何根据速度误差控制小车倾角?`
- 一,第一个问题,可以通过安装在``**电机输出轴上的光码盘**``来测量得到小车的车轮速度。`利用控制单片机的计数器测量在固定时间间隔内脉冲信号的个数可以反馈电机的转速`。
- 二,第二个问题,可以通过角度控制给定值来解决。给定小车直立控制的设定值,在角度控制调节下,小车将会自动维持在一个角度。通过前面小车直立控制算法可以知道,小车倾角最终是跟踪重力加速度Z轴的角度。因此小车的倾角给定值与重力加速度Z轴角度相减,便可以最终得到小车的倾角。
- 三,第三个问题,析起来相对比较困难,远比直观进行速度负反馈分析复杂。首先对一个简单例子进行分析。假设小车开始保持静止,然后增加给定速度,为此需要小车往前倾斜以便获得加速度。在小车直立控制下,为了能够得到一个往前的倾斜角度,车轮需要往后运动,这样会引起车轮速度下降(因为车轮往负方向运动了)。由于负反馈,使得小车往前倾角需要更大。如此循环,小车很快就会倾倒。原本利用负反馈进行速度控制反而成了“正”反馈。
- `为什么负反馈控制在这儿失灵了呢?`
- 为了保证系统稳定,往往取的小车倾角控制时间常数Tz很大。这样便会引起系统产生两个共轭极点,而且极点的实部变得很小,使得系统的速度控制会产生的震荡现象。这个现象在实际参数整定的时候可以观察到。那么如何消除速度控制过程中的震荡呢?这样便会引起系统产生两个共轭极点,而且极点的实部变得很小,使得系统的速度控制会产生的震荡现象。这个现象在实际参数整定的时候可以观察到。
- `那么如何消除速度控制过程中的震荡呢?`
- 要解决震荡问题,在前面的小车角度控制中已经有了经验,那就是在控制反馈中增加速度微分控制。但由于车轮的速度反馈信号中往往存在着噪音,对速度进行微分运算会进一步加大噪声的影响。为此需要对上面控制方法进行改进。`原系统中倾角调整过程时间常数往往很大,因此可以将该系统近似为一个积分环节。`将原来的`微分环节和这个积分环节合并,形成一个比例控制环节。`这样可以保持系统控制传递函数不变,同时避免微分计算。
- 但在控制反馈中,只是使用反馈信号的比例和微分,没有利用误差积分,所以最终这个速度控制是有残差的控制。但是直接引入误差积分控制环节,会增加系统的负杂度,为此不再增加积分控制,而是通过与角度控制结合在进行改进。
- 要求小车在原地停止,速度为0.但是由于采用的是比例控制,如果此时陀螺仪有漂移,或者加速度传感器安装误差,最终小车倾角不会最终调整为0.小车会朝着倾斜方向恒速运动下去。注意此时车模不会像没有速度控制那样加速运行下去了,但是速度不会最终为0。为了消除这个误差,可以将小车倾角设定量直接积分补偿在角度控制输出中,这样就会彻底消除速度控制误差。第二点,由于加入了速度控制,它可以补偿陀螺仪和重力加速度的漂移和误差。所以此时重力加速度传感器实际没有必要了。
- 此时小车在控制启动的时候,需要保持小车的垂直状态。此时陀螺仪的积分角度也初始化为0。当然如果电路中已经包括了重力加速度传感器,也可以保留这部分,从而提高小车的稳定性。在后面的最终给定的控制方案中,保留了这部分的控制回路。
- 四,转向控制(PD算法)
- 五,全方案整合
- 为了实现小车直立行走,需要采集如下信号:
- (1)小车倾角速度陀螺仪信号,获得小车的倾角和角速度。
- (2)重力加速度信号
- (z轴信号)补偿陀螺仪的漂移,该信号可以省略,有速度控制替代。
- (3)小车电机转速脉冲信号,获得小车运动速度,进行速度控制
- (4)小车转动速度陀螺仪信号,获得小车角速度,进行方向控制
- 在小车控制中的直立,速度和方向控制三个环节中,都使用了比例微分(PD)控制,这三种控制算法的输出量最终通过叠加通过电机运动来完成
- (1)小车直立控制:使用小车倾角的PD(比例,微分)控制;
- (2)小车速度控制:使用PD(比例,微分)控制:
- (3)小车方向控制:使用PD(比例、微分)控制。
- 在上面控制过程中,车模的角度控制和方向控制都是通过直接将输出电压叠加后控制电机的转速实现的。而车模的速度控制本质上是通过调节车模的倾角实现的,由于车模是一个非最小相位系统,因此该反馈控制如果比例和速度过大,很容易形成正反馈,使得车模失控,造成系统的不稳定性。因此速度的调节过程需要非常缓慢和平滑。
- 六,PID算法
- AngleCalculate:小车倾角计算函数。根据采集到的陀螺仪和重力加速度传感器的数值计算小车的角度和角速度。如果这部分的算法由外部一个运放实现,那么采集得到的直接是小车的角度和角速度,这部分算法可以省略。该函数每5ms调用一次。
- AngleControl:小车直立函数。根据小车的角度和角速度计算小车电机的控制量。直立是5ms调用一次。
- SpeedControl:小车速度控制函数。根据小车采集到 的电机转速和速度设定值,计算电机的控制量。该函数每40ms调用一次
- MotorOutput:电机输出量汇集函数。根据直立函数,速度控制和方向控制所得到的控制量进行叠加,分别得到左右两个电极的输出电压控制量。对叠加后的输出量进行饱和处理。函数每5ms调用一次。在此请大家注意速度控制量叠加的极性是负。
- MotorSpeedOut:电机PWM输出计算函数。根据左右两个电极的输出控制量的正负极性,叠加上一个小的死区数值,克服车模机械静态摩擦力。函数调用周期5毫秒。
- SetMotorVoltage:PWM输出函数:根据两个电机的输出量,计算出PWM控制寄存器的数值,设置四个PWM控制寄存器的数值。函数调用周期1毫秒。
- 以上9个函数都是在1毫秒中断服务中进行被相互调用的。下图显示了这些函数之间的调用与参数传递关系。在个函数附近也表明了调用的周期。
- Chaoshengbo:加入超声波壁障模块:根据前方障碍物的距离检测,一旦检测到后,通过直接PWM值输出(g_fchaoshengbooutput),相障碍物反方上运动,无需算法实现。每30毫秒调用一次。
- 七,程序
- (3)蓝牙以及超声波
- /**********************超声波距离计算***************************/
- 提供电路图,按模块进行电路分析
- 铝电池10V转5V,DC-DC转换
- 一,从电路图看Q1为SI 2306此为N mos管,当Vgs>0时则三极管导通,G极为电池VM端为12V,S极为电池负极,开关没开时,Q1 N型mos管的S极为接GND,G极没有电压则为0V,则Vgs=Vg-Vs=0V-0v=0V,此时N管不导通;当电源打开时,Q1的G极为12V,则Vg-Vs=12v-0=12v>0,则N mos管导通接地;`Q1启到保护电池作用,我猜测这个CDS-2P卧式容易短路,如果短路容易造成电池回路然后烧电池;将GND放在Q1mos管下面可以在电源打开时,CDS-2P的GND才会接地;我认为CDS-2P的GND处没有真正的接地,真正接地的是Q1 N型mos管的D极`
- 二,SS34肖特基二极管,防止当N管是开状态时,电池反灌。
- 三,S1我也不知道什么来的,应该是电池保护芯片 ,SW输入,OUT输出,D1灯的作用是为了,灯亮时说明有电通过,灯灭时无电通过。
- 四,芯片78M05是DC-转DC5v的芯片,Q2为Nmos管当Vgs>0时导通,芯片78M05VOUT为5V,则Q2的S极为5V,`Q2的G极为ON应该是电池端(ON不是电池端,这里是ON低压保护ON=5.78V,后面分析)`,则开机时Vgs=Vg-Vs=12v-5v>0,则Q2导通(注:Vgs=5.78v-5v>0不影响分析,后面给出解释),芯片通过Q2输出5V
- 五,D2灯的作用:芯片是否有5V供电
- 低压保护电路
- 一,此电路相当于将+IN与-IN相加得输出V0(个人理解)
- `VKA=(1+ R1/R2) *2.5`
- 还有好多用法不做解释
- 回归电路图
- `ON=5.78V.当电池电压降低,ON电压降低,则Q2 nmos管Vgs=Vg-Vs=5.78v-5v=0.78v>0,Q2导通,当电源电压低到一定程度时,5.78v降低则Q2mos管关闭,可以让DC5v关闭。`完美,就是这样了
- CH340G芯片
- 一,在电路设计原理上,5V 供电时芯片 V3 引脚需要接一个 104 电容到地,3.3V 供电时直接将 V3 脚与 3.3V 电源引脚短接就可以了。这里的是5V供电,所以V3需接个104的电容。V3 的引脚除了在不同电压供电模式下接法不同,对于电容数值选用也是需要注意的。V3 引脚的电容用于内部电源节点退耦,来改善 USB 传输过程中的 EMI,通常容量在 4700pF 到 0.1uF 范围,建议容量为 0.01uF,即 103 电容。这里使用C9为104电容。
- 二,在实际应用中,当 CH340 与其他 IC 譬如 MCU 等器件一同使用时,如果串口直连的双方器件有一方不需要供电工作时,要注意电流倒灌导致未供电的芯片继续工作的情况
- `CH340 芯片的 发送引脚 TXD 上接一个反向二极管D5,然后再连接到对端 IC`;`在接收引脚上加一个限流电阻来防止对端 IC 对CH340 倒灌电。`此电路图没加
- 通过反向二极管的原理是:在 CH340 发送数据时,发送高电平时二极管截止,但是由于对端 RXD 默认上拉也是高电平不会有采样问题,而发送低电平时二极管导通,对端 RXD 接收到低电平,因此可以正常通讯。并防止了 CH340 的 TXD 发送引脚将电流倒灌到对端 IC。
- 三,对于 CH340 系列需要外部晶振的芯片,在选用晶振时如果选择 12MHz 的石英晶体,那么旁路电容选择 22pF 的独石或高频瓷片电容如图,C11,C12。如果选用的低成本陶瓷晶体,那么旁路电路的容量必须用该晶体厂家的推荐值,一般情况下是 47pF。
- TB6612FNG芯片(重点分析下)
- TB6612FNG模块相对于传统的L298N效率上提高很多;TB6612 是双驱动,也就是可以驱动两个电机
- 下面分别是控制两个电机的 IO 口
- 一,STBY 口接单片机的 IO 口清零电机全部停止,置 1 通过 AIN1 AIN2,BIN1,BIN2 来控制正反转
- 二,VM 接 12V 以内电源
- 三,VCC 接 5V 电源
- 四,GND 接电源负极
- 五,PWMA 接单片机的 PWM 口
- 定时器 PWM 输出
- 一,首先需设置 T/C 中断屏蔽寄存器 TIMSKx 使能定时器溢出中断。
- 二,其次分别设置 T/C 控制寄存器 TCC-RxA 和TCCRxB 选择 PWM 模式和预分频比
- 三,最后将控制信号引脚 I/0 置为输出。程序运行时,每当定时器计数产生溢出,CPU 响应中断,定时器回零后重新开始计数。
- 以下代码的示例设置为`快速PWM反向输出模式`,当系统时钟计为fclk时,PWM输出频率fPWM=fclk/64/256
- 为获得更高的 PWM 波形精度,可以采用相位修正的 PWM 输出模式,不过在精度提高的同时,fPWM 也将减半,以下代码得到 fPWM=fclk/64/512。
- PWM 占空比大小的改变通过对输出比较寄存器 OCRxx 的数值操作来实现,例如当 OCRxx=203 时,占空比为 204/256=80%。编程时将速度变量值写入 OCRxx寄存器,从而达到改变占空比和对电机调速的目的。
- 四,通过电位器调速试验来检测TB6612FNG的PWM控制与电机输出转速间的线性关系。单片机 ADC 对精密多圈电位器的电压值进行采样,用于控制电机转速。
- (2)试验中,随着电位器阻值的调整,TB6612FNG 输出端电压测量值成比例变化,同时对电机实现启停和加减速控制,达到了预期试验效果,表明其输出和 PWM 输入之间具有良好的线性关系。
- (3)运行性能
- 1.器件输出状态在驱动/制动之间切换时,`电机转速和 PWM 占空比之间能保持较好的线性关系`,其运行控制效果好于器件在驱动/停止状态之间切换,所以表 1 中的 INl/IN2 一般不采用 L/L 控制组合。
- 2.`fPWM 较高时,电机运行连续平稳、噪音小`,但器件功耗会随频率升高而增大;fPWM 较低时,利于降低功耗,并能提高调速线性度,但过低的频率可能导致电机转动连贯性的降低。通常 fPWM>1 kHz 时,器件能够稳定的控制电机。
- 3..过大的 PWM 占空比会影响电机驱动电流的稳定性和器件的输出负载能力,应根据不同的速度要求合理设定占空比范围。
- 4.器件工作温度过高会导致其输出功率的下降,电路 PCB 设计中应保证足够面积的覆铜,这样有助于散热,利于器件长时间稳定工作
- `1-1.霍尔测速码盘 `
- `1-2超声波测距模块(HC-SR04) `
- 1-2-1接口定义
- 控制口发一个 10US 以上的高电平,就可以在接收口等待高电平输出.一有输出就可以开定时器计时,当此口变为低电平时就可以读定时器的值,此时就为此次测距的时间,方可算出距离.如此不断的周期测,就可以达到你移动测量的值了。
- 1-2-2模块工作原理
- 以上时序图表明你只需要提供一个10us以上脉冲触发信号,该模块内部将发出8个40khz周期电平检测回波。一旦检测有回波信号则输出回响信号。回响信号的脉冲宽度与所测的距离成正比。由此通过发射信号到收到的回响信号时间间隔可以计算得到距离。公式:uS/58=厘米或者uS/148=英寸;或是:距离=高电平时间*声速(340m/s)/2;建议测量周期为60ms以上,以防止发射信号对回响信号的影响。
- 通讯方式
- 1-2-3 超声波测距原理及系统组成
- `ATK-MPU6050 六轴传感器模块 `
- 一,初始化步骤
- `简单介绍几个重要的寄存器。 `
- `DMP 使用`
- 姿态 数据,也就是欧拉角:航向角(yaw)、横滚角(roll)和俯仰角(pitch)。有了这三个角, 我们就可以得到当前四轴的姿态,这才是我们想要的结果。
- 其中 quat[0]~ quat[3]是 MPU6050 的 DMP 解算后的四元数,q30 格式,所以要除以一个 2 的 30 次方,其中 q30 是一个常量:1073741824,即 2 的 30 次方,然后带入公式,计算出 欧拉角。上述计算公式的 57.3 是弧度转换为角度,即 180/π,这样得到的结果就是以度(°) 为单位的。
- `介绍几个函数库`
- 此函数首先通过 IIC_Init(需外部提供)初始化与 MPU6050 连接的 IIC 接口,然后调用 mpu_init 函数,初始化 MPU6050,之后就是设置 DMP 所用传感器、FIFO、采样率和加载 固件等一系列操作,在所有操作都正常之后,最后通过 mpu_set_dmp_state(1)使能 DMP 功 能,在使能成功以后,我们便可以通过 mpu_dmp_get_data 来读取姿态解算后的数据了。
- `问题一:采用 NANO STM32F103 开发板连接 ATK-MPU6050 模块,模块共有两个实验例程,分别是串口助手打印和上位机显示。 `
- `1:串口助手打印:将读取的温度传感器、加速度传感器、陀螺仪、DMP 姿态解算后的 欧拉角等数据通过串口输出在串口调试助手上。同时 DS0 闪烁,以表示程序正在运行。 `
- `2:上位机显示:将读取的加速度传感器、陀螺仪、DMP 姿态结算后的欧拉角等数据, 通过串口上报给上位机,利用上位机软件(ANO_Tech 匿名四轴上位机_V2.6.exe),可以 实时显示 MPU6050 的传感器状态曲线,并显示 3D 姿态。同时 DS0 闪烁,以表示程序正在 运行。 `
- 硬件连接
- 软件实现
- 串口助手打印
- `先了解前辈写好的MPU6050函数,需要调用`
- `MPU_Init`初始化函数
- 该函数对 MPU6050 进行初始化,该函数执行成功后, 便可以读取传感器数据了。 然后再看 `MPU_Get_Temperature`、`MPU_Get_Gyroscope` 和 `MPU_Get_Accelerometer` 等 三个函数,源码如下:
- 最后看 `MPU_Write_Len` 和 `MPU_Read_Len` 这两个函数,代码如下
- MPU_Write_Len 用于指定器件和地址,连续写数据,可用于实现 DMP 部分的:i2c_write 函数。而 MPU_Read_Len 用于指定器件和地址,连续读数据,可用于实现 DMP 部分的: i2c_read 函数。DMP 移植部分的 4 个函数,这里就实现了 2 个,剩下的 delay_ms 就直接采 用我们 delay.c 里面的 delay_ms 实现,get_ms 则直接提供一个空函数即可。
- 上位机显示
- 此部分代码除了 main 函数,还有几个函数,用于上报数据给上位机软件,利用上位机 软件显示传感器波形,以及 3D 姿态显示,有助于更好的调试 MPU6050
- 其中,`usart1_niming_report`函数用于数据打包,计算校验和,然后上报给上位机。`mpu6050_send_data`函数用于上报加速度和陀螺仪的原始数据,可用波形显示传感器数据,通过A1自定义帧发送。而`usar1_report_imu`函数,则用于上报飞快显示帧,可以实时3D显示MPU6050的姿态,传感器数据等。
- 最后,我们将 `MPU_Write_Byte`、`MPU_Read_Byte` 和 `MPU_Get_Temperature` 等三个函 数加入 USMART 控制,这样,我们就可以通过串口调试助手,改写和读取 MPU6050 的寄 存器数据了,并可以读取温度传感器的值
- 验证:串口助手打印
- 验证:上位机显示
- `通过APP进行蓝牙遥控`
- 可重力显示,电池电压显示,超声波距离显示,左右电机转速显示,具有按键控制,重力感应控制,摇杆控制
- 角度环参数D,角度环参数P,速度环参数P,速度环参数I
- 波形显示:加速度,陀螺仪,电池电压,卡尔曼滤波等波形
- `通信协议包定义`
- 下位机(小车接收数据)数据协议格式
- 数据例子:前进
- 数据例子:更新AP,AD
- 上行数据协议格式
- 数据例子:左车速20.5 右车速33.2 加速度140 陀螺仪40 超声波50 电压12.3v
- 下位机接收数据格式:
- 上位机接收数据格式:
- 上行数据协议格式
- `项目代码main`
- `项目代码timer.c`
- `UltrasonicWave_Configuration`超声波测距模块
- `UltrasonicWave_StartMeasure`初始化超声模块
- `项目代码USART`
- `项目代码usart`
- `项目代码motor`
- 待续
多路径小车机器人
购买的散装图
一、平衡小车原理
平衡小车是通过两个电机运动下实现小车不倒下直立行走的多功能智能小车,在外力的推拉下,小车依然保持不倒下。
一是放在指尖上可以移动,二是通过眼睛观察木棒的倾斜角度和倾斜趋势(角速度)。通过手指的移动去抵消木棒倾斜的角度和趋势,使得木棒能直立不倒。这样的条件是不可以缺一的,实际上加入这两个条件,控制过程中就是负反馈机制。
平衡小车也是这样的过程,通过负反馈实现平衡。与上面保持木棒直立比较则相对简单,因为小车有两个轮子着地,车体只会在轮子滚动的方向上发生倾斜。控制轮子转动,抵消在一个维度上倾斜的趋势便可以保持车体平衡了。
所以根据上述原理,通过测量小车的倾角和倾角速度控制小车车轮的加速度来消除小车的倾角。因此,小车倾角以及倾角速度的测量成为直立小车的关键。
测量倾角和倾角速度的集成传感器陀螺仪-MPU6050
二、角度(物理分析 PD算法)
控制平衡小车,使得它作加速运动。这样站在小车上(非惯性系,以车轮作为坐标原点)分析倒立摆受力,它就会受到额外的惯性力,该力与车轮的加速度方向相反,大小成正比。这样倒立摆(如图2)所受到的回复力为:公式1F = mg sin θ-ma cos θ≈mg θ-mk1θ
式1中,由于θ很小,所以进行了线性化。假设负反馈控制是车轮加速度a与偏角θ成正比,比例为k1。如果比例k1>g,(g是重力加速度)那么回复力的方向便于位移方向相反了。
而为了让倒立摆能够尽快回到垂直位置稳定下来,还需要增加阻尼力。增加的阻尼力与偏角的速度成正比,方向相反,因此公式1可改为:
F = mg θ-mk1 θ -mk2 θ'
按照上述倒立摆的模型,可得出控制小车车轮加速度的算法:
a =k1θ+k2θ'
式中θ为小车角度,θ`为角速度。k1 k2都是比例系数
根据上述内容,建立速度的比例微分负反馈控制,根据基本控制理论讨论小车通过闭环控制保持稳定的条件(这里需要对控制理论有基本了解)。假设外力干扰引起车模产生角加速度x(t)。沿着垂直于车模地盘方向进行受力分析,可以得到车模倾角与车轮运动加速度以及外力干扰加速度a(t)x(t)之间的运动方程。如图3所示。
在角度反馈控制中,与角度成正比例的控制量是称为比例控制
与角速度比例的控制量称为微分控制(角速度是角度的微分)。
因此上面的系数k1,k2分别称为比例和微分控制参数。其中微分参数相当于阻尼力,可以有效抑制车模震荡。通过微分抑制控制震荡的思想在后面的速度和方向控制中也同样适用。
总结控制车模直立稳定的条件如下:
(1)能够精确测量车模倾角θ的大小和角速度θ‘的大小
(2)可以控制车轮的加速度。
上述控制实际结果是小车与地面不是严格垂直,而是存在一个对应的倾角。在重力的作用下,小车朝着一个方向加速前进。为了保持小车的静止或者均速运动需要消除这个安装误差。在实际小车制作过程中需要进行机械调整和软件参数设置。另外需要通过软件中的速度控制来实现速度的稳定性。在小车角度控制中出现的小车倾角偏差,使得小车在倾斜的方向上产生加速。这个结果可以用来进行小车的速度控制。下面利用这个原理来调节小车的速度
三,测速(物理模型 建立数学模型 传递函数PD算法)
假设小车在上面直立控制调节下已经能够保持平衡了,但是由于安装误差,传感器实际测试的角度与车模角度有偏差,因此小车实际不是保持与地面垂直,而是存在一个倾角。在重力的作用下,小车就会朝倾斜的方向加速前进。控制速度只要通过控制小车的倾角就可以实现了。
具体实现需要解决三个问题
(1)如何测量小车的速度?
·
(2)如何通过小车直立控制实现小车倾角的改变?
(3)如何根据速度误差控制小车倾角?
一,第一个问题,可以通过安装在**电机输出轴上的光码盘**
来测量得到小车的车轮速度。利用控制单片机的计数器测量在固定时间间隔内脉冲信号的个数可以反馈电机的转速
。
二,第二个问题,可以通过角度控制给定值来解决。给定小车直立控制的设定值,在角度控制调节下,小车将会自动维持在一个角度。通过前面小车直立控制算法可以知道,小车倾角最终是跟踪重力加速度Z轴的角度。因此小车的倾角给定值与重力加速度Z轴角度相减,便可以最终得到小车的倾角。
三,第三个问题,析起来相对比较困难,远比直观进行速度负反馈分析复杂。首先对一个简单例子进行分析。假设小车开始保持静止,然后增加给定速度,为此需要小车往前倾斜以便获得加速度。在小车直立控制下,为了能够得到一个往前的倾斜角度,车轮需要往后运动,这样会引起车轮速度下降(因为车轮往负方向运动了)。由于负反馈,使得小车往前倾角需要更大。如此循环,小车很快就会倾倒。原本利用负反馈进行速度控制反而成了“正”反馈。
为什么负反馈控制在这儿失灵了呢?
原来在直立控制下的小车速度与小车倾角之间传递函数具有非最小相位特性,在反馈控制下容易造成系统的不稳定性。
.
.
为了保证系统稳定,往往取的小车倾角控制时间常数Tz很大。这样便会引起系统产生两个共轭极点,而且极点的实部变得很小,使得系统的速度控制会产生的震荡现象。这个现象在实际参数整定的时候可以观察到。那么如何消除速度控制过程中的震荡呢?这样便会引起系统产生两个共轭极点,而且极点的实部变得很小,使得系统的速度控制会产生的震荡现象。这个现象在实际参数整定的时候可以观察到。
那么如何消除速度控制过程中的震荡呢?
要解决震荡问题,在前面的小车角度控制中已经有了经验,那就是在控制反馈中增加速度微分控制。但由于车轮的速度反馈信号中往往存在着噪音,对速度进行微分运算会进一步加大噪声的影响。为此需要对上面控制方法进行改进。原系统中倾角调整过程时间常数往往很大,因此可以将该系统近似为一个积分环节。
将原来的微分环节和这个积分环节合并,形成一个比例控制环节。
这样可以保持系统控制传递函数不变,同时避免微分计算。
但在控制反馈中,只是使用反馈信号的比例和微分,没有利用误差积分,所以最终这个速度控制是有残差的控制。但是直接引入误差积分控制环节,会增加系统的负杂度,为此不再增加积分控制,而是通过与角度控制结合在进行改进。
要求小车在原地停止,速度为0.但是由于采用的是比例控制,如果此时陀螺仪有漂移,或者加速度传感器安装误差,最终小车倾角不会最终调整为0.小车会朝着倾斜方向恒速运动下去。注意此时车模不会像没有速度控制那样加速运行下去了,但是速度不会最终为0。为了消除这个误差,可以将小车倾角设定量直接积分补偿在角度控制输出中,这样就会彻底消除速度控制误差。第二点,由于加入了速度控制,它可以补偿陀螺仪和重力加速度的漂移和误差。所以此时重力加速度传感器实际没有必要了。
此时小车在控制启动的时候,需要保持小车的垂直状态。此时陀螺仪的积分角度也初始化为0。当然如果电路中已经包括了重力加速度传感器,也可以保留这部分,从而提高小车的稳定性。在后面的最终给定的控制方案中,保留了这部分的控制回路。
四,转向控制(PD算法)
通过左右电机速度差
驱动小车转向消除小车距离路中心的偏差。通过调整小车的方向,再加上车前行运动,可以消除小车距离中心线的偏差。这个过程是积分的过程,因此小车差动控制一般只需要进行简单的比例控制
就可以完成小车的方向控制。但是由于小车装有电池等重物体,具有很大的转到惯性,在调整过程中会出现小车转向过冲现象,如果不加以抑制,会使得小车过度转向而倒下。根据前面角度和速度控制的经验,为了消除小车方向控制中的过冲,需要增加微分控制
。
五,全方案整合
为了实现小车直立行走,需要采集如下信号:
(1)小车倾角速度陀螺仪信号,获得小车的倾角和角速度。
(2)重力加速度信号
(z轴信号)补偿陀螺仪的漂移,该信号可以省略,有速度控制替代。
(3)小车电机转速脉冲信号,获得小车运动速度,进行速度控制
(4)小车转动速度陀螺仪信号,获得小车角速度,进行方向控制
在小车控制中的直立,速度和方向控制三个环节中,都使用了比例微分(PD)控制,这三种控制算法的输出量最终通过叠加通过电机运动来完成
(1)小车直立控制:使用小车倾角的PD(比例,微分)控制;
g_fAngleControlOut = g_fCarAngle* g_fCarAngle_P +
gyro[0] *g_fCarAngle_D;
(2)小车速度控制:使用PD(比例,微分)控制:
g_fSpeedControlOutNew = (CAR_SPEED_SET - g_fCarSpeed) * g_fCarSpeed_P+
(CAR_POSITION_SET - g_fCarPosition) * g_fCarSpeed_I;
(3)小车方向控制:使用PD(比例、微分)控制。
speednow=-speedtarget*3.4 -gyro[2]*0.0015 ;
在上面控制过程中,车模的角度控制和方向控制都是通过直接将输出电压叠加后控制电机的转速实现的。而车模的速度控制本质上是通过调节车模的倾角实现的,由于车模是一个非最小相位系统,因此该反馈控制如果比例和速度过大,很容易形成正反馈,使得车模失控,造成系统的不稳定性。因此速度的调节过程需要非常缓慢和平滑。
六,PID算法
AngleCalculate:小车倾角计算函数。根据采集到的陀螺仪和重力加速度传感器的数值计算小车的角度和角速度。如果这部分的算法由外部一个运放实现,那么采集得到的直接是小车的角度和角速度,这部分算法可以省略。该函数每5ms调用一次。
AngleControl:小车直立函数。根据小车的角度和角速度计算小车电机的控制量。直立是5ms调用一次。
SpeedControl:小车速度控制函数。根据小车采集到 的电机转速和速度设定值,计算电机的控制量。该函数每40ms调用一次
MotorOutput:电机输出量汇集函数。根据直立函数,速度控制和方向控制所得到的控制量进行叠加,分别得到左右两个电极的输出电压控制量。对叠加后的输出量进行饱和处理。函数每5ms调用一次。在此请大家注意速度控制量叠加的极性是负。
MotorSpeedOut:电机PWM输出计算函数。根据左右两个电极的输出控制量的正负极性,叠加上一个小的死区数值,克服车模机械静态摩擦力。函数调用周期5毫秒。
SetMotorVoltage:PWM输出函数:根据两个电机的输出量,计算出PWM控制寄存器的数值,设置四个PWM控制寄存器的数值。函数调用周期1毫秒。
以上9个函数都是在1毫秒中断服务中进行被相互调用的。下图显示了这些函数之间的调用与参数传递关系。在个函数附近也表明了调用的周期。
Chaoshengbo:加入超声波壁障模块:根据前方障碍物的距离检测,一旦检测到后,通过直接PWM值输出(g_fchaoshengbooutput),相障碍物反方上运动,无需算法实现。每30毫秒调用一次。
七,程序
(1)时序总算法
void SysTick_Handler(void) //5ms定时器
{
BST_u8MainEventCount++; //总循环计算器
BST_u8trig++;
BST_u8SpeedControlCount++; //小车速度控制调用计数值
GetMotorPulse(); //脉冲计算函数
BST_u8SpeedControlPeriod++;
BST_u8DirectionControlPeriod++; //转向平滑输出计算比例值
BST_u8DirectionControlCout++;
AngleControl(); //角度PD控制PWM输出
MotorOutput(); //小车总PWM输出
if(BST_u8trig>=2)
{
Ultrasonic Wave_StartMeasure(); //调用超声波发送程序给Trig脚<10us高电平
chaoshengbo(); //超声波测距 距离
BST_u8trig=0;
}
if(BST_u8SpeedControlCount>=8) //当计数值为8时,即总系统运行40ms时候(每10个角度PWM输出中融入1个速度PWM输出,这样能保持速度PID输出不干扰角度PID输出,从而影响小车平衡)
{
SpeedControl(); //车模速度控制函数 每40ms调用一次
BST_u8SpeedControlCount=0; //小车速度控制调用计数值清零
BST_u8SpeedControlPeriod=0; //平滑输出比例值清零
}
}
(2)平衡程序
速度环控制函数
void SpeedControl(void)
{
BST_fCarSpeed=(BST_s32LeftMotoPulseSigma +BST_s32RightMotorPulseSigma) //左右电机脉冲数平均值作为小车当前车速
//sumamm = (BST_s32LeftMotorPulseSigma + BST_s32RightMotorPulseSigma );
BST_s32LeftMotorPulseSigma=BST_u32RightMotorPulseSigma=0; //全局变量 注意及时清零
BST_fCarSpeedOld*=0.7;
BST_fCarSpeedOld+=BST_fCarSpeed*0.3;
//BST_fCarSpeed = 0.7 * BST_fCarSpeedOld + 0.3 * BST_fCarSpeed ; //速度一阶滤波
BST_fCarPosition+=BST_fCarSpeedOld; //路程 即速度积分
BST_fCarPosition+=BST_fBluetoothSpeed; //融合蓝牙给定速度
BST_fCarPosition+=fchaoshengbo; //融合超声波给定速度
if(stopflag==1)
{
BST_fCarPosition=;
}
//积分上限设限
if((s32)BST_fCarPosition > CAR_POSITION_MAX)
BST_fCarPosition = CAR_POSITION_MAX;
if((s32)BST_fCarPosition < CAR_POSITION_MIN)
BST_fCarPosition = CAR_POSITION_MIN;
BST_fSpeedControlOutNew= (BST_fCarSpeedOld -CAR_SPEED_SET ) * BST_fCarSpeed_P + (BST_fCarPosition - CAR_POSITION_SET ) *BST_fCarSpeed_I;
//速度PI算法 速度*P+位移*I=速度PWM输出
}
void SpeedControlOutput(void)
//速度平滑输出函数
{
float fValue;
fValue=BST_fSpeedControlOutNew - BST_fSpeedControlOutOld;
//电压=速度PI算法-速度平滑输出函数
BST_fSpeedControlOut=fValue* (BST_u8SpeedControlPeriod + 1)/SPEED_CONTROL_PERIOD + BST_fSpeedControlOutOld;
//转向平滑输出计算公式
}
(3)蓝牙以及超声波
/***************************************************************
** 函数名称: BluetoothControl
** 功能描述: 蓝牙控制函数
手机发送内容
前:0x01 后:0x02
左:0x04 右:0x03
停止:0x07
功能键:(旋转)
左旋转:0x05 右旋转:0x06
停转:0x07
** 输 入:
** 输 出:
***************************************************************/
void USART3_IRQHandler(void)
{
u8 ucBluetoothValue;//定义一个u8类型数据的bluetoothValue
if(USART3->SR&(1<<5))//接收到数据
{
ucBluetoothValue =USART3->DR;
USART_ClearFlag(USART3,USART_FLAG_RXNE);
//USART_ClearITPendingBit(USART3,USART_IT_RXNE);
if(ucBluetoothValue<10)
{
switch(ucBluetoothValue)
{
case 0x01:
BST_fBluetoothSpeed = 3000;
chaoflag=1;
break;
//向前速度 250
case 0x02 :
BST_fBluetoothSpeed = (-3000);
chaoflag=1;
break;
//后退速度 -250
case 0x03:
BST_fBluetoothDirectionNew=-300;
chaoflag=1;
break;
//左旋
case 0x04 :
BST_fBluetoothDirectionNew=300;
chaoflag=1;
break;
//右旋转
case 0x05 :
BST_fBluetoothDirectionNew=driectionxco;
chaoflag=1;
break;
//左旋
case 0x06 :
BST_fBluetoothDirectionNew=-driectionxco;
chaoflag=1;
break;
//右旋转
case 0x07 :
BST_fBluetoothDirectionL =0;
BST_fBluetoothDirectionR = 0;
BST_fBluetoothDirectionSL =0;
BST_fBluetoothDirectionSR =0;
fchaoshengbo=0;
BST_fBluetoothDirectionNew=0;
btcount1=0;
chaoflag=0;
break;
//停
case 0x08 :
BST_fBluetoothDirectionSL =0;
BST_fBluetoothDirectionSR = 0;
directionl=0;
directionr=0;
btcount1=0;
fchaoshengbo=0;
BST_fBluetoothDirectionNew=0;
chaoflag=0;
break;
//停旋转
case 0x09 :
BST_fBluetoothSpeed =0;
break;
default :
BST_fBluetoothSpeed = 0;
BST_fBluetoothDirectionL=BST_fBluetoothDirectionR =0;
BST_fBluetoothDirectionSR=BST_fBluetoothDirectionSL=0;
btcount1=0;
chaoflag=0;
break;
}
}
else if(ucBluetoothValue<64&ucBluetoothValue>9)
{
BST_fCarAngle_P=ucBluetoothValue*1.71875;
}
else if(ucBluetoothValue<128&ucBluetoothValue>64)
{
BST_fCarAngle_D=(ucBluetoothValue-64)*0.15625;
}
else if(ucBluetoothValue<192&ucBluetoothValue>128)
{
BST_fCarSpeed_P=(ucBluetoothValue-128)*0.46875;
}
else if(ucBluetoothValue<256&ucBluetoothValue>192)
{
BST_fCarSpeed_I=(ucBluetoothValue-192)*0.15625;
}
}
}
/超声波距离计算*****/
void chaoshengbo(void)
{
if(chaoflag==0)
{
juli=TIM_GetCounter(TIM1)*5*34/200.0;
if(juli<=4.00)
//判断若距离小于8cm,小车输出向后PWM值。
{
fchaoshengbo= (-300);
}
else if(juli>=5&juli<=8)
{
fchaoshengbo=300;
}
else fchaoshengbo=0;
//距离大于8cm ,超声波PWM输出为0
}
}
提供电路图,按模块进行电路分析
铝电池10V转5V,DC-DC转换
一,从电路图看Q1为SI 2306此为N mos管,当Vgs>0时则三极管导通,G极为电池VM端为12V,S极为电池负极,开关没开时,Q1 N型mos管的S极为接GND,G极没有电压则为0V,则Vgs=Vg-Vs=0V-0v=0V,此时N管不导通;当电源打开时,Q1的G极为12V,则Vg-Vs=12v-0=12v>0,则N mos管导通接地;Q1启到保护电池作用,我猜测这个CDS-2P卧式容易短路,如果短路容易造成电池回路然后烧电池;将GND放在Q1mos管下面可以在电源打开时,CDS-2P的GND才会接地;我认为CDS-2P的GND处没有真正的接地,真正接地的是Q1 N型mos管的D极
二,SS34肖特基二极管,防止当N管是开状态时,电池反灌。
三,S1我也不知道什么来的,应该是电池保护芯片 ,SW输入,OUT输出,D1灯的作用是为了,灯亮时说明有电通过,灯灭时无电通过。
四,芯片78M05是DC-转DC5v的芯片,Q2为Nmos管当Vgs>0时导通,芯片78M05VOUT为5V,则Q2的S极为5V,Q2的G极为ON应该是电池端(ON不是电池端,这里是ON低压保护ON=5.78V,后面分析)
,则开机时Vgs=Vg-Vs=12v-5v>0,则Q2导通(注:Vgs=5.78v-5v>0不影响分析,后面给出解释),芯片通过Q2输出5V
五,D2灯的作用:芯片是否有5V供电
低压保护电路
一,D3为肖特基二极管,VM经过后压降约0.3v-0.4v则12v-0.3v=11.7v;C4为滤波电容,作用是电源滤波,R4的作用是电源关闭时,C4与R4形成回路,C4放电。感觉R4可以去掉,C4本身形成回路状态也可以放电。
二,R5与R6分压,此处的电路图画错,IN端电压=VM*(R6/(R5+R6))=11.7*(3.9/(10+3.9))=3.28V
LM321
一,此电路相当于将+IN与-IN相加得输出V0(个人理解)
TL431
它可以看成是稳压值可调的稳压二极管。如同稳压二极管电路一样,输入电源流过电阻R3后到TL431,然后输出。当Vk的电压小于稳压值,则TL431不动作。当VkA的电压大于稳压值,TL431就会像稳压二极管一样,把VkA的电压钳制在稳压值上。TL431的稳压值是可调的,由下面公式决定:
VKA=(1+ R1/R2) *2.5
这里有两种用法图一为经典接法,输出一个固定电压值,计算公式是: Vout = (1+R1/R2)*2.5;同时R3的数值应该满足1mA < (Vcc-Vout)/R3 < 500mA
这里的电路用到图一的直接输出2.5V
还有好多用法不做解释
综上LM321的VCC电压=3.28+2.5=5.78v
回归电路图
ON=5.78V.当电池电压降低,ON电压降低,则Q2 nmos管Vgs=Vg-Vs=5.78v-5v=0.78v>0,Q2导通,当电源电压低到一定程度时,5.78v降低则Q2mos管关闭,可以让DC5v关闭。
完美,就是这样了
CH340G芯片
一,在电路设计原理上,5V 供电时芯片 V3 引脚需要接一个 104 电容到地,3.3V 供电时直接将 V3 脚与 3.3V 电源引脚短接就可以了。这里的是5V供电,所以V3需接个104的电容。V3 的引脚除了在不同电压供电模式下接法不同,对于电容数值选用也是需要注意的。V3 引脚的电容用于内部电源节点退耦,来改善 USB 传输过程中的 EMI,通常容量在 4700pF 到 0.1uF 范围,建议容量为 0.01uF,即 103 电容。这里使用C9为104电容。
二,在实际应用中,当 CH340 与其他 IC 譬如 MCU 等器件一同使用时,如果串口直连的双方器件有一方不需要供电工作时,要注意电流倒灌导致未供电的芯片继续工作的情况
CH340 芯片的 发送引脚 TXD 上接一个反向二极管D5,然后再连接到对端 IC
;在接收引脚上加一个限流电阻来防止对端 IC 对CH340 倒灌电。
此电路图没加
通过反向二极管的原理是:在 CH340 发送数据时,发送高电平时二极管截止,但是由于对端 RXD 默认上拉也是高电平不会有采样问题,而发送低电平时二极管导通,对端 RXD 接收到低电平,因此可以正常通讯。并防止了 CH340 的 TXD 发送引脚将电流倒灌到对端 IC。
三,对于 CH340 系列需要外部晶振的芯片,在选用晶振时如果选择 12MHz 的石英晶体,那么旁路电容选择 22pF 的独石或高频瓷片电容如图,C11,C12。如果选用的低成本陶瓷晶体,那么旁路电路的容量必须用该晶体厂家的推荐值,一般情况下是 47pF。
TB6612FNG芯片(重点分析下)
TB6612FNG模块相对于传统的L298N效率上提高很多;TB6612 是双驱动,也就是可以驱动两个电机
下面分别是控制两个电机的 IO 口
一,STBY 口接单片机的 IO 口清零电机全部停止,置 1 通过 AIN1 AIN2,BIN1,BIN2 来控制正反转
二,VM 接 12V 以内电源
三,VCC 接 5V 电源
四,GND 接电源负极
五,PWMA 接单片机的 PWM 口
定时器 PWM 输出
一,首先需设置 T/C 中断屏蔽寄存器 TIMSKx 使能定时器溢出中断。
二,其次分别设置 T/C 控制寄存器 TCC-RxA 和TCCRxB 选择 PWM 模式和预分频比
三,最后将控制信号引脚 I/0 置为输出。程序运行时,每当定时器计数产生溢出,CPU 响应中断,定时器回零后重新开始计数。
以下代码的示例设置为快速PWM反向输出模式
,当系统时钟计为fclk时,PWM输出频率fPWM=fclk/64/256
TIMSKx |=1<<toiex;< p="">
TCCRxA=OxF3;
TCCRxB=Ox03;
DDRx |=(1<<pxx);< p="">
为获得更高的 PWM 波形精度,可以采用相位修正的 PWM 输出模式,不过在精度提高的同时,fPWM 也将减半,以下代码得到 fPWM=fclk/64/512。
TCCCRxA=0xF1:
TCCRxB=0x03;
PWM 占空比大小的改变通过对输出比较寄存器 OCRxx 的数值操作来实现,例如当 OCRxx=203 时,占空比为 204/256=80%。编程时将速度变量值写入 OCRxx寄存器,从而达到改变占空比和对电机调速的目的。
四,通过电位器调速试验来检测TB6612FNG的PWM控制与电机输出转速间的线性关系。单片机 ADC 对精密多圈电位器的电压值进行采样,用于控制电机转速。
###程序流程:首先进行电机控制信号,接着通过设置ADC控制状态寄存器ADCSRA和ADC多路复用选择寄存器ADMUX选择ADC频率和通道,然后选择合适的样本数量,对ADC循环采样并计算样本均值作为当前速度值,带入速度函数
(2)试验中,随着电位器阻值的调整,TB6612FNG 输出端电压测量值成比例变化,同时对电机实现启停和加减速控制,达到了预期试验效果,表明其输出和 PWM 输入之间具有良好的线性关系。
(3)运行性能
1.器件输出状态在驱动/制动之间切换时,电机转速和 PWM 占空比之间能保持较好的线性关系
,其运行控制效果好于器件在驱动/停止状态之间切换,所以表 1 中的 INl/IN2 一般不采用 L/L 控制组合。
2.fPWM 较高时,电机运行连续平稳、噪音小
,但器件功耗会随频率升高而增大;fPWM 较低时,利于降低功耗,并能提高调速线性度,但过低的频率可能导致电机转动连贯性的降低。通常 fPWM>1 kHz 时,器件能够稳定的控制电机。
3…过大的 PWM 占空比会影响电机驱动电流的稳定性和器件的输出负载能力,应根据不同的速度要求合理设定占空比范围。
4.器件工作温度过高会导致其输出功率的下降,电路 PCB 设计中应保证足够面积的覆铜,这样有助于散热,利于器件长时间稳定工作
1-1.霍尔测速码盘
减速比为 1:30 的电机霍尔传感器编码器的测速模块,配有 13 线强磁码盘,A B 双相输出共同利用下,通过计算可得出车轮转一圈时,脉冲数可达30132=780 个,单相也可以达到 390 个
1-2超声波测距模块(HC-SR04)
1-2-1接口定义
vcc,Trig(控制端),Echo(接收端),GND
控制口发一个 10US 以上的高电平,就可以在接收口等待高电平输出.一有输出就可以开定时器计时,当此口变为低电平时就可以读定时器的值,此时就为此次测距的时间,方可算出距离.如此不断的周期测,就可以达到你移动测量的值了。
1-2-2模块工作原理
(1)采用 IO 触发测距,给至少 10us 的高电平信号;
(2)模块自动发送 8 个 40khz 的方波,自动检测是否有信号返回;
(3)有信号返回,通过 IO 输出一高电平,高电平持续的时间就是超声波从发射到返回的时间.测试距离=(高电平时间*声速(340M/S))/2;
以上时序图表明你只需要提供一个10us以上脉冲触发信号,该模块内部将发出8个40khz周期电平检测回波。一旦检测有回波信号则输出回响信号。回响信号的脉冲宽度与所测的距离成正比。由此通过发射信号到收到的回响信号时间间隔可以计算得到距离。公式:uS/58=厘米或者uS/148=英寸;或是:距离=高电平时间*声速(340m/s)/2;建议测量周期为60ms以上,以防止发射信号对回响信号的影响。
通讯方式
1.如果用UART方式和其他设备通讯,在模块的硬件上不需要做任何设置,仅仅设置软件的波特率为57600bps
2.如果通过 IIC 的方式和其它设备通讯,本模块只能作为 severs,不能作为 master 使用。并且在硬件上要有相应的跳线支持,用来设置本模块的 IIC 地址。
1-2-3 超声波测距原理及系统组成
基本原理:经发射器发射出长约 6mm,频率为 40KHZ 的超声波信号。此信号被物 体反射回来由接收头接收,接收头实质上是一种压电效应的换能器。它接收到信号后产 生 mV 级的微弱电压信号。
ATK-MPU6050 六轴传感器模块
其中,SCL 和 SDA 是连接 MCU 的 IIC 接口,MCU通过这个IIC接口来控制MPU6050,另外还有一个IIC接口:AUX_CL和AUX_DA,这个接口可以用来连接外部从设备,比如磁 传感器,这样就可以组成一个九轴传感器。VLOGIC 是 IO 口电压,该引脚最低可以到 1.8V, 我们一般直接接 VDD 即可。AD0 是从 IIC 接口(接 MCU)的地址控制引脚,该引脚控制 IIC 地址的最低位。如果接 GND,则 MPU6050 的 IIC 地址是:0X68,如果接 VDD,则是 0X69,注意:这里的地址是不包含数据传输的最低位的
一,初始化步骤
(1)初始化IIC接口
采用 IIC通信先初始化与 MPU6050 连接的 SDA 和 SCL 数据线。
(2)复位 MPU6050
这一步让 MPU6050 内部所有寄存器恢复默认值,通过对电源管理寄存器 1(0X6B)的 bit7 写 1 实现。 复位后,电源管理寄存器 1 恢复默认值(0X40),然后必须设置该寄存器为 0X00,以唤醒 MPU6050,进入正常工作状态。
(3)设置角速度传感器(陀螺仪)和加速度传感器的满量程范围
这一步,我们设置两个传感器的满量程范围(FSR),分别通过陀螺仪配置寄存器(0X1B) 和加速度传感器配置寄存器(0X1C)设置。我们一般设置陀螺仪的满量程范围为±2000dps, 加速度传感器的满量程范围为±2g。
(4)设置其他参数
关闭中断,关闭AUX IIC接口,禁止FIFO,设置陀螺仪采样率和设置数字低通滤波器(DLPF)本章我们不用中断方式读取数据,所以关 闭中断,然后也没用到 AUX IIC 接口外接其他传感器,所以也关闭这个接口。分别通过中 断使能寄存器(0X38)和用户控制寄存器(0X6A)控制。MPU6050 可以使用 FIFO 存储传 感器数据,不过本章我们没有用到,所以关闭所有 FIFO 通道,这个通过 FIFO 使能寄存器 (0X23)控制,默认都是 0(即禁止 FIFO),所以用默认值就可以了。陀螺仪采样率通过采 样率分频寄存器(0X19)控制,这个采样率我们一般设置为 50 即可。数字低通滤波器(DLPF) 则通过配置寄存器(0X1A)设置,一般设置 DLPF 为带宽的 1/2 即可。
5)配置系统时钟源并使能角速度传感器和加速度传感器
系统时钟源同样是通过电源管理寄存器 1(0X1B)来设置,该寄存器的最低三位用于 设置系统时钟源选择,默认值是 0(内部 8M RC 震荡)不过我们一般设置为 1,选择 x 轴 陀螺 PLL 作为时钟源,以获得更高精度的时钟。同时,使能角速度传感器和加速度传感器, 这两个操作通过电源管理寄存器 2(0X6C)来设置,设置对应位为 0 即可开启。
简单介绍几个重要的寄存器。
电源管理寄存器 1,该寄存器地址为 0X6B
其中,DEVICE_RESET 位用来控制复位,设置为1,复位MPU6050,复位结束后,MPU硬件自动清零该位。SLEEP位用于控制MPU6050的工作模式,复位后,该位为1,即进入睡眠模式(低功耗),所以我们要清0该位,以进入工作模式。TEMP_DIS 用于设置是否使能温度传感器,设置为0,则使能。最后 CLKSEL[2:0]用于选择时钟系统时钟源
默认是使用内部 8M RC 晶振的,精度不高,所以我们一般选择 X/Y/Z 轴陀螺作为参考 的 PLL 作为时钟源,一般设置 CLKSEL=001 即可。
陀螺仪配置寄存器,该寄存器地址为:0X1B
该寄存器只关心 FS_SEL[1:0]这两个位,用于设置陀螺仪的满量程范围:0,±250 °/S;1,±500°/S;2,±1000°/S;3,±2000°/S;我们一般设置为3,即±2000°/S, 因为陀螺仪的ADC为16位分辨率,所以得到灵敏度为:65536/4000=16.4LSB/(°/S)。
加速度传感器配置寄存器,寄存器地址为:0X1C
AFS_SEL[1:0]这两个位,用于设置加速度传感器的满量程范围: 0,±2g;1,±4g;2,±8g;3,±16g;我们一般设置为 0,即±2g,因为加速度传感器 的 ADC 也是 16 位,所以得到灵敏度为:65536/4=16384LSB/g。
FIFO 使能寄存器,寄存器地址为:0X1C
该寄存器用于控制 FIFO 使能,在简单读取传感器数据的时候,可以不用 FIFO,设置 对应位为 0 即可禁止 FIFO,设置为 1,则使能 FIFO。注意加速度传感器的 3 个轴,全由 1 个位(ACCEL_FIFO_EN)控制,只要该位置 1,则加速度传感器的三个通道都开启 FIFO 了。
陀螺仪采样率分频寄存器,寄存器地址为:0X19
该寄存器用于设置 MPU6050 的陀螺仪采样频率,计算公式为:
采样频率 = 陀螺仪输出频率 / (1+SMPLRT_DIV)
这里陀螺仪的输出频率,是 1Khz 或者 8Khz,与数字低通滤波器(DLPF)的设置有关,当 DLPF_CFG=0/7 的时候,频率为 8Khz,其他情况是 1Khz。而且 DLPF 滤波频率一般设置 为采样率的一半。采样率,我们假定设置为 50Hz,那么 SMPLRT_DIV=1000/50-1=19。
配置寄存器,寄存器地址为:0X1A
主要关心数字低通滤波器(DLPF)的设置位,即:DLPF_CFG[2:0],加速 度计和陀螺仪,都是根据这三个位的配置进行过滤的。DLPF_CFG 不同配置对应的过滤情 况如表 1. 1. 2 所示:
这里的加速度传感器,输出速率(Fs)固定是1KHZ,而角速度传感器的输出速率(Fs),则根据DLPF_CFG的配置有所不同。一般我们设置角速度传感器的带宽为其采样率的一半,如前面所说,如果设置采样率为50HZ,那么带宽就应该设置为25HZ,取近似值20HZ,就应该设置DLPF_CFG=100.
电源管理寄存器 2,寄存器地址为:0X6C
该寄存器的 LP_WAKE_CTRL 用于控制低功耗时的唤醒频率。剩下的 6 位,分别控制加速度和陀螺仪的 x/y/z 轴是否进入待机模式,这里我们全部都不进入待机模 式,所以全部设置为 0 即可。
陀螺仪数据输出寄存器,总共有 8 个寄存器组成,地址为: 0X43~0X48,通过读取这 8 个寄存器,就可以读到陀螺仪 x/y/z 轴的值,比如 x 轴的数据, 可以通过读取 0X43(高 8 位)和 0X44(低 8 位)寄存器得到,
DMP 使用
姿态 数据,也就是欧拉角:航向角(yaw)、横滚角(roll)和俯仰角(pitch)。有了这三个角, 我们就可以得到当前四轴的姿态,这才是我们想要的结果。
使用 MPU6050 的 DMP 输出的四元数是 q30 格式的,也就是浮点数放大了 2 的 30 次方 倍。在换算成欧拉角之前,必须先将其转换为浮点数,也就是除以 2 的 30 次方,然后再进 行计算,计算公式为:
q0=quat[0] / q30; //q30 格式转换为浮点数
q1=quat[1] / q30;
q2=quat[2] / q30;
q3=quat[3] / q30;
//计算得到俯仰角/横滚角/航向角
pitch=asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3; //俯仰角
roll=atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)* 57.3; //横滚角
yaw=atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3; //航向角
其中 quat[0]~ quat[3]是 MPU6050 的 DMP 解算后的四元数,q30 格式,所以要除以一个 2 的 30 次方,其中 q30 是一个常量:1073741824,即 2 的 30 次方,然后带入公式,计算出 欧拉角。上述计算公式的 57.3 是弧度转换为角度,即 180/π,这样得到的结果就是以度(°) 为单位的。
介绍几个函数库
mpu_dmp_init,是 MPU6050 DMP 初始化函数
//mpu6050,dmp 初始化
//返回值:0,正常
// 其他,失败
u8 mpu_dmp_init(void)
{
u8 res=0;
IIC_Init(); //初始化 IIC 总线
if(mpu_init()==0) //初始化 MPU6050
{
res=mpu_set_sensors(INV_XYZ_GYRO|INV_XYZ_ACCEL);//需要的传感器
if(res)return 1;
res=mpu_configure_fifo(INV_XYZ_GYRO|INV_XYZ_ACCEL);//设置 FIFO
if(res)return 2;
res=mpu_set_sample_rate(DEFAULT_MPU_HZ); //设置采样率
if(res)return 3;
res=dmp_load_motion_driver_firmware(); //加载 dmp 固件
if(res)return 4;
res=dmp_set_orientation(inv_orientation_matrix_to_scalar(gyro_orientation));
//设置陀螺仪方向
if(res)return 5;
res=dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT|DMP_FEATURE_TAP |DMP_FEATURE_ANDROID_ORIENT|DMP_FEATURE_SEND_RAW_ACCEL |DMP_FEATURE_SEND_CAL_GYRO|DMP_FEATURE_GYRO_CAL);
//设置 dmp 功能
if(res)return 6;
res=dmp_set_fifo_rate(DEFAULT_MPU_HZ);//设置DMP 输出速率(最大 200Hz)
if(res)return 7;
res=run_self_test(); //自检
if(res)return 8;
res=mpu_set_dmp_state(1); //使能 DMP
if(res)return 9;
}
return 0;
}
此函数首先通过 IIC_Init(需外部提供)初始化与 MPU6050 连接的 IIC 接口,然后调用 mpu_init 函数,初始化 MPU6050,之后就是设置 DMP 所用传感器、FIFO、采样率和加载 固件等一系列操作,在所有操作都正常之后,最后通过 mpu_set_dmp_state(1)使能 DMP 功 能,在使能成功以后,我们便可以通过 mpu_dmp_get_data 来读取姿态解算后的数据了。
mpu_dmp_get_data 函数代码如下
//得到 dmp 处理后的数据(注意,本函数需要比较多堆栈,局部变量有点多)
//pitch:俯仰角 精度:0.1° 范围:-90.0° <---> +90.0°
//roll:横滚角 精度:0.1° 范围:-180.0°<---> +180.0°
//yaw:航向角 精度:0.1° 范围:-180.0°<---> +180.0°
//返回值:0,正常
// 其他,失败
u8 mpu_dmp_get_data(float *pitch,float *roll,float *yaw)
{
float q0=1.0f,q1=0.0f,q2=0.0f,q3=0.0f;
unsigned long sensor_timestamp;
short gyro[3], accel[3], sensors;
unsigned char more;
long quat[4];
if(dmp_read_fifo(gyro, accel, quat, &sensor_timestamp, &sensors,&more))
return 1;
if(sensors&INV_WXYZ_QUAT)
{
q0 = quat[0] / q30; //q30 格式转换为浮点数
q1 = quat[1] / q30;
q2 = quat[2] / q30;
q3 = quat[3] / q30;
//计算得到俯仰角/横滚角/航向角
*pitch = asin(-2 * q1 * q3 + 2 * q0* q2)* 57.3; // 俯仰角
*roll = atan2(2 * q2 * q3 + 2 * q0 * q1, -2 * q1 * q1 - 2 * q2* q2 + 1)* 57.3; // 横滚角
*yaw= atan2(2*(q1*q2 + q0*q3),q0*q0+q1*q1-q2*q2-q3*q3) * 57.3; //航向角
}else return 2;
return 0;
}
此函数用于得到 DMP 姿态解算后的俯仰角、横滚角和航向角。不过本函数局部变量有 点多,大家在使用的时候,如果死机,那么请设置堆栈大一点(在 startup_stm32f103xb.s 里面 设置,默认是 400)。这里就用到了我们前面介绍的四元数转欧拉角公式,将 dmp_read_fifo函数读到的 q30 格式四元数转换成欧拉角。
问题一:采用 NANO STM32F103 开发板连接 ATK-MPU6050 模块,模块共有两个实验例程,分别是串口助手打印和上位机显示。
1:串口助手打印:将读取的温度传感器、加速度传感器、陀螺仪、DMP 姿态解算后的 欧拉角等数据通过串口输出在串口调试助手上。同时 DS0 闪烁,以表示程序正在运行。
2:上位机显示:将读取的加速度传感器、陀螺仪、DMP 姿态结算后的欧拉角等数据, 通过串口上报给上位机,利用上位机软件(ANO_Tech 匿名四轴上位机_V2.6.exe),可以 实时显示 MPU6050 的传感器状态曲线,并显示 3D 姿态。同时 DS0 闪烁,以表示程序正在 运行。
硬件连接
ATK-MPU6050 模块,通过 P1 排针与外部连接,引出了 VCC、GND、 IIC_SDA、IIC_SCL、MPU_INT 和 MPU_AD0 等信号,其中,IIC_SDA 和 IIC_SCL 带了 4.7K 上拉电阻,外部可以不用再加上拉电阻了,另外 MPU_AD0 自带了 10K 下拉电阻,当 AD0 悬空时,默认 IIC 地址为(0X68)。
软件实现
串口助手打印
先了解前辈写好的MPU6050函数,需要调用
MPU_Init
初始化函数
u8 MPU_Init(void)
{
u8 res;
MPU_IIC_Init(); //初始化 IIC 总线
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X80);//复位 MPU6050
delay_ms(100);
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X00);//唤醒 MPU6050
MPU_Set_Gyro_Fsr(3); //陀螺仪传感器,±2000dps
MPU_Set_Accel_Fsr(0); //加速度传感器,±2g
MPU_Set_Rate(50); //设置采样率 50Hz
MPU_Write_Byte(MPU_INT_EN_REG,0X00); //关闭所有中断
MPU_Write_Byte(MPU_USER_CTRL_REG,0X00); //I2C 主模式关闭
MPU_Write_Byte(MPU_FIFO_EN_REG,0X00); //关闭 FIFO
MPU_Write_Byte(MPU_INTBP_CFG_REG,0X80); //INT 引脚低电平有效
res=MPU_Read_Byte(MPU_DEVICE_ID_REG);
if(res==MPU_ADDR)//器件 ID 正确
{
MPU_Write_Byte(MPU_PWR_MGMT1_REG,0X01);
//设置 CLKSEL,PLL X 轴为参考
MPU_Write_Byte(MPU_PWR_MGMT2_REG,0X00);//加速度与陀螺仪都工作
MPU_Set_Rate(50); //设置采样率为 50Hz
}else return 1;
return 0;
}
该函数对 MPU6050 进行初始化,该函数执行成功后, 便可以读取传感器数据了。 然后再看 MPU_Get_Temperature
、MPU_Get_Gyroscope
和 MPU_Get_Accelerometer
等 三个函数,源码如下:
获取温度函数MPU_Get_Temperature
//得到温度值
//返回值:温度值(扩大了 100 倍)
short MPU_Get_Temperature(void)
{
u8 buf[2];
short raw;
float temp;
MPU_Read_Len(MPU_ADDR,MPU_TEMP_OUTH_REG,2,buf);
raw=((u16)buf[0]<<8)|buf[1];
temp=36.53+((double)raw)/340;
return temp*100;
}
//得到陀螺仪值(原始值)
//gx,gy,gz:陀螺仪 x,y,z 轴的原始读数(带符号)
//返回值:0,成功
// 其他,错误代码
u8 MPU_Get_Gyroscope(short *gx,short *gy,short *gz)
{
u8 buf[6],res;
res=MPU_Read_Len(MPU_ADDR,MPU_GYRO_XOUTH_REG,6,buf);
if(res==0)
{
*gx=((u16)buf[0]<<8)|buf[1];
*gy=((u16)buf[2]<<8)|buf[3];
*gz=((u16)buf[4]<<8)|buf[5];
}
return res;
}
//得到加速度值(原始值)
//gx,gy,gz:陀螺仪 x,y,z 轴的原始读数(带符号)
//返回值:0,成功
// 其他,错误代码
u8 MPU_Get_Accelerometer(short *ax,short *ay,short *az)
{
u8 buf[6],res;
res=MPU_Read_Len(MPU_ADDR,MPU_ACCEL_XOUTH_REG,6,buf);
if(res==0)
{
*ax=((u16)buf[0]<<8)|buf[1];
*ay=((u16)buf[2]<<8)|buf[3];
*az=((u16)buf[4]<<8)|buf[5];
}
return res;
}
其中 MPU_Get_Temperature 用于获取 MPU6050 自带温度传感器的温度值,然后 MPU_Get_Gyroscope和MPU_Get_Accelerometer分别用于读取陀螺仪和加速度传感器的 原始数据。
最后看 MPU_Write_Len
和 MPU_Read_Len
这两个函数,代码如下
//IIC 连续写
//addr:器件地址
//reg:寄存器地址
//len:写入长度
//buf:数据区
//返回值:0,正常
// 其他,错误代码
u8 MPU_Read_Len(u8 addr,u8 reg,u8 len,u8 *buf)
{
IIC_Start();
IIC_Send_Byte((addr<<1)|0); //发送器件地址+写命令
if(IIC_Wait_Ack()){ IIC_Stop();return 1; } //等待应答
IIC_Send_Byte(reg); //写寄存器地址
IIC_Wait_Ack(); //等待应答
IIC_Start();
IIC_Send_Byte((addr<<1)|1);//发送器件地址+读命令
IIC_Wait_Ack(); //等待应答
while(len)
{
if(len==1)*buf=IIC_Read_Byte(0);//读数据,发送 nACK
else *buf=IIC_Read_Byte(1); //读数据,发送 ACK
len--; buf++;
}
IIC_Stop(); //产生一个停止条件
return 0;
}
MPU_Write_Len 用于指定器件和地址,连续写数据,可用于实现 DMP 部分的:i2c_write 函数。而 MPU_Read_Len 用于指定器件和地址,连续读数据,可用于实现 DMP 部分的: i2c_read 函数。DMP 移植部分的 4 个函数,这里就实现了 2 个,剩下的 delay_ms 就直接采 用我们 delay.c 里面的 delay_ms 实现,get_ms 则直接提供一个空函数即可。
main.c 里面修改代码如下
int main(void)
{
u8 t=0;
u8 key=0;
u8 GetData=1;
float pitch,roll,yaw; //欧拉角
short aacx,aacy,aacz; //加速度传感器原始数据
short gyrox,gyroy,gyroz; //陀螺仪原始数据
short temp; //温度
HAL_Init(); //初始化 HAL 库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M
delay_init(72); //初始化延时函数
uart_init(115200); //初始化串口 115200
LED_Init(); //初始化 LED
KEY_Init(); //初始化按键
usmart_init(72); //USMART 初始化
printf("ALIENTEK NANO STM32\r\n");
printf("MPU6050 TEST\r\n");
while(mpu_dmp_init())//MPU DMP 初始化
{
printf("MPU6050 Error!!!\r\n");
delay_ms(500);
LED0=!LED0;//DS0 闪烁
}
printf("MPU6050 OK\r\n");
while(1)
{
GetData=!GetData;
if(GetData)printf("GETDATA ON\r\n");
else printf("GETDATA OFF\r\n");
}
if(mpu_dmp_get_data(&pitch,&roll,&yaw)==0)
{
temp=MPU_Get_Temperature(); //得到温度值
MPU_Get_Accelerometer(&aacx,&aacy,&aacz); //得到加速度传感器数据
MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz); //得到陀螺仪数据
if(GetData)//GetData=0 时 用于 USMART 调试 MPU6050 寄存器
{
if((t%10)==0)
{
//temp 值
if(temp<0)
{
temp=-temp; //转为正数
printf(" Temp: -%d.%dC\r\n",temp/100,temp%10);
}else
printf(" Temp: %d.%dC\r\n",temp/100,temp%10);
//pitch 值
temp=pitch*10;
if(temp<0)
{
temp=-temp; //转为正数
printf(" Pitch: -%d.%dC\r\n",temp/10,temp%10);
}else
printf(" Pitch: %d.%dC\r\n",temp/10,temp%10);
//roll 值
temp=roll*10;
if(temp<0)
{
temp=-temp; //转为正数
printf(" Roll: -%d.%dC\r\n",temp/10,temp%10);
}else
printf(" Roll: %d.%dC\r\n",temp/10,temp%10);
//yaw 值
temp=yaw*10;
if(temp<0)
{
temp=-temp; //转为正数
printf(" Yaw: -%d.%dC\r\n",temp/10,temp%10);
}else
printf(" Yaw: %d.%dC\r\n",temp/10,temp%10);
printf("\r\n");
t=0;
LED0=!LED0;//LED 闪烁
}
}
}
t++;
}
该文件就一个main函数,main函数也非常简单,先对用到的外设(串口,按键)、MPU6050 传感器等初始化,然后将获取的温度、加速度、陀螺仪的数据输出到串口上。
上位机显示
该例程与串口助手打印例程底层函数一样,这里不再做讲解,下面我们说下 main.c 文 件,main.c 文件代码如下:
//串口 1 发送 1 个字符
//c:要发送的字符
void usart1_send_char(u8 c)
{
while((USART1->SR&0X40)==0);//等待上一次发送完毕
USART1->DR=c;
}
//传送数据给匿名四轴上位机软件
//fun:功能字. 0XA0~0XAF
//data:数据缓存区,最多 28 字节!!
//len:data 区有效数据个数
void usart1_niming_report(u8 fun,u8*data,u8 len)
{
u8 send_buf[32];
u8 i;
if(len>28)return; //最多 28 字节数据
send_buf[len+3]=0; //校验数置零
send_buf[0]=0x88;//帧头
send_buf[1]=fun; //功能字
send_buf[2]=len; //数据长度
for(i=0;i<len;i++)send_buf[3+i]=data[i]; //复制数据
for(i=0;i<len+3;i++)send_buf[len+3]+=send_buf[i]; //计算校验和
for(i=0;i<len+4;i++)usart1_send_char(send_buf[i]); //发送数据到串口 1
}
//发送加速度传感器数据和陀螺仪数据
//aacx,aacy,aacz:x,y,z 三个方向上面的加速度值
//gyrox,gyroy,gyroz:x,y,z 三个方向上面的陀螺仪值
void mpu6050_send_data(short aacx,short aacy,short aacz,short gyrox,short gyroy,short gyroz)
{
u8 tbuf[12];
tbuf[0]=(aacx>>8)&0XFF;
tbuf[1]=aacx&0XFF;
tbuf[2]=(aacy>>8)&0XFF;
tbuf[3]=aacy&0XFF;
tbuf[4]=(aacz>>8)&0XFF;
tbuf[5]=aacz&0XFF;
tbuf[6]=(gyrox>>8)&0XFF;
tbuf[7]=gyrox&0XFF;
tbuf[8]=(gyroy>>8)&0XFF;
tbuf[9]=gyroy&0XFF;
tbuf[10]=(gyroz>>8)&0XFF;
tbuf[11]=gyroz&0XFF;
usart1_niming_report(0XA1,tbuf,12);//自定义帧,0XA1
}
//通过串口 1 上报结算后的姿态数据给电脑
//aacx,aacy,aacz:x,y,z 三个方向上面的加速度值
//gyrox,gyroy,gyroz:x,y,z 三个方向上面的陀螺仪值
//roll:横滚角.单位 0.01 度。 -18000 -> 18000 对应 -180.00 -> 180.00 度
//pitch:俯仰角.单位 0.01 度。-9000 - 9000 对应 -90.00 -> 90.00 度
//yaw:航向角.单位为 0.1 度 0 -> 3600 对应 0 -> 360.0 度
void usart1_report_imu(short aacx,short aacy,short aacz,short gyrox,short gyroy,short gyroz,short roll,short pitch,short yaw)
{
u8 tbuf[28];
u8 i;
for(i=0;i<28;i++)tbuf[i]=0;//清 0
tbuf[0]=(aacx>>8)&0XFF;
tbuf[1]=aacx&0XFF;
tbuf[2]=(aacy>>8)&0XFF;
tbuf[3]=aacy&0XFF;
tbuf[4]=(aacz>>8)&0XFF;
tbuf[5]=aacz&0XFF;
tbuf[6]=(gyrox>>8)&0XFF;
tbuf[7]=gyrox&0XFF;
tbuf[8]=(gyroy>>8)&0XFF;
tbuf[9]=gyroy&0XFF;
tbuf[10]=(gyroz>>8)&0XFF;
tbuf[11]=gyroz&0XFF;
tbuf[18]=(roll>>8)&0XFF;
tbuf[19]=roll&0XFF;
tbuf[20]=(pitch>>8)&0XFF;
tbuf[21]=pitch&0XFF;
tbuf[22]=(yaw>>8)&0XFF;
tbuf[23]=yaw&0XFF;
usart1_niming_report(0XAF,tbuf,28);//飞控显示帧,0XAF
}
int main(void)
{
u8 t=0;
float pitch,roll,yaw; //欧拉角
short aacx,aacy,aacz; //加速度传感器原始数据
short gyrox,gyroy,gyroz; //陀螺仪原始数据
HAL_Init(); //初始化 HAL 库
Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M
delay_init(72); //初始化延时函数
uart_init(500000); //初始化串口 500000
LED_Init(); //初始化 LED
KEY_Init(); //初始化按键
usmart_init(72); //USMART 初始化
printf("ALIENTEK NANO STM32\r\n");
printf("MPU6050 TEST\r\n");
while(mpu_dmp_init())//MPU DMP 初始化
{
printf("MPU6050 Error!!!\r\n");
delay_ms(500);
LED0=!LED0;//DS0 闪烁
}
printf("MPU6050 OK\r\n");
while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)
{
GetData=!GetData;
if(GetData)printf("GETDATA ON\r\n");
else printf("GETDATA OFF\r\n");
}
if(mpu_dmp_get_data(&pitch,&roll,&yaw)==0)
{
MPU_Get_Accelerometer(&aacx,&aacy,&aacz); //得到加速度传感器数据
MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz); //得到陀螺仪数据
if(GetData)//(GetData=0 时 用于 USMART 调试 MPU6050 寄存器)
{
mpu6050_send_data(aacx,aacy,aacz,gyrox,gyroy,gyroz);
//用自定义帧发送加速度和陀螺仪原始数据
usart1_report_imu((aacx,aacy,aacz,gyrox,gyroy,gyroz, (int)(roll*100),(int)(pitch*100),(int)(yaw*10));
//发送数据到上位机
}
if((t%10)==0)
{
t=0;
LED0=!LED0;//LED 闪烁
}
}
t++;
}
}
此部分代码除了 main 函数,还有几个函数,用于上报数据给上位机软件,利用上位机 软件显示传感器波形,以及 3D 姿态显示,有助于更好的调试 MPU6050
其中,usart1_niming_report
函数用于数据打包,计算校验和,然后上报给上位机。mpu6050_send_data
函数用于上报加速度和陀螺仪的原始数据,可用波形显示传感器数据,通过A1自定义帧发送。而usar1_report_imu
函数,则用于上报飞快显示帧,可以实时3D显示MPU6050的姿态,传感器数据等。
最后,我们将 MPU_Write_Byte
、MPU_Read_Byte
和 MPU_Get_Temperature
等三个函 数加入 USMART 控制,这样,我们就可以通过串口调试助手,改写和读取 MPU6050 的寄 存器数据了,并可以读取温度传感器的值
验证:串口助手打印
出现问题:
1,当该报错闪烁比较快的时候,约 1 秒钟 1 次,则说明读取 ID 错误了,此时建议给 模块断电,然后重新上电。
2,当该报错闪烁比较慢的时候,约 3 秒钟 1 次,则说明 ID 读取正常,DMP 初始化异常
串口调试助手打印显示了 MPU6050 的温度、俯仰角(pitch)、横滚角(roll)和航向角 (yaw)的数值。
验证:上位机显示
上图就是波形化显示我们通过 mpu6050_send_data
函数发送的数据。采用 A1 功能帧 发送,总共 6 条线(Series1~6)显示波形,全部来自 A1 功能帧,int16 数据格式,Series1~6 分别代表:加速度传感器 x/y/z 和角速度传感器(陀螺仪)x/y/z 方向的原始数据。
上图就是 3D 显示了我们开发板的姿态,通过usart1_report_imu
函数发送的数据显示, 采用飞控显示帧格(AF)式上传,同时还显示了加速度陀螺仪等传感器的原始数据。
还可以用 USMART 读写 MPU6050 的任何寄存器,来调试代码
通过APP进行蓝牙遥控
可重力显示,电池电压显示,超声波距离显示,左右电机转速显示,具有按键控制,重力感应控制,摇杆控制
角度环参数D,角度环参数P,速度环参数P,速度环参数I
波形显示:加速度,陀螺仪,电池电压,卡尔曼滤波等波形
通信协议包定义
下位机(小车接收数据)数据协议格式
数据例子:前进
'$1,0,0,0,0,0,0,0,0,0#'
数据例子:更新AP,AD
'$0,0,0,0,1,0,AP23.4,AD0.45,VP0,VI0#'
上行数据协议格式
数据例子:左车速20.5 右车速33.2 加速度140 陀螺仪40 超声波50 电压12.3v
'$LV20.5,RV33.2,AC140,GY40,CSB50,VI12.3#'
下位机接收数据格式:
'$0,0,0,0,0,0,0,0,0,0#' 浮点数字符串
上位机接收数据格式:
'$LVx,RVx,ACx,GYx,CSBx,VIx#' 浮点数字符串
上位机数据以字符串格式发送和接收,编译下位机(小车)程序时,需要进行数字与字符串之间转换后,才可以使用。
上行数据协议格式
备注:放开手指摇杆会自动复位到区域⑤
项目代码main
#include "mpu6050.h"
#include "i2c_mpu6050.h"
#include "motor.h"
#include "upstandingcar.h"
#include "sysTick.h"
#include "led.h"
#include "adc.h"
#include "usart.h"
#include "i2c.h"
#include "timer.h"
#include "UltrasonicWave.h"
float gyz;
int acc;
int acc1;
/*协议相关*/
int main(void)
{
SystemInit();//系统初始化
Timerx_Init(5000,7199);//定时器TIM1
UltrasonicWave_Configuration();//超声波初始化设置 IO口及中断设置
USART1_Config();//串口1初始化 上位机
USART3_Config();//串口3初始化 蓝牙与USART3公用相同IO口
TIM2_PWM_Init();//PWM输出初始化
MOTOR_GPIO_Config();//电机IO口初始化
LED_GPIO_Config();
Adc_Init();
TIM3_Encoder_Init();//编码器获取脉冲数 PA6 7
TIM4_Encoder_Init();//编码器获取脉冲数 PB6 7
i2cInit();//IIC初始化 用于挂靠在总线上的设备使用
delay_nms(10);//延时10ms
MPU6050_Init();//MPU6050 DMP陀螺仪初始化
SysTick_Init();//SysTick函数初始化
CarUpstandInit();//小车直立参数初始化
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;//使能总算法时钟
while (1)
{
MPU6050_Pose();//获取MPU6050角度状态
}
if (newLineReceived)
{
ProtocolCpyData();
Protocol();
}
/*通过状态控制小车*/.
CarStateOut();
SendAutoUp();
}
项目代码timer.c
#include "timer.h"
#include "led.h"
#include "upstandingcar.h"
#include "outputdata.h"
#include "mpu6050.h"
#include "UltrasonicWave.h"
#include "I2C_MPU6050.h"
//通用定时器中断初始化
//这里时钟选择为APB2 72Mhz,而APB1为36M
//arr:自动重装值。
//psc:时钟预分频数
//这里使用的是定时器1 ,定时时间t=arr*(psc*1/APB2(72Mhz))
void Timerx_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //TIM1时钟使能
TIM_TimeBaseStructure.TIM_Period = 5000;
//设置在下一个更新事件装入活动的自动重装载寄存器周期的值 计数到5000为500ms
TIM_TimeBaseStructure.TIM_Prescaler =(7200-1);
//设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 1;
//设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
//TIM向上计数模式
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
//根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
TIM_ITConfig( //使能或者失能指定的TIM1中断
TIM1, //TIM1
TIM_IT_Update | //TIM1 中断源
TIM_IT_Trigger, //TIM1 触发中断源
ENABLE //使能
);
NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn;//TIM1中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;//先占优先级3级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
void TIM1_UP_IRQHandler(void) //TIM1中断
{
if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)
//检查指定的TIM1中断发生与否:TIM1 中断源
{
TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
//清除TIMx的中断待处理位:TIM1 中断源
}
}
UltrasonicWave_Configuration
超声波测距模块
UltrasonicWave_StartMeasure
初始化超声模块
#include "UltrasonicWave.h"
#include "usart.h"
#include "timer.h"
#include "delay.h"
#include "stm32f10x_exti.h"
#define TRIG_PORT GPIOB //定义TRIG_PORT为PB
#define ECHO_PORT GPIOB //定义ECHO_PORT为PB
#define TRIG_PIN GPIO_Pin_0 //定义TRIG 对应IO口 PB0
#define ECHO_PIN GPIO_Pin_1 //定义ECHO 对应IO口 PB1
int count;
void UltrasonicWave_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStrure;
EXTI_InitTypeDef EXTI_InitStrure;//事件控制器 外部中断 挂起寄存器
NVIC_InitTypeDef NVIC_InitStructure;//嵌套向量中断控制器
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE); //关闭jtag
//PA13/14/15 & PB3/4默认配置为JTAG功能。有时我们为了充分利用MCU I/O口的资源,会把这些端口设置为普通I/O口。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitStructure.GPIO_Pin = TRIG_PIN;//PC8接TRIG
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//设为推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(TRIG_PORT, &GPIO_InitStructure);//初始化外设GPIO
GPIO_InitStructure.GPIO_Pin = ECHO_PIN;//PB1接ECH0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;//设为输入
GPIO_Init(ECHO_PORT,&GPIO_InitStructure);//初始化GPIOB
//GPIOB.1 中断线以及中断初始化配置
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);
EXTI_InitStructure.EXTI_Line=EXTI_Line1;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
//使能按键所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
//子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
//使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
//根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
}
/***********EXTI1 中断程序****************/
void EXTI1_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line1) != RESET)
{
TIM_SetCounter(TIM1,0);
TIM_Cmd(TIM1, ENABLE);//开启时钟 Tim1
count= 1;
while(count)//等待低电平
{
if(TIM_GetCounter(TIM1)>=10)//9*10us=90us
{
TIM_Cmd(TIM1, DISABLE);// 关闭时钟 Tim1
count=0;// 循环数清零
}
else count=GPIO_ReadInputDataBit(ECHO_PORT,ECHO_PIN);
//检测PB1是否高电平 高 则count为1 低 则count为0
}
TIM_Cmd(TIM1, DISABLE);//定时器 Tim1 关闭
EXTI_ClearITPendingBit(EXTI_Line1);//清除EXTI1线路挂起位
}
}
开始测距,发送一个>10us的脉冲,然后测量返回的高电平时间
void UltrasonicWave_StartMeasure(void)
{
GPIO_SetBits(TRIG_PORT,TRIG_PIN);
//送>10US的高电平 本指令与:GPIO_SetBits(PB,GPIO_Pin_0) 等同
delay_us(12);//延时11US
GPIO_ResetBits(TRIG_PORT,TRIG_PIN);
//本指令与:GPIO_SetBits(PB,GPIO_Pin_1) 等同
}
项目代码USART
#include "usart.h"
#include <stdarg.h>
#include "misc.h"
//串口接收DMA缓存
uint8_t Uart_Rx[UART_RX_LEN] = {0};
void USART1_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
void USART3_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;//定义串口初始化结构体
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3 , ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
NVIC_Configurationusart3();
USART_InitStructure.USART_BaudRate = 9600;//波特率9600
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//8位数据
USART_InitStructure.USART_StopBits = USART_StopBits_1;//1个停止位
USART_InitStructure.USART_Parity = USART_Parity_No ;//无校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//禁用RTSCTS硬件流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //使能发送接收
USART_Init(USART3, &USART_InitStructure);
USART_ITConfig(USART3, USART_IT_RXNE, ENABLE);//使能接收中断
USART_Cmd(USART3, ENABLE);
}
void NVIC_Configurationusart3(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_3);
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 4;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
int fputc(int ch,FILE *f)
{
/* 将Printf内容发往串口 */
USART_SendData(USART1,(unsigned char) ch);
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!= SET);
return (ch);
}
函数名:itoa
描述 :将整形数据转换成字符串
输入 :-radix =10 表示10进制,其他结果为0
-value 要转换的整形数
-buf 转换后的字符串
-radix = 10
static char *itoa(int value,char *string,int radix)
{
int i, d;
int flag = 0;
char *ptr = string;
if (radix != 10)
{
*ptr = 0;
return string;
}
if (!value)
{
*ptr++ = 0x30;
*ptr = 0;
return string;
}
if (value < 0)
{
*ptr++ = '-';
value *= -1;
}
for (i = 10000; i > 0; i /= 10)
{
d = value / i;
if(d || flag)//或者,有真输出真
{
*ptr++ = (char)(d + 0x30);
value -=(d * i);
flag = 1;
}
}
*ptr = 0;
return string;
}
函数名:USART1_printf
描述 :格式化输出,类似于C库中的printf,但这里没有用到C库
输入 :-USARTx 串口通道,这里只用到了串口1,即USART1
Data 要发送到串口的内容的指针
... 其他参数
void USART1_Printf(USART_TypeDef* USARTx,uint8_t *Data,...)
{
const char *s;//定义一个指向字符常量的指针
}
const char *ptr;
char const *ptr;
char * const ptr;
一,const char *ptr;
定义一个指向字符常量的指针,这里,ptr是一个指向char*类型的常量,所以不能用ptr来修改所指向的内容,换句话说,*ptr的值为const,不能修改。但是ptr的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对ptr而言,这个值是常量。实验如下:ptr指向str,而str不是const,可以直接通过str变量来修改str的值,但是确不能通过ptr指针来修改。
来段C++
#include
using namespace std;
int main()
{
int i;
char str[ ] = “hello world”;
//char ss[] = “good game!!”;
const char *ptr = str;
for(int i = 0;i<11;i++)
{
cout<<ptr[i];
}
cout<<endl;
ptr[0] = ‘s’;
str[0] = ‘g’;
//ptr = ss;
for(int i = 0;i<11;i++)
{
cout<<ptr[i];
}
cout<<endl;
}
gcc编译报错信息:
注释掉ptr[0] = ‘s’;运行正常,运行结果为:
hello world
gello world
另外还可以通过重新赋值给该指针来修改指针指向的值,如上代码中取消
//char ss[] = “good game!!”;
//ptr = ss;
运行结果为:
hello world
good game!!
二、char const *ptr;
此种写法和const char *等价,定义一个指向字符常量的指针
三,char * const ptr;
定义一个指向字符的指针常数,即const指针
#include <iostream>
using namespace std;
int main()
{
int i;
char str[] = "hello world";
char ss[] = "good game!!";
char *const ptr = str;
for(int i = 0;i<11;i++)
{
cout<<ptr[i];
}
cout<<endl;
ptr[0] = 's';
ptr = ss;
for(int i = 0;i<11;i++)
{
cout<<ptr[i];
}
cout<<endl;
}
gcc报错信息:

注释掉 ptr = ss;代码运行正常,运行结果为:
hello world
sello world
#### 个人总结:
const char *ptr==char const *ptr;可以直接改变指针指向,但不能直接改变指针指向的值;*ptr=*ss;
char *const ptr; 可以直接改变指针指向的值,但不能直接改变指针指向;ptr[0]='s';
但两者都可以通过改变所指向的指针的内容,来改变它的值。
```cpp
#include <iostream>
using namespace std;
int main()
{
char str[] = "hello world";
char sec[] = "code world";
const char *ptr1 =str;//定义一个指向字符常量的指针
cout <<ptr1<< endl;
strcpy(str,"hi world");//对str进行赋值,正常
cout <<ptr1<<endl;
ptr1 = sec;//直接改变指针指向
cout<<ptr1<<endl;
sec[0] = 'o';
cout<<ptr1<<endl;
ptr1[0] = 'a';//直接改变指针指向的值,报错
char ss[] = "good game";
char *const ptr2 = ss;//定义一个指向字符的指针常数,即const指针
cout<<ptr2<<endl;
ptr2[0]='a';//直接改变指针指向的值
cout<<ptr2<<endl;
strcpy(ptr2,"last");
cout<<ptr2<<endl;
ss[0]='z';
cout<<ptr2<<endl;
ptr2 = sec;//直接改变指针指向,报错
systeam("pause");
}
继续
项目代码usart
函数名:USART1_printf
void USART1_Printf(USART_TypeDef* USARTx,unit8_t *Data,...)
{
const char* s;
int d;
char buf[16];
va_list ap;
//(称为ap)具有va_list类型。变量ap可以被传递为另一个函数的参数;注意:va_list是一种数组类型,这样当对象为该类型时作为参数传递,它通过引用传递
va_start(ap, Data);
//参数ap指向具有va_list类型的对象va_start宏初始化ap,以便va_arg和后续使用va_end。参数parmN是最右边参数的标识符在函数定义的变量参数列表中在“…”)。如果参数parmN是用寄存器声明的存储类给出一个错误;如果parmN是一个窄类型(char, short, float),则给出一个错误严格的ANSI模式,否则警告返回:没有价值
while (*Data !=0)// 判断是否到达字符串结束符
{
if(*Data == 0x5c)//‘\’ASCLL码,92;16进制是0x52
{
switch (*++Data)
{
case 'r'://回车符
USART_SendData(USARTx,0x0d);//10进制为13,D上传回车键
Data ++;
break;
case 'a'://换行符
USART_SendData(USARTx,0x0a);
Data ++;
break;
default:
Data ++;
break;
}
}
else if(*Data == '%')
{
switch (*++Data)
{
case 's'://字符串
s = va_arg(ap, const char *);
//宏扩展为类型和值为的表达式调用中的下一个参数。参数ap应与
//va_list ap由va_start初始化。每次调用va_arg都会进行修改
//ap,以便依次返回连续的参数。的参数
//“类型”是一个类型名称,使指向对象的指针的类型使指定的类型可以简单地通过后缀*获得吗“类型”。如果类型是窄类型,则在严格ANSI中给出一个错误模式,否则将发出警告。如果类型是数组或函数类型,给出了一个错误。
//在非严格的ANSI模式下,‘type’允许是任何表达式。返回:在调用宏之后的第一次va_arg调用va_start宏返回后面的参数值由parmN规定。的值剩下的论据接二连三。结果被强制转换为“type”,即使“type”很窄。
for ( ;*s;s++)
{
USART_SendData(USARTx,*s);
while(USRAT_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);//等待发送缓冲区空才能发送下一个字符
}
Data++;
break;
case 'd'://十进制
d = va_arg(ap, int);
itoa(d, buf, 10);
//将整形数据转换成字符串,d要转换的整形数buf 转换后的字符串10 表示10进制
for(s = buf;*s;s++)
{
USART_SendData(USARTx,*s);
while(USART_GetFlagStatus(USARTx,USART_FLAG_TC) == RESET);
}
Data++;
break;
default:
Data++;
break;
}
}
else USART_SendData(USARTx, *Data++);
while( USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET );
//在每一个字符发送后检测状态位《串口USART收发》第一个字符丢失问题
}
}
void USART1_Send_Byte(unsigned char byte)//串口发送一个字节
{
USART_SendData(USART1, byte);//通过库函数 发送数据
while( USART_GetFlagStatus(USART1,USART_FLAG_TC)!= SET);
//等待发送完成。 检测 USART_FLAG_TC 是否置1;
}
/*串口3发送函数*/
void USART3_Send_Byte(unsigned char byte)//串口发送一个字节
{
USART_SendData(USART3,byte);//通过库函数 发送数据
while( USART_GetFlagStatus(USART3,USART_FLAG_TC)!= SET);
//等待发送完成。 检测 USART_FLAG_TC 是否置1;
}
void UART3_Send_Char(char *s)
{
char *p;
p=s;
while(*p !='\0')
{
USART3_Send_Byte(*p);
p++;
}
}
//输出字符串
void PrintChar(char *s)
{
char *p;
p=s;
while(*p != '\0')
{
USART1_Send_Byte(*p);
p++;
}
}
va_list 的定义
#ifdef ___TARGET_ARCH_AARCH64
typedef struct _va_list
{
void *__stack;
void *__gr_top;
void *__vr_top;
int __gr_offs;
int __vr_offs;
}va_list;
#else
typedef struct __va_list { void *__ap; } va_list;
#endif
项目代码motor
#include "stm32f10x.h"
#include "motor.h"
#include "stm32f10x_exti.h"
#define ENCODER_TIM_PERIOD (u16)0xFFFF
#define MAX_COUNT (u16)0x0FFF
int leftcount;
int rightcount;
/控制GPIO PB12/PB13/PB14/PB15
电机MOTOR TIM2_CH3/TIM2_CH4
光电编码器 TIM3_CH1/TIM3_CH2
光电编码器 TIM4_CH1/TIM4_CH2/
void MOTOR_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*定义一个GPIO_InitTypeDef类型的结构体*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
/*开启GPIOB的外设时钟*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
/*选择要控制的GPIOB引脚*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*设置引脚速率为50MHz */
GPIO_Init(GPIOB,&GPIO_InitStructure);//初始化
GPIO_SetBits(GPIOB, GPIO_Pin_13 | GPIO_Pin_14|GPIO_Pin_12 | GPIO_Pin_15);
}
static void TIM2_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//PCLK1经过2倍频后作为TIM2的时钟源等于72MHz
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//使能定时器2的时钟定时器2是挂载APB1总线上的
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能总线APB2上的A组GPIO时钟,我们用的就是A组上的GPIO。
//GPIOA配置:TIM2通道3和4作为备选推拉功能
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3;//选通A组GPIO上的2通道和3通道,(这个是根据你硬件的设计来选通的)。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
TIM2配置:产生4种不同占空比的PWM信号:TIM3CLK = 72 MHz,分频器= 0x0, TIM3计数器时钟= 72 MHz TIM3 ARR寄存器= 71999 =>TIM3频率= TIM3计数器时钟/(ARR + 1) TIM3频率= 1khz。CC1更新率= TIM2计数器时钟/ CCR1_Val TIM3信道占空比= (TIM2_CCRx/ TIM2_ARR)* 100 = x
* 函数名:TIM2_Mode_Config
* 描述 :配置TIM3输出的PWM信号的模式,如周期、极性、占空比
static void TIM2_Mode_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
//基本定时器TIM2的定时配置的结构体(包含定时器配置的所有元素例如:TIM_Period = 计数值)
/* PWM信号电平跳变值 */
u16 CCR3_Val = 0;
u16 CCR4_Val = 0;
/* PWM信号电平跳变值 */
//u16 CCR1_Val = 500;
//u16 CCR2_Val = 800;
//时间基本配置
TIM_TimeBaseStructure.TIM_Period = 999;//ARR 当定时器从0计数到999,即为1000次,为一个定时周期
//前面我们把计数设置1000-1分频为72-1时周期是2ms
TIM_TimeBaseStructure.TIM_Prescaler = 0;//设置预分频:不预分频,即为72MHz
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟分频系数:不分频;定时器时钟预分频设置为1,也就是不分频,直接使用相应总线频率。
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);//输出比较pWM通道
//PWM2模式配置:Channel3
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//配置定时器为PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//比较输出使能,开启OC*输出对应引脚
TIM_OCInitStructure.TIM_Pulse = CCR3_Val;
//设置跳变值,当计数器计数到这个值时,电平发生跳变;设置通道3的电平跳变值,输出另外一个占空比的PWM ,相当于说计数周期是1000,在计数还没到0的时候输出高电平,在计数0-1000时是低电平
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//当定时器计数值小于CCR1_Val时为高电平
TIM_OC3Init(TIM2,&TIM_OCInitStructure); //使能通道2
TIM_OC3PreloadConfig(TIM2,TIM_OCPreload_Enable);//开启OC1重装,计数器计数满后自动重装
//PWM1模式配置:Channel4
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//开启OC*输出对应引脚
TIM_OCInitStructure.TIM_Pulse = CCR4_Val;//设置通道2的电平跳变值,输出另外一个占空比的PWM
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//当定时器计数值小于CCR1_Val时为高电平
TIM_OC4Init(TIM2, &TIM_OCInitStructure);//使能通道3
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);//开启OC1重装,计数器计数满后自动重装
TIM_ARRPreloadConfig(TIM2, ENABLE);// 使能TIM3重载寄存器ARR
TIM_Cmd(TIM2, ENABLE); //使能定时器2
}
void TIM2_PWM_CHANGE(u16 CCR3,u16 CCR4)
{
TIM_OCInitTypeDef TIM_OCInitStructure;
u16 CCR3_Val;
u16 CCR4_Val;
CCR3_Val = CCR3;
CCR4_Val = CCR4;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;//配置为PWM模式1
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = CCR3_Val;//设置跳变值,当计数器计数到这个值时,电平发生跳变
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;//当定时器计数值小于CCR1_Val时为高电平
TIM_OC3Init(TIM2,&TIM_OCInitStructure);//使能通道2
TIM_OC3PreloadConfig(TIM2,TIM_OCPreload_Enable);//开启OC1重装,计数器计数满后自动重装
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = CCR4_Val;//设置通道2的电平跳变值,输出另外一个占空比的PWM
TIM_OC4Init(TIM2,&TIM_OCInitStructure);//使能通道3
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);
}
void TIM2_PWM_Init(void)
{
TIM2_GPIO_Config();
TIM2_Mode_Config();
}
void TIM3_IRQHandler(void)
{
u16 tsr;
tsr = TIM3->SR;
if(tsr&0X0001)//溢出中断
{
}
TIM3->SR&=~(1<<0);//清除中断标志位
}
void TIM4_IRQHandler(void)
{
u16 tsr;
tsr=TIM4->SR;
if(tsr&0X0001)//溢出中断
{
}
TIM4->SR&=~(1<<0);//清除中断标志位
}
void TIM3_Encoder_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
//编码器单元连接到tim3,4x模式
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_StructInit(&GPIO_InitStructure);
GPIO_InitStructur.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
启用TIM3更新中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//优先级组别
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//定时器配置在编码器模式
TIM_DeInit(TIM3);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0;//没有分频
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;//65535
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟分频系数:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数模式模式
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_BothEdge, TIM_ICPolarity_BothEdge);
//上升沿捕获
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_ICFilter = 6;//无滤波器
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//清除所有挂起的中断
TIM_ClearFlag(TIM3, TIM_FLAG_Update);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); //使能中断
TIM_Cmd(TIM3, ENABLE); //使能定时器3
}
void TIM4_Encoder_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
//TIM4时钟源启用
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_StructInit(&GPIO_InitStructure);
//配置pa .06,07作为编码器输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 启用TIM3更新中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//优先级组别
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd =ENABLE;
NVIC_Init(&NVIC_InitStructure);
//定时器配置在编码器模式
TIM_DeInit(TIM4);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Prescaler = 0x0;//无预分频
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;//65535
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟分频系数:不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_BothEdge, TIM_ICPolarity_BothEdge); //TIM_ICPolarity_Rising上升沿捕获
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_ICFilter = 6; //无滤波器
TIM_ICInit(TIM4, &TIM_ICInitStructure);
//清除所有挂起的中断
TIM_ClearFlag(TIM4, TIM_FLAG_Update);
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //使能中断
//复位计数器
//TIM4->CNT = 0;
TIM_Cmd(TIM4, ENABLE); //使能定时器3
}
TIM2配置:产生4种不同占空比的PWM信号:TIM3CLK = 72 MHz,分频器= 0x0, TIM3计数器时钟= 72 MHz TIM3 ARR寄存器= 71999 =>TIM3频率= TIM3计数器时钟/(ARR + 1) TIM3频率= 1khz。CC1更新率= TIM2计数器时钟/ CCR1_Val TIM3信道占空比= (TIM2_CCRx/ TIM2_ARR)* 100 = x
PWM功能(定时器、占空比、初始化)
脉冲宽度调制,简称脉宽调制。脉冲宽度调制(pwm)是一种对模拟信号电平进行数字编码的方法。电压或电流是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用PWM进行编码。
输出电压=(接通时间/脉冲时间)*最大电压值
怎么配置才能是我们想要的定时时间呢
假设
系统时钟是72Mhz,TIM1 是由PCLK2 (72MHz)得到,TIM2-7是由 PCLK1 得到
关键是设定时钟预分频数,自动重装载寄存器周期的值
按照上面的设置每2ms发生一次更新事件(进入中断服务函数)
RCC_Configuration()的SystemInit()的 RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2表明TIM2CLK为72MHz。
因此,每次进入中断服务程序间隔时间为
((1+TIM_Prescaler )/72M)*(1+TIM_Period )=((1+(72-1))/72)*(1+999)=2000约为2ms
TIME2我们用来产生舵机PWM波,既然PWM波已经产生了,那我们怎么样才能去驱动舵机让他转任意 角度呢?
驱动舵机必须要用50HZ的PWM方波驱动,1/50=20ms的周期的方波.
当给舵机一个0.5ms的脉宽(就是0.5ms的高电平)时舵机转到-90°,
当给舵机一个1ms的脉宽(就是1ms的高电平)时舵机转到-45°,
当给舵机一个1.5ms的脉宽(就是1.5ms的高电平)时舵机转到0°,
当给舵机一个2ms的脉宽(就是2ms的高电平)时舵机转到45°,
当给舵机一个2.5ms的脉宽(就是2.5ms的高电平)时舵机转到+90°
当我们给定一个0.5ms~2.5ms之间的一个任意脉宽时必然会对应舵机一个-90 ~+90°之间的一个角度,我们只要把这个脉宽和角度对应起来,就能在程序里控制舵机的任意角度。
前面我们把计数设置1000-1分频为72-1时周期是2ms
那么转-90°时需要计0.5/20*1000=25个数,需要25个脉冲初始化在-90度
再来看上面
我们设置的电平跳变值是0,在定时器初始化后舵机就自然转到-90°
要想控制任意舵机角度,只要给这个语句赋值不同的值就OK。TIM_Pulse
看看
void TIM2_PWM_CHANGE(u16 CCR3,u16 CCR4)
我们仅利用TIM3的CH2产生一路PWM输出。
要使STM32的通用定时器TIMx产生PWM输出,我们会用到3个寄存器,来控制PWM的。这三个寄存器分别是:捕获/比较模拟寄存器(TIMx_CCMR1/2),捕获/比较使能寄存器(TIMx_CCER),捕获/比较寄存器(TIMx_CCR1~4)还有个TIMx的ARR寄存器是用来控制pwm的输出频率。
首先是捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器总共有2个,TIMx_CCMR1和TIMx_CCMR2。TIMx_CCMR1控制CH1和2,而TIMx_CCMR2控制CH3和CH4.
其次是捕获比较使能寄存器(TIMx_CCER),该寄存器控制着各个输入输出通道开关。
最后是捕获/比较寄存器(TIMx_CCR1~4),该寄存器总共有4个,对应4个输出通道CH1 ~4.这个寄存器用来设置PWM的占空比的。
void PWM_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
//GPIOB使用的RCC时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO,ENABLE);
//配置使用的GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
TIM_TimeBaseStructure.TIM_Period = 100-1; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 80K
(._Period这个即是设置TIMx_ARR的值,即用来设置pwm的频率)
TIM_TimeBaseStructure.TIM_Prescaler =0;//设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = 0;//设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//TIM向上计数模式
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);//根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
输出比较活动模式配置:Channel1
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//比较输出使能
TIM_OCInitStructure.TIM_Pulse = 0;//设置待装入捕获比较寄存器的脉冲值,设置占空比
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC3Init(TIM4,&TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
TIM_OC3PreloadConfig(TIM4,TIM_OCPreload_Enable);//使能TIMx在CCR3上的预装载寄存器
TIM_ARRPreloadConfig(TIM4, ENABLE);//使能TIMx在ARR上的预装载寄存器
TIM_Cmd(TIM4, ENABLE); //使能TIMx外设
}
#### `那么TIMx_ARR寄存器的值是怎样来确定pwm的频率的呢?`
TIM_Period(即是TIMx_ARR寄存器的值) 的大小实际上表示的是需要经过TIM_Period次计算和中断才发现一次更新和中断接下来需要设置时钟预分频数TIM_Prescaler,这里有一个公式,我们举例来说明:例如时钟频率=72MHZ/(时钟预分频+1)。(假设72MHZ为系统运行的频率,这里的时钟频率即是产生这个pwm的时钟的频率)说明当前设置的这个TIM_Prescaler,直接决定定时器的时钟频率。`通俗点说,就是一秒钟能计数多少次。比如算出来的时钟频率是2000,也就是
一秒钟会计数2000 次,而此时如果TIM_Period 设置为4000,即4000 次计数后就会中断一次。
由于时钟频率是一秒钟计数2000 次,因此只要2 秒钟,就会中断一次。还有一个需要注意的,就是我们一般采用向上计数模式。`
接下来,就是占空比的配置了
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式
已经选择定时器为pwm1,所以下面直接给TIMx_CCRx(捕获比较寄存器)赋值就可以了。在pwm1模式下,TIMx_CCRx的值越大,占空比就越大。TIMx_CCRx寄存器,确定PWM的占空比。TIMx_CCR1—TIMx_CCR4确定定时器的CH1—CH4四路PWM的占空比。直接给该寄存器赋0—65535值即可确定占空比。`占空比计算方法:TIMx_CCRx的值除以ARR寄存器的值即为占空比,因为占空比在0-100%之间,所以一般TIMx_CCRx寄存器值不能超过ARR寄存器的值,否则可能会引起PWM的频率或占空比的准确性。`
下面就是我自己写的并理解的程序了,只是简单的输出占空比
下面就是我自己写的并理解的程序了,只是简单的输出占空比
#include "stm32f10x.h"
void gpio_init(void);///tim3的ch1,在pa6上面
void TIM2_init(void);
int main (void)
{
gpio_init();
TIM2_Iint();
while(1);
}
void gpio_init()
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockcmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitStructure.GPIO_Pin = 6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM2_init();
}
void TIM2_init()
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//PWM频率 = 72000000 / 4 / 1000 = 18Khz
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; //PWM计数上限
TIM_TimeBaseStructure.TIM_Prescaler = 4 - 1; //设置用来作为TIM2时钟频率除数的预分频值,4分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0;//设置时钟分割:TDTS = Tck_tim/
//一般我们在设置时钟分割时,都将其设置为0(采样频率为定时器输入频率),
//实际上也就是TIM_CKD_DIV1
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIMx向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseStructure中指定的参数初始化外设TIM2
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = 200; //设置待装入捕获比较寄存器的脉冲值
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); //使能TIM2在CCR1上的预装载寄存器
TIM_ARRPreloadConfig(TIM3, ENABLE); //使能TIM2在ARR上的预装载寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIM2外设
}
固定的pwm波,实验证实,这里不需要中断函数,定时器会自动重装数值,32真是强大在这里啊。定时器自带BGM.
定时器配置中时钟分割
STM32的定时器配置中的时钟分割,是与定时器的输入捕获功能和滤波单元相关的。不同的参数代表以不同的采样频率对输入信号进行采样,当连续采样到 N(滤波带宽) 次个有效电平时,认为一次有效的输入电平。(但是这个采样频率只是一个基准频率,实际中的采样频率是可以设置的,我们根据程序所设置这个实际的采样频率与定时器通道的滤波单元有关,下面的内容中会细说这一部分)。
解释说明:
1.首先要知道这三种不同的采样频率基准是通过设置控制寄存器TIMx_CR1
中的CKD(时钟分频因子)
来实现的,手册上有详细的说明:
一般我们在设置时钟分割时,都将其设置为0(采样频率为定时器输入频率),实际上也就是TIM_CKD_DIV1:
2.关于定时器通道的滤波单元
定时器输入通道都有一个滤波单元,分别位于每个输入通路上和外部触发输入通路上,它们的作用是滤除输入信号上的高频干扰。
那么滤除高频干扰的功能是如何实现的呢?
举个例子:
检测一次输入信号为高电平,假设采样频率是18MHZ,滤波带宽是6,那么我们需要以18MHZ的采样频率,在连续6个采样周期内检测到高电平,才可判断输入信号为高电平
也就是说采样信号的高电平持续时间至少为6个采样周期(1/3MHZ),相对应的采样信号的频率最大为3MHZ,从而滤去了频率高于3MHZ的信号。
再举一个例子:
假如要监测一个占空比为50%,频率为1MHZ的方波信号,且利用滤波功能,滤去高频信号干扰(假设采样频率仍为18MHZ),那么我们的滤波带宽应该是多少呢? N<=9
仍然可以利用检测高电平的思路来理解:占空比为50%,频率为1MHZ的方波信号,它的高电平持续时间为1/2MHZ,那么连续采样的时间就是1/2MHZ,也就是9个采样周期,且最多为9个采样周期。
图上可见:假如采样脉宽大于9,那么就监测不到该方波信号了。
上面的时钟分割设置中我们设置了采样频率基准,而实际中的采样频率和滤波带宽是可以设置的:外部触发输入通道的滤波参数在从模式控制寄存器(TIMx_SMCR)的ETF[3:0]中设置;每个输入通道的滤波参数在捕获/比较模式寄存器1(TIMx_CCMR1)或捕获/比较模式寄存器2(TIMx_CCMR2)的IC1F[3:0]、IC2F[3:0]、IC3F[3:0]和IC4F[3:0]中设置。
在这里插入图片描述
从上面的图中我们也可以看出:实际采样频率与采样频率基准有关,故改变采样频率基准可改变滤波的频率。
关于“数字滤波器是一个事件计数器,它记录到N个事件后会产生一个输出的跳变。”这句话的说明:
事件是什么?
数字滤波器的一个事件就是对输入信号的一次采样结果
输出的跳变是什么?
输出的跳变就是一个脉冲。
比如要求检测一个输入的上升沿,当选择ETF[3:0]=0011时,以频率fSAMPLING=fCK_INT对输入信号采样,如果在监测到一个上升沿(连续2次采样的结果为一低一高)后,连续8个采样周期都检测到高电平,则数字滤波器输出一个脉冲,否则不输出脉冲。
应用:结合输入捕获的中断,可实现按键的去抖动功能,每个定时器最多可以实现4个按键的输入,这个方法也可以用于键盘矩阵的扫描,而且因为是通过中断实现,软件不需频繁的进行扫描动作。
一个通用定时器(TIM2)产生4路频率相同(1KHz)占空比不同的PWM。Ch1占空比75%,Ch2占空比50%,Ch3占空比25%,Ch4占空比10%。四路输出分别对应PA端口的PA0,PA1,PA2,PA3。
定时器的时钟不是直接来自APB1或APB2,而是来自于输入为APB1或APB2的一个倍频器。
以定时器2~7的时钟说明这个倍频器的作用(TIM2-TIM7则挂在APB1总线上,APB1最大是36MHz。)
当APB1的预分频系数为1时,这个倍频器不起作用,定时器的时钟频率等于APB1的频率;当APB1的预分频系数为其它数值(即预分频系数为2、4、8或16)时,这个倍频器起作用,定时器的时钟频率等于APB1的频率两倍。
假定AHB=36MHz,因为APB1允许的最大频率为36MHz,所以APB1的预分频系数可以取任意数值;当预分频系数=1时,APB1=36MHz,TIM27的时钟频率=36MHz(倍频器不起作用);当预分频系数=2时,APB1=18MHz,在倍频器的作用下,TIM27的时钟频率=36MHz。
既然需要TIM2~7的时钟频率=36MHz,为什么不直接取APB1的预分频系数=1?
答:APB1不但要为TIM2~7提供时钟,而且还要为其它外设提供时钟;设置这个倍频器可以在保证其它外设使用较低时钟频率时,TIM2 ~ 7仍能得到较高的时钟频率。
/*使用一个定时器产生4路不同占空比的PWM波型 */
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);//打开定时器时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
//定时器2 设置 360分频 1KHz 向上计数
TIM_TimeBaseStructure.TIM_Period = 199;//1KHz
//f=Tck/(psc+1)*(arr+1)=72/(359+1)*(199+1)=0.001MHZ=1khz
//预分频为360所以Tck=72HZ
TIM_TimeBaseStructure.TIM_Prescaler = 359;//360分频
TIM_TimeBaseStructure.TIM_ClockDivision = 0x00; //时钟分频系数,不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure); //初始化定时器
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //主定时器TIM2为PWM1模式
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//使能输出比较状态
TIM_OCInitStructure.TIM_OCPolarity =TIM_OCPolarity_High;
//ch1 配置项
TIM_OCInitStructure.TIM_Pulse = 150;
TIM_OC1Init(TIM2,&TIM_OCInitStructure);//ch1占空比75%
TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);//使能的预装载寄存器
//ch2 配置项
TIM_OCInitStructure.TIM_Pulse = 100;
TIM_OC2Init(TIM2, & TIM_OCInitStructure); //ch2占空比50%
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); //使能的预装载寄存器
//ch3 配置项
TIM_OCInitStructure.TIM_Pulse = 25;
TIM_OC3Init(TIM2, &TIM_OCInitStructure); //ch3占空比25%
TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable); //使能的预装载寄存器
//ch4 配置项
TIM_OCInitStructure.TIM_Pulse = 10;
TIM_OC4Init(TIM2, & TIM_OCInitStructure);//ch4占空比10%
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable); //使能的预装载寄存器
TIM_ARRPreloadConfig(TIM2,ENABLE);//使能TIM2在ARR上的预装载寄存器
TIM_ITConfig(TIM2,TIM_IT_CC1,ENABLE);//开定时器中断
TIM_CtrlPWMOutputs(TIM2,ENABLE); //使能TIM2的外设的主输出
TIM_Cmd(TIM2,ENABLE); //使能定时器2
}
通道1,2
通道3,4
对定时器周期公式的理解:
T=(arr+1)*(PSC+1)/Tck 其中TCK为时钟频率,PSC为时钟预分频系数,arr为自动重装载值
f=Tck/((psc+1)*(arr+1))
Tck/(psc+1)即为时钟频率,1/f为机器周期,除以(arr+1)即可得出定时器周期。
例子:TCK=72MHZ,psc=71.时钟周期=1us.(arr+1)值为多少,定时器周期就为多少毫秒。
TIMx_CCR1—TIMx_CCR4(捕获/比较寄存器)确定定时器的CH1—CH4四路PWM的占空比。
直接给该寄存器赋0—65535值即可确定占空比。TIM_OCInitStructure.TIM_Pulse ,0—65535的值
占空比计算方法:TIMx_CCRx的值除以ARR寄存器的值即为占空比,
因为占空比在0—100%之间,所以一般TIMx_CCRx寄存器值不能超过ARR寄存器的值,否则可能会引起PWM的频率或占空比的准确性。
正交编码器模式
编码器分类:
按工作原理:光电式、磁电式和触点电刷式
差分编码器:一般由8根线连接 信号线分别为 A+ A- B+ B- Z+ Z- 以及VCC和GND
正交编码器:一般是5根线连接,信号线分别为A B Z VCC和GND
编码器线数: 就是旋转一圈你的A(B)会输出多少个脉冲 ,这里的A B就是上面的输出脉冲信号线,它们转一圈发出的脉冲数一样的,不过存在90°相位差 通常都是360线的 线数越高代表编码器能够反应的位置精度越高
相位差为90° 通过判断哪个信号在前 哪个信号在后 来决定TIM->COUNT是++ 还是 –
360线 AB一圈各为360个,Z信号为一圈一个
编码器信号:
A 脉冲输出
B 脉冲输出
Z 零点信号 当编码器旋转到零点时,Z信号会发出一个脉冲表示现在是零位置 这个零点位置是固定,厂商指定的
VCC 电源通常分为24V的和5V的
GND 地线
1.这里的正交编码器是如果是24V的工作电压还需要用光耦隔离,24V转为3V3在接到STM32的定时器两个通道上
2.脉冲输出是OC门输出,需要上拉电阻
3.Z信号接到STM32的外部中断口上,很容易受到干扰 ,通常需要接一个电容到GND
硬件连接(这里使用的STM32F103ZET6的TIM4的CH1和CH2):
PB6–A
PB7–B
PA1–Z
#include "stm32f10x.h"
#include "encode.h"
#include "misc.h"
#include "nvic.h"
#include "sys.h"
#include "delay.h"
void TIM4_Mode_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
//PB6 ch1 A,PB7 ch2
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);//使能TIM4时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能GPIOA时钟
GPIO_StructInit(&GPIO_InitStructure);//将GPIO_InitStruct中的参数按缺省值输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 |GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//PA6 PA7浮空输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
NVIC_Config(2);
TIM_DeInit(TIM4);
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
TIM_TimeBaseStructure.TIM_Period = 359*4;//设定计数器重装值 TIMx_ARR = 359*4
TIM_TimeBaseStructure.TIM_Prescaler = 0;//TIM3时钟预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;//设置时钟分割 T_dts = T_ck_int
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;//TIM向上计数
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_BothEdge ,TIM_ICPolarity_BothEdge);//使用编码器模式3,上升下降都计数
TIM_ICStructInit(&TIM_ICInitStructure);//将结构体中的内容缺省输入
TIM_ICInitStructure.TIM_ICFilter = 6;//选择输入比较滤波器
TIM_ICInit(TIM4, &TIM_ICInitStructure);//将TIM_ICInitStructure中的指定参数初始化TIM3
TIM_ClearFlag(TIM4, TIM_FLAG_Update);//清除TIM3的更新标志位
TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE);//运行更新中断
TIM4->CNT = 0;//复位计数器
TIM_Cmd(TIM4, ENABLE);//启动TIM4定时器
}
void TIM3_Mode_Config(void)
{
RCC->APB1ENR|=1<<1; //TIM3时钟使能
RCC->APB2ENR|=1<<2; //使能PORTA时钟
GPIOA->CRL&=0XF0FFFFFF;//PA6
GPIOA->CRL|=0X04000000;//浮空输入
GPIOA->CRL&=0X0FFFFFFF;//PA7
GPIOA->CRL|=0X40000000;//浮空输入
//这两个东东要同时设置才可以使用中断
TIM3->DIER|=1<<0; //允许更新中断
TIM3->DIER|=1<<6; //允许触发中断
TIM3_NVIC_Config();
TIM3->PSC = 0x0;//预分频器
TIM3->ARR = 15-1;//设定计数器自动重装值
TIM3->CR1 &=~(3<<8);// 选择时钟分频:不分频
TIM3->CR1 &=~(3<<5);// 选择计数模式:边沿对齐模式
TIM3->CCMR1 |= 1<<0; //CC1S='01' IC1FP1映射到TI1
TIM3->CCMR1 |= 1<<8; //CC2S='01' IC2FP2映射到TI2
TIM3->CCER &= ~(1<<1); //CC1P='0' IC1FP1不反相,IC1FP1=TI1
TIM3->CCER &= ~(1<<5); //CC2P='0' IC2FP2不反相,IC2FP2=TI2
TIM3->CCMR1 |= 3<<4; // IC1F='1000' 输入捕获1滤波器
TIM3->SMCR |= 3<<0; //SMS='011' 所有的输入均在上升沿和下降沿有效
TIM3->CNT = 0;
TIM3->CR1 |= 0x01; //CEN=1,使能定时器
}
void TIM4_Init(void)
{
TIM4_Mode_Config();
}
1.选择哪个定时器 即TIM4
2.编码器模式有三种 见下图
3.TIM_IC1的极性
4.TIM_IC2的极性
这里设置的是编码器模式3,且TI1和TI2都是双边沿触发–即上下边沿都计数
1.有效边沿 其实就是对应上面设置的编码器的三种模式
2.相对信号的电平,这里没有理解手册意思,我把它理解为于它的高低电平意味着将PB6和PB7接口对换,PB7接A PB6接B 这样一来就意味着原来的正转变成反转 计数上升变为下降
TIx 就相当于输入信号的 TIM4->CH1 TIM4->CH2
TIxF 滤波后信号
TIxFPx经过带极性选择的边缘检测器过后的产生的信号
至于TI1FP1和TI2FP2信号在上身沿计数还是下降沿计数受两点影响 极性(是否反向) 边缘检测(上升沿还是下降沿)
我们这里设置的是不反向 在双边沿计数,即在A上升下降 B的上身下降都计数
而计数为什么是x4倍 ,下图结合上面的配置详细说明了
至于读取编码器角度的时间,要根据实际需要来设置
编码器线数为 w线/圈
转速为 V 圈/min
读取间隔时间 t(线间隔时间)
t <= 60/WV 单位为秒
还有Z信号归零,在遇到Z信号的时候,将定时器的CNT=0,这样就能保证位置与CNT实际对应上了
中断代码如下
//外部中断1,编码器Z相归零 优先级--① 0 0
void EXTI1_IRQHandler(void)
{
TIM4->CNT = 0;//每次遇到相对零(Z信号)就将计数归0
TIM_Cmd(TIM4, ENABLE);
EXTI_ClearITPendingBit(EXTI_Line1);
}
//编码器接口模式 优先级--2 1 1
void TIM4_IRQHandler(void)
{
if(TIM4->SR&0x0001)//溢出中断
{
;
}
TIM4->SR&=~(1<<0);//清除中断标志位
}
TIM_TimeBaseInitTypeDef
:时基单位,配置定时器预分频参数,计数器模式(上溢/下溢),周期频率以及分频系数;
TIM_OCInitTypeDef
:振荡输出单元,可以用于产生PWM波形;
TIM_ICInitTypeDef
:输入捕获单元,可以用于检测编码器信号的输入;
TIM_BDTRInitTypeDef
:适用于TIM1和TIM8作为插入死区时间配置的结构体;
编码器接口大概需要进行以下几项的配置:
编码器接口模式的配置:
1.上升沿触发
2.下降沿触发
3.跳变沿触发
4.极性配置
5.滤波器配置
以下是官方给出的配置方案:
CC1S=’01’ (TIMx_CCMR1寄存器, IC1FP1映射到TI1)
CC2S=’01’ (TIMx_CCMR2寄存器, IC2FP2映射到TI2)
CC1P=’0’ (TIMx_CCER寄存器, IC1FP1不反相, IC1FP1=TI1)
CC2P=’0’ (TIMx_CCER寄存器, IC2FP2不反相, IC2FP2=TI2)
SMS=’011’ (TIMx_SMCR寄存器,所有的输入均在上升沿和下降沿有效).
CEN=’1’ (TIMx_CR1寄存器,计数器使能)
这意味着计数器TIMx_CNT寄存器只在0到TIMx_ARR寄存器的自动装载值之间连续计数(根据方向,或是0到ARR计数,或是ARR到0计数)。
检测方法
综上所述,如果想得到转速,和方向:
1.在间隔固定时间Ts,读取TIMx_CNT寄存器的值,假设是1000线的编码器,转速:
n = 1/TsTIMx_CNT1000;
2.根据TIMx_CNT的计数方向判断转向,不同极性,TIMx_CNT增长方向也不同,这里要加以区分;
TIM3通道1,Pin6和Pin7;
机械复位信号;
可以通过encoder_get_signal_cnt
接口读取当前编码的脉冲数,采用M
法测速;
关于计数器溢出的情况
TIM3_IRQHandler
中断通过判断SR
寄存器中的上溢和下溢标志位,检测定时器可能溢出的方向,通过N
做一个补偿,encoder_get_signal_cnt
中未考虑到定时器溢出的情况;
#ifndef ENCODER_H
#define ENCODER_H
#include <stdint.h>
typedef enum{
FORWARD = 0,
BACK
}MOTO_DIR;
brief init编码器pin for pha phb和零
和interrpts
void encoder_init(void);
//获取编码器捕获信号计数
int32_t encoder_get_signal_cnt(void);
//获取编码器运行方向
MOTO_DIR encoder_get_motor_dir(void);
#endif
#include "encoder.h"
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_tim.h"
#include "stm32f10x_exti.h"
#include "misc.h"
#define SAMPLE_FRQ 10000L
#define SYS_FRQ 72000000L
volatile int32_t N = 0;
volatile uint32_t EncCnt = 0;
static void encoder_pin_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//IPU是指IO口的工作模式是带上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
static void encoder_rcc_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//afio时钟打开
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//定时器3打开
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
}
static void encoder_tim_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_TimeBaseStructure.TIM_Period = ENCODER_MAX_CNT;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStructure.TIM_Prescaler = 0;
TIM_TimeBaseStructure.TIM_ClockDivision =0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStructure);
TIM_EncoderInterfaceConfig(TIM3,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);//TIM_ICPolarity_Rising上升沿捕获
//必须在启用中断前清除它标志
TIM_ClearFlag(TIM3,TIM_FLAG_Update);
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
TIM_SetCounter(TIM3,ENCODER_ZERO_VAL);
TIM_ICInit(TIM3,&TIM_ICInitStructure);
TIM_Cmd(TIM3,ENABLE);
//配置嵌套矢量中断控制器
static void encoder_irq_init(void)
{
NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitTypeDef这个结构体我们在misc.h文件中可以找到
EXTI_InitTypeDef EXTI_InitStructure;这个结构体在stm32f10x_exti.h中有定义
//启用TIM3全局中断
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;//需要配置的中断向量
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;//配置相应中断向量抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//配置相应中断响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能或关闭响应中断向量中断
NVIC_Init(&NVIC_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource5);
EXTI_InitStructure.EXTI_Line = EXTI_Line5;//外部中断5
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//配置为中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;//上升沿中断
EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd =ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void encoder_init(void)
{
encoder_rcc_init();
encoder_pin_init();
encoder_irq_init();
encoder_tim_init();
}
// 机械复位信号
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line5) == SET)
{
}
EXTI_ClearITPendingBit(EXTI_Line5);
}
MOTO_DIR encoder_get_motor_dir(void)
{
if((TIM3->CR1 & 0x0010) == 0x0010)
{
return FORWARD;
}
else
{
return BACK;
}
}
int32_t encoder_get_signal_cnt(void)
{
int32_t cnt = 0;
if(TIM3->CNT > ENCODER_ZERO_VAL)
{
EncCnt = cnt = TIM3->CNT - ENCODER_ZERO_VAL;
}
else
{
EncCnt = cnt = ENCODER_ZERO_VAL - TIM3->CNT;
}
TIM_SetCounter(TIM3,ENCODER_ZERO_VAL);
return cnt;
}
//这个函数处理TIM3全局中断请求
void TIM3_IRQHandler(void)
{
uint16_t flag = 0x0001 << 4;
if(TIM3->SR&(TIM_FLAG_Update))
{
if((TIM3->CR1 & flag) == flag)
{
N--;
}
else
{
N++;
}
}
TIM3->SR&=~(TIM_FLAG_Update);//再次开始计数
}
//TIM3全局中断请求
void TIM3_IRQHandler(void)
{
uint16_t flag = 0x0001 << 4;
if(TIM3->SR&(TIM_FLAG_Update))//计数到这个值
{
if((TIM3->CR1 & flag) == flag)
{
N--;
}
else
{
N++;
}
}
TIM3->SR&=~(TIM_FLAG_Update);//再次开始计数
}
}
对于中端配置主要的内容是配置中断优先级,stm32有两种中断优先级,我们该怎么配置呢?
中断向量有两个属性,抢占式优先级和响应式优先级,编号越小,优先级越高。当两个中断抢占式优先级相同,则响应式中断优先级高的先执行。
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx更新中断标志
清除中断标志位,用TIM_FLAG_Update也是
通过TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)和TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG)的定义,可以知道,它们都是作用在TIMx->SR寄存器上的,所以这两个值是一样的。至于为什么取两个不同的名字,应该是为了方便阅读和理解代码吧。
任务:用TIM做一个us级别的延时。
路子1:做一个很小的定时,每1us就中断一次,你现在如果要延时200us那么我就做这个动作200次!
路子2:做一个稍大的定时,每1000us中断一次,如果要求延时200us那么我就截取其中一部分时间段给你,用计数值控制。
显然2比较好
现在是说一说路子1的模块。
头文件:
#ifndef _TIM4ER_H
#define _TIM4ER_H
#include "stm32f10x_tim.h"
#include "stm32f10x_rcc.h"
//一些调用函数
void Do_TIM4_Init(void);
void TIMDelay_Nus(int Times);
void TIMDelay_Nms(int Times);
#endif
//一些基本配置
#include "TIM4er.h"
#define TIM4Period 8
#define TIM4Prescaler 9
void Do_TIM4_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
TIM_TimeBaseStruct.TIM_Period = TIM4Period;
TIM_TimeBaseStruct.TIM_Prescaler = TIM4Prescaler;
TIM_TimeBaseStruct.TIM_ClockDivision = 0;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM4,&TIM_TimeBaseStruct);
TIM_ARRPreloadConfig(TIM4,ENABLE);
}
void TIMDelay_Nus(int Times)
{
TIM4->CR1 |=TIM_CR1_CEN;//打开TIM
while(Times--)
{
while((TIM4->SR & TIM_FLAG_Update)==RESET);//定时器到了溢出
TIM4->SR &= ~TIM_FLAG_Update;//再次开始计数
}
TIM4->CR1 &=~TIM_CR1_CEN;//关闭TIM
}
void TIMDelay_Nms(int Times)
{
while(Times--)
{
TIMDelay_Nus(1000);
}
}
现在讨论:一次溢出就是1us如何做到的?
72M 除以两个数 TIM4Period TIM4Prescaler
72000000/(9*8)=1 000 000HZ
测试结果 1us误差比较大
0xff & ~(1<<0),0xff & ~(1<<1)这两个值是多少?
0xff & ~(1<<1)的计算过程:
1 的二进制是
0000 0000 0000 0001
<<1 左移1位变成
0000 0000 0000 0010
~ 按位取反变成
1111 1111 1111 1101
0xff 的二进制是 0000 0000 1111 1111
按位“与”运算
0000 0000 1111 1111
&
1111 1111 1111 1101
=0000 0000 1111 1101
0000 0000 1111 1101 的十进制是 253
问题:TIM3->SR&=~(1<<0);//清除中断标志位
1<<0
0000 0000 0000 0001
~ 按位取反变成
1111 1111 1111 1110
TIM3->SR&1111 1111 1111 1110=//清除中断标志位
中断函数
在STM32中执行中断主要分三部分:
1.配置NVIC_Config()函数
2.配置EXTI_Config()函数
3.编写中断服务函数
配置NVIC_Config()函数
NVIC 是嵌套向量中断控制器,控制着整个芯片中断相关的功能,它跟内核紧密耦合,是内核里面的一个外设。
NVIC_Config()函数代码如下:
static void NVIC_Config(void)/* 主要是配置中断源的优先级与打开使能中断通道 */
{
NVIC_InitTypeDef NVIC_InitStruct;
/* 配置中断优先级分组(设置抢占优先级和子优先级的分配),在函数在misc.c */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/* 配置初始化结构体 在misc.h中 */
/* 配置中断源 在stm32f10x.h中 */
NVIC_InitStruct.NVIC_IRQChannel = KEY1_EXTI_IRQN;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;/* 配置抢占优先级 */
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;/* 配置子优先级 */
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE ;/* 使能中断通道 */
NVIC_Init(&NVIC_InitStruct) ;/* 调用初始化函数 */
/* 对key2执行相同操作 */
NVIC_InitStruct.NVIC_IRQChannel = KEY2_EXTI_IRQN ;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE ;
NVIC_Init(&NVIC_InitStruct) ;
}
配置NVIC_Config()的目的是选择中断源的优先级以及打开中断通道
,主要功能通过配置NVIC初始化结构体NVIC_InitStruct来完成。通俗的讲,STM32中有很多中断,而当有多个中断同时发生时就涉及到中断执行的先后问题了,所以引入了中断优先级的概念,中断优先级越高中断就越先执行。在这里我们只讨论外部中断的优先级,在 NVIC 有一个专门的寄存器:中断优先级寄存器 NVIC_IPRx,用来配置外部中断的优先级。优先级高低的比较包括抢占优先级和子优先级,先比较抢占优先级,如果抢占优先级相同就比较子优先级,从而得出中断之间的优先级高低。NVIC的主要任务就是给对应的中断源分配中断优先级。 中断优先级分配的原理繁杂,但固件库编程的好处就是化繁为简,我们只需要按照NVIC_InitStruct()中的内容进行配置就行。
接下来简单讲解一下NVIC_Config()函数的内容:
1.首先设置中断优先级分组
中断优先级分组其实是确立一个大纲,中断优先级寄存器 NVIC_IPRx中有4个位用来确定优先级,中断优先级的分组就是把这4个位分配在抢占优先级和子优先级中。比如设定一个位配置抢占优先级,其余三个位配置子优先级。通过函数NVIC_PriorityGroupConfig() ; 实现分组,详细代码如下:
配置中断优先级分组:抢占优先级和子优先级
形参如下:
NVIC_PriorityGroup_0:
0bit for 抢占优先级
4 bits for 子优先级
NVIC_PriorityGroup_1:
1 bit for 抢占优先级
3 bits for 子优先级
NVIC_PriorityGroup_2:
2 bit for
2 bits for 子优先级
NVIC_PriorityGroup_3:
3 bit for 抢占优先级
1 bits for 子优先级
NVIC_PriorityGroup_4:
4 bit for 抢占优先级
0 bits for 子优先级
如果优先级分组为 0,则抢占优先级就不存在,优先级就全部由子优先级控制
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
// 设置优先级分组
SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}
优先级分组完毕后,是配置NVIC初始化结构体
typedef struct
{
uint8_t NVIC_IRQChannel; // 中断源
uint8_t NVIC_IRQChannelPreemptionPriority; // 抢占优先级
uint8_t NVIC_IRQChannelSubPriority; // 子优先级
FunctionalState NVIC_IRQChannelCmd; // 中断使能或者失能
}NVIC_InitTypeDef;
初始化结构体的作用是,收集中断源的信息(包括配置的是哪一个中断源、中断源的抢占优先级是多少、中断源的子优先级是多少、中断源的使能是否开启)。
NVIC_IRQChannel:用来设置中断源,不同的中断中断源不一样,且不可写错,即使写错了程序也不会报错,只会导致不响应中断。stm32f10x.h 头文件里面的 IRQn_Type 结构体定义,这个结构体包含了所有的中断源。
NVIC_IRQChannelPreemptionPriority和NVIC_IRQChannelSubPriority 分别设置抢占优先级和子优先级,具体的值要根据中断优先级分组来确定。
NVIC_IRQChannelCmd:设置中断使能(ENABLE)或者失能(DISABLE),相当于一个电源总开关。
最后借助NVIC初始化函数将NVIC初始化结构体中的信息写入相应的寄存器中 (体现了固件库编程的优点,不需要我们深入到寄存器层次去,只需要掌握相应函数的配置即可)
配置EXTI_Config()函数
EXTI:外部中断/事件控制器,管理了控制器的 20个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI 可以实现对每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。
按我的理解,EXTI是一个有着多达20个接口的控制器,它可以为每一个接入接口的信号源配置中断(或事件)线、设置信号的检测方式、设置触发事件的性质,也就是说传入EXTI的仅仅是一个信号,EXTI的功能就是根据信号传入的“线”对信号做出相应的处理,然后将处理后的信号转向NVIC。
就像一个分拣机器,传入的东西经过筛选处理被送往不同的地方,只是EXTI分拣的是信号罢了。如果说NVIC是配置中断源,那么EXTI就是向NVIC传送中断信号。
EXTI功能框图:
两大部分功能,一个是产生中断,另一个是产生事件,线路1-2-4-5是产生中断的流程,20/代表着有20条相同的线路。
EXTI_Config()函数代码:
void EXTI_Config() /* 主要是连接EXTI与GPIO */
{
GPIO_InitTypeDef GPIO_InitStruct ;
EXTI_InitTypeDef EXTI_InitStruct ;
NVIC_Config();
/* 初始化要与EXTI连接的GPIO */
/* 开启GPIOA与GPIOC的时钟 */
RCC_APB2PeriphClockCmd(KEY1_EXTI_GPIO_CLK |KEY2_EXTI_GPIO_CLK,ENABLE);
GPIO_InitStruct.GPIO_Pin = KEY1_EXTI_GPIO_PIN ;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING ;
GPIO_Init(KEY1_EXTI_GPIO_PORT , &GPIO_InitStruct) ;
GPIO_InitStruct.GPIO_Pin = KEY2_EXTI_GPIO_PIN ;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING ;
GPIO_Init(KEY2_EXTI_GPIO_PORT , &GPIO_InitStruct) ;
/* 初始化EXTI外设 */
/* EXTI的时钟要设置AFIO寄存器 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE) ;
/* 选择作为EXTI线的GPIO引脚 */
GPIO_EXTILineConfig( KEY1_GPIO_PORTSOURCE , KEY1_GPIO_PINSOURCE) ;
/* 配置中断or事件线 */
EXTI_InitStruct.EXTI_Line = KEY1_EXTI_LINE ;
/* 使能EXTI线 */
EXTI_InitStruct.EXTI_LineCmd = ENABLE ;
/* 配置模式:中断or事件 */
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt ;
/* 配置边沿触发 上升or下降 */
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising ;
EXTI_Init(&EXTI_InitStruct) ;
GPIO_EXTILineConfig( KEY2_GPIO_PORTSOURCE , KEY2_GPIO_PINSOURCE) ;
EXTI_InitStruct.EXTI_Line = KEY2_EXTI_LINE ;
EXTI_InitStruct.EXTI_LineCmd = ENABLE ;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt ;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling ;
EXTI_Init(&EXTI_InitStruct);
}
代码可大体分为三部分:
配置GPIO相应引脚、配置EXTI并连接GPIO引脚、传入NVIC_Config()
·1.配置GPIO相应引脚·
该代码是通过按键产生一个电平信号,然后经EXTI处理传入NVIC产生中断的,所以要配置连接按键的GPIO引脚,主要是设置相应的引脚模式为浮空输入 。老规矩,先开启相应GPIO的时钟,然后配置引脚初始化结构体,再利用初始化函数将初始化结构体写入寄存器中。
2.配置EXTI并连接GPIO引脚
要操作外设,首先要打开相关的时钟,EXTI挂载在APB2总线上,并且开启时钟时要操作AFIO寄存器
,准备工作就绪后连接GPIO相应的引脚到EXTI中,前面说了EXTI有20个接口,所以特定的引脚有特定的接口,所以要根据GPIO_EXTILineConfig();函数选择用作EXTI线的GPIO引脚,函数说明如下
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
{
uint32_t tmp = 0x00;
//检查参数
assert_param(IS_GPIO_EXTI_PORT_SOURCE(GPIO_PortSource));
assert_param(IS_GPIO_PIN_SOURCE(GPIO_PinSource));
tmp = ((uint32_t)0x0F) << (0x04 * (GPIO_PinSource & (uint8_t)0x03));
AFIO->EXTICR[GPIO_PinSource >> 0x02] &= ~tmp;
AFIO->EXTICR[GPIO_PinSource >> 0x02] |= (((uint32_t)GPIO_PortSource) << (0x04 * (GPIO_PinSource & (uint8_t)0x03)));
}
其实对应的EXTI线就对应GPIO引脚号
,这样看起来还比较直观。
连接好GPIO引脚与EXTI后就该配置EXTI的初始化结构体了,结构体如下:
typedef struct
{
uint32_t EXTI_Line;// 中断/事件线
EXTIMode_TypeDef EXTI_Mode;// EXTI 模式
EXTITrigger_TypeDef EXTI_Trigger;// 触发类型
FunctionalState EXTI_LineCmd;// EXTI 使能
}EXTI_InitTypeDef;
配置此结构体主要是:选择相应的EXTI线 、选择触发模式、选择产生的结果(中断还是事件)、是否使能EXTI线。
EXTI_Line:中断线选择,可选 EXTI_0 至 EXTI_19(一共20个)。既然刚才配置好了与GPIO引脚对应的EXTI线,所以初始化结构体中的EXTI线就是与GPIO连接的那个线。
EXTI_Mode:EXTI 模式选择,可选为产生中断或者产生事件。就是决定信号的发展方向,是产生中断呢?还是产生事件呢?此处是中断。
EXTI_Trigger: EXTI 边沿触发模式,可选上升沿触发、下降 沿 触 发 或 者 上 升 沿 和 下 降 沿 都 触 发。触发信号。
EXTI_LineCmd:控制是否使能 EXTI 线,可选使能 EXTI 线或禁用。
初始化结构体配置完毕后交由初始化函数写入相应的寄存器中。
传入NVIC_Config()
之后就自动传入NVIC中了。。。
编写中断服务函数
,每个中断都有其固定的名字,只有找到这个名字,在这个固定的函数名下编写中断服务函数才是有效的,所有中断函数的编写都要在stm32f10x_it.c 中
EXTI线0到EXTI线4线都是单独的中断函数名、EXTI线5到EXTI线9共用一个中断函数名、EXTI线10线到EXTI线15线共用一个中断函数名.
我们要做的就是以相应的EXTI线的中断函数名字在stm32f10x_it.c中编写中断函数
如下:
void EXTI0_IRQHandler(void)
{
if( EXTI_GetITStatus(KEY1_EXTI_LINE)!=RESET)
{
LED1_TOGGLE; //LED1的亮灭状态反转
}
EXTI_ClearITPendingBit(KEY1_EXTI_LINE);
}
void EXTI15_10_IRQHandler(void)
{
if( EXTI_GetITStatus(KEY2_EXTI_LINE)!=RESET)
{
LED2_TOGGLE; //LED2的亮灭状态反转
}
EXTI_ClearITPendingBit(KEY2_EXTI_LINE);
}
每次进入中断函数后,靠ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)读取中断是否执行
,执行完之后要利用void EXTI_ClearITPendingBit(uint32_t EXTI_Line)清除清除中断标志位,以免不断进入中断
到此完整的中断系统就已经完成,主函数只需调用即可
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_key.h"
int main(void)
{
LED_GPIO_Config();
EXTI_Config();
while(1)
{
}
}
#ifndef __BSP_KEY_H
#define __BSP_KEY_H
#include "stm32f10x.h"
#define KEY1_EXTI_GPIO_CLK RCC_APB2Periph_GPIOA
#define KEY1_EXTI_GPIO_PORT GPIOA
#define KEY1_EXTI_GPIO_PIN GPIO_Pin_0
#define KEY1_EXTI_IRQN EXTI0_IRQn /* 对应着引脚号 */
#define KEY1_EXTI_LINE EXTI_Line0 /* 中断、事件线对应引脚号 */
#define KEY1_GPIO_PORTSOURCE GPIO_PortSourceGPIOA
#define KEY1_GPIO_PINSOURCE GPIO_PinSource0
#define KEY1_EXTI_IRQHANDLER EXTI0_IRQHandler
#define KEY2_EXTI_GPIO_CLK RCC_APB2Periph_GPIOC
#define KEY2_EXTI_GPIO_PORT GPIOC
#define KEY2_EXTI_GPIO_PIN GPIO_Pin_13
#define KEY2_EXTI_IRQN EXTI15_10_IRQn
#define KEY2_EXTI_LINE EXTI_Line13
#define KEY2_GPIO_PORTSOURCE GPIO_PortSourceGPIOC
#define KEY2_GPIO_PINSOURCE GPIO_PinSource13
#define KEY2_EXTI_IRQHANDLER EXTI15_10_IRQHandler
void EXTI_Config(void);
#endif
#ifndef __BSP_LED_H
#define __BSP_LED_H
#include "stm32f10x.h"
#define LED1_GPIO_CLK RCC_APB2Periph_GPIOC /*时钟*/
#define LED1_GPIO_PORT GPIOC /*端口*/
#define LED1_GPIO_PIN GPIO_Pin_2 /*引脚*/
#define LED2_GPIO_PIN GPIO_Pin_3
#define LED2_GPIO_CLK RCC_APB2Periph_GPIOC
#define LED2_GPIO_PORT GPIOC
#define digitalTOGGLE(p,i) {p->ODR ^=i;}
#define LED1_TOGGLE digitalTOGGLE(LED1_GPIO_PORT,LED1_GPIO_PIN)
#define LED2_TOGGLE digitalTOGGLE(LED2_GPIO_PORT,LED2_GPIO_PIN) /* LED状态反转 */
void LED_GPIO_Config(void);
#endif
发出响应的从设备必须将SDA拉低并在时钟脉冲的高电平期间保持住。这表示该设备给出一个ACK.如果它不拉低SDA线,就表示不响应(NACK)
主机发送数据到从机的状态下:主机控制SCL信号线和SDA信号线,从机只是在SCL线为高的时候去被动读取SDA
项目代码I2C
PB8 - I2C1_SCL
PB9 - I2C1_SDA
#include "I2C.h"
#define SCL_H GPIOB->BSRR = GPIO_Pin_8//((uint16_t)0x0100)
//设置对应的ODRy位为1 管脚对于位写1 gpio 管脚为高电平
#define SCL_L GPIOB->BRR = GPIO_Pin_8
//BRR 只写寄存器:只能改变管脚状态为低电平,对寄存器 管脚对于位写 1 相应管脚会为低电平。
#define SDA_H GPIOB->BSRR = GPIO_Pin_9
#define SDA_L GPIOB->BRR = GPIO_Pin_9
#define SCL_read GPIOB->IDR & GPIO_Pin_8//
#define SDA_read GPIOB->IDR & GPIO_Pin_9
static void I2C_delay(void)
{
volatile int i = 7;
while(i)
{
}
i--;
}
static bool I2C_Start(void)
{
SDA_H;
SCL_H;
I2C_delay();
if(!SDA_read)
return false;
SDA_L;
I2C_delay();
if(SDA_read)
return false;
SDA_L;
I2C_delay();
return true;
}
static void I2C_Stop(void)
{
SCL_L;
I2C_delay();
SDA_L;
I2C_delay();
SCL_H;
I2C_delay();
SDA_H;
I2C_delay();
}
//产生 ACK 应答
//时钟数据,低低 时钟高时钟低
static void I2C_Ack(void)
{
SCL_L;//GPIOB->BRR = GPIO_Pin_8
I2C_delay();
SDA_L;
I2C_delay();
SCL_H;
I2C_delay();
SCL_L;
I2C_delay();
}
//不产生 ACK 应答
//低高 时钟高时钟低
static void I2C_NoAck(void)
{
SCL_L;
I2C_delay();
SDA_H;
I2C_delay();
SCL_H;
I2C_delay();
SCL_L;
I2C_delay();
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
static bool I2C_WaitAck(void)
{
SCL_L;GPIOB->BRR = GPIO_Pin_8
I2C_delay();
SDA_H;//GPIOB->BSRR = GPIO_Pin_8//((uint16_t)0x0100)
I2C_delay();
SCL_H;
I2C_delay();
if (SDA_read)//SDA_read ------GPIOB->IDR & GPIO_Pin_9
{
SCL_L;
return false;//false=0
}
SCL_L;
return true;
}
//IIC 发送一个字节
//返回从机有无应答
//1,有应答
//0,无应
static void I2C_SendByte(uint8_t byte)
{
uint8_t i = 8;
while (i--)
{
SCL_L;//拉低时钟开始数据传输
I2C_delay();
if(byte & 0x80)
SDA_H;
else
SDA_L;
byte <<= 1;
I2C_delay();
SCL_H;
I2C_delay();
}
SCL_L;
}
static uint8_t I2C_ReceiveByte(void)
{
uint8_t i = 8;
uint8_t byte = 0;
SDA_H;
while (i--)
{
byte <<=1;
SCL_L;
I2C_delay();
SCL_H;
I2C_delay();
if(SDA_read)
{
byte |= 0x01;
}
}
SCL_L;
return byte;
}
void i2cInit(void)
{
GPIO_InitTypeDef gpio;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
gpio.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
gpio.GPIO_Speed = GPIO_Speed_2MHz;
gpio.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_Init(GPIOB, &gpio);
}
bool i2cWriteBuffer(uint8_t addr,uint8_t reg,uint8_t len,uint8_t * data)
{
int i;
if (!I2C_Start())
return false;
I2C_SendByte(addr << 1 | I2C_Direction_Transmitter);
if(!I2C_WaitAck())
{
I2C_Stop();
return false;
}
I2C_SendByte(reg);
I2C_WaitAck();
for (i = 0; i < len; i++)
{
I2C_SendByte(data[i]);
if (!I2C_WaitAck())
{
I2C_Stop();
return false;
}
}
I2C_Stop();
return true;
}
int8_t i2cwrite(uint8_t addr,uint8_t reg,uint8_t len,uint8_t *data)
{
if(i2cWriteBuffer(addr,reg,len,data))
{
return TRUE;
}
else
{
return FALSE;
}
int8_t i2cread(uint8_t addr,uint8_t reg,uint8_t len, uint8_t *buf)
{
if(i2cRead(addr,reg,len,buf))
{
return TRUE;
}
else
{
return FALSE;
}
}
}
bool i2cWrite(uint8_t addr, uint8_t reg, uint8_t data)
{
if (!I2C_Start())
return false;
I2C_SendByte(addr << 1 | I2C_Direction_Transmitter);
if (!I2C_WaitAck())
{
I2C_Stop();
return false;
}
I2C_SendByte(reg);
I2C_WaitAck();
I2C_SendByte(data);
I2C_WaitAck();
I2C_Stop();
return true;
}
bool i2cRead(uint8_t addr, uint8_t reg, uint8_t len, uint8_t *buf)
{
if (!I2C_Start())
return false;
I2C_SendByte(addr << 1 | I2C_Direction_Transmitter);
if (!I2C_WaitAck())
{
I2C_Stop();
return false;
}
I2C_SendByte(reg);
I2C_WaitAck();
I2C_Start();
I2C_SendByte(addr << 1 | I2C_Direction_Receiver);
I2C_WaitAck();
while (len)
{
*buf = I2C_ReceiveByte();
if (len == 1)
I2C_NoAck();
else
I2C_Ack();
buf++;
len--;
}
I2C_Stop();
return true;
}
uint16_t i2cGetErrorCounter(void)
{
// TODO maybe fix this, but since this is test code, doesn't matter.
return 0;
}
GPIO 配置之ODR, BSRR, BRR
ODR寄存器可读可写:既能控制管脚为高电平,也能控制管脚为低电平。
管脚对于位写1 gpio 管脚为高电平,写 0 为低电平
BSRR 只写寄存器:[color=Red]既能控制管脚为高电平,也能控制管脚为低电平。
对寄存器高 16bit 写1 对应管脚为低电平,对寄存器低16bit写1对应管脚为高电平。写 0 ,无动作
BRR 只写寄存器:只能改变管脚状态为低电平,对寄存器 管脚对于位写 1 相应管脚会为低电平。写 0 无动作。
1.既然ODR 能控制管脚高低电平为什么还需要BSRR和SRR寄存器?
你用BSRR和BRR去改变管脚状态的时候,没有被中断打断的风险。也就不需要关闭中断。
用ODR操作GPIO的伪代码如下:
disable_irq()
save_gpio_pin_sate = read_gpio_pin_state();
save_gpio_pin_sate = xxxx;
chang_gpio_pin_state(save_gpio_pin_sate);
enable_irq();
关闭中断明显会延迟或丢失一事件的捕获,所以控制GPIO的状态最好还是用SBRR和BRR
2.既然BSRR能实现BRR的全部功能,为什么还需要SRR寄存器?
因为BSRR的 低 16bsts 恰好是set操作,而高16bit是 reset 操作 而BRR 低 16bits 是reset 操作。
简单地说GPIOx_BSRR的高16位称作清除寄存器,而GPIOx_BSRR的低16位称作设置寄存器。
另一个寄存器GPIOx_BRR只有低16位有效,与GPIOx_BSRR的高16位具有相同功能。
举个例子说明如何使用这两个寄存器和所体现的优势。
例如GPIOE的16个IO都被设置成输出,而每次操作仅需要改变低8位的数据而保持高8位不变,假设新的8位数据在变量Newdata中,这个要求可以通过操作这两个寄存器实现,STM32的固件库中有两个函数GPIO_SetBits()和GPIO_ResetBits()使用了这两个寄存器操作端口。
上述要求可以这样实现:
GPIO_SetBits(GPIOE, Newdata & 0xff);
GPIO_ResetBits(GPIOE, (~Newdata & 0xff));
也可以直接操作这两个寄存器:
GPIOE->BSRR = Newdata & 0xff;
GPIOE->BRR = ~Newdata & 0xff;
当然还可以一次完成对8位的操作
GPIOE->BSRR = (Newdata & 0xff) | ( (~Newdata & 0xff)<<16 );
当然还可以一次完成对16位的操作:
GPIOE->BSRR = (Newdata & 0xffff) | ( (~Newdata )<<16 );
有人问是否BSRR的高16位是多余的,请看下面这个例子:
假如你想在一个操作中对GPIOE的位7置’1’,位6置’0’,则使用BSRR非常方便:
如果没有BSRR的高16位,则要分2次操作,结果造成位7和位6的变化不同步!
GPIOE->BSRR = 0x80;
GPIOE->BRR = 0x40;
BSRR还有一个特点,就是Set比Reset的级别高,
I2C通信时序图解析
起始信号:当 SCL 线是高电平时 SDA 线从高电平向低电平切换。
停止信号:当 SCL 线是高电平时 SDA 线由低电平向高电平切换
起始信号
void I2C_Start(void)
{
I2C_SDA_High(); //SDA=1
I2C_SCL_High(); //SCL=1
I2C_Delay();
I2C_SDA_Low();
I2C_Delay();
I2C_SCL_Low();
I2C_Delay();
}
停止信号代码
void I2C_Stop(void)
{
I2C_SDA_Low();
I2C_SCL_High();
I2C_Delay();
I2C_SDA_High();
I2C_Delay();
}
发送一个字节
CPU向I2C总线设备发送一个字节(8bit)数据
u8 I2C_SendByte(uint8_t Byte)
{
uint8_t i;//1字节
/* 先发送高位字节 */
for(i = 0 ; i < 8 ; i++)
{
if(Byte & 0x80)
{
I2C_SDA_High();
}
else
{
I2C_SDA_Low();
}
I2C_Delay();
I2C_SCL_High();
I2C_Delay();
I2C_SCL_Low();
I2C_Delay();
if(i == 7)
{
I2C_SDA_High(); /* 释放SDA总线 */
}
Byte <<= 1; /* 左移一位 */
I2C_Delay();
}
}
字节格式
SDA数据线上的每个字节必须是8位,每次传输的字节数量没有限制。每个字节后必须跟一个响应位(ACK)。
首先传输的数据是最高位(MSB),SDA上的数据必须在SCL高电平周期时保持稳定,数据的高低电平翻转变化发生在SCL低电平时期。
以传输Byte:1010 1010 (0xAAh)为例,SDA SCL传输时序如下所示:
2.数据位传输
SCL时钟电平为低, 可以改换SDA数据线的电平,在SCL上升沿的过程将SDA数据发送出去。
(切记:请先将SCL变为低电平,再改变SDA电平状态。 主要用于I2C读写Byte函数)
发送一位“高”数据流程:
SCL_LOW时钟低 -> SDA_HIGH数据 -> SCL_HIGH时钟高
3.应答位信息
I2C 是以字节(8位)的方式进行传输,总线上每传输完1字节之后会有一个应答信号,主器件(主机)需要产生对应的一个额外时钟。
应答位产生及接收:
1.在(主机)写数据的时候是从机应答(给主机),主机检测;
2.在(主机)读数据的时候是主机应答(给从机),从机检测;
时序图(主机写,从机应答,主机读取应答):
时序图(主机读,主机产生应答):
I2C写一字节
主机往从机接入1Byte的数据;
“写”要求按照上面的“数据为传输”来操作:在SCL时钟为低电平时准备好,待SCL为高电平时发送出去。
写完一字节(8位)之后,读取从机的应答位:
若为0,表示从机应答,可以继续下一步操作;
若为1,表示从机非应答,不能进行下一步操作。
注意
I2C写一字节不是EEPROM写一字节(需要区分开来)
写一字节时序(前面8位数据 + 最后1为应答):
I2C写数据(网上常见几种不规范写法 - 或许整个I2C驱动能通信成功,但各个函数之间依赖关系很强,不便理解,也不是标准的函数):
1.首先将SCL置高:
void I2C_WriteByte(uint8_t Data)
{
uint8_t cnt;
for(cnt=0; cnt<8; cnt++)
{
I2C_SCL_HIGH;
if(Data & 0x80)
{
I2C_SDA_HIGH;
}
else
{
I2C_SDA_LOW;
}
Data <<= 1;
I2C_SCL_LOW;
}
I2C_GetAck();
}
这种程序的写法有一个致命的地方(有可能停止,或重新开始I2C通信):
首先将SCL置高:
A.若之前SDA是低电平,第一位写入高电平,将停止I2C通信。
B.若之前SDA是高电平,第一位写入低电平,将重新开始I2C通信。
2.写完8位数据之后,未将SCL置低(也就是SCL保持高电平状态):
由于写完8位数据之后,将要读取应答信号,也就是要SDA将从输出状态变为输入状态。
这个时候SCL为高,如果SDA最后一位是低且SDA是开漏模式,需要将SDA释放,也就是要将SDA置位高,那么,这个时候就进行了一个停止操作。
3.时序混乱:
void I2C_WriteByte(uint8_t Data)
{
uint8_t cnt;
I2C_SCL_HIGH;
for(cnt=0; cnt<8; cnt++)
{
if(Data & 0x80)
{
I2C_SDA_HIGH;
}
else
{
I2C_SDA_LOW;
}
Data <<= 1;
I2C_SCL_LOW;
I2C_SCL_HIGH;
}
I2C_GetAck();
}
多种问题的例子,有可能产生以下问题:
A.有可能多写1位数据;
B.有可能停止I2C通信;
C.有可能重新开始I2C通信。
5.I2C读一字节
I2C的读一字节函数,其实和“写一字节”类似,只是数据传输方向相反,应答的方向也是相反。
读完一字节(8位)之后,由主机产生应答(或非应答)位:
若产生应答,表示可以继续读下一字节操作(从设备地址指向下一字节);
若产生非应答,表示不可以继续读下一字节操作;
读一字节时序(主机读取前面8位数据 + 主机产生1为非应答<连续读,主机产生应答位>):
6.EEPROM读(或写)一字节数据需要I2C多次通信过程,下面将讲述几个重要的内容:
设备(从机、器件)地址
I2C的开始信号之后的第一步就是发送设备物理地址, AT24Cxx的物理地址的格式如上面:
前面四位固定为:1010
第567位对应A2 A1 A0(有些器件未使用)
第8位是读/写位。
一个设备一般是接地,这就是为什么我们看到A0这个宏定义的来由。
数据地址长度
有些芯片数据地址只有8位(如:AT24C01、AT24C02),那么它只发送一字节地址即可;有些芯片有16位地址,它需要发送两字节地址(看下面读写函数)。
注意两个地方:
1.设备地址更加需要看你看引脚的情况;
2.数据地址长度根据芯片不同而不同。
IIC中的应答和非应答
应答:是一个低电平信号。
非应答:是一个高电平信号,也许,叫做应答非更合适。
IIC协议规定,当主机作为接收设备时,主机对最后一个字节不应答,以向发送设备(从设备)标识数据传输结束。这是因为每次传输都应答信号后再进行下一个字节传送。如果此时接收机应答了,那它就接收的不是最后一个字节了。如果是最后一个字节,第9个时钟周期发送的是非应答信号(此时发送的不是应答信号就是非应答信号),最后发送停止信号。
并非每传输8位数据之后,都会有ACK信号,有以下3中例外
1.当从机不能响应从机地址时(例如它正忙于其他事而无法响应IIC总线的操作,或者这个地址没有对应的从机),在第9个SCL周期内SDA线没有拉低,即没有ACK信号。这时,主机发出一个P信号终止传输或者重新发出一个S信号开始新的传输。
2.如果从机接收器在传输过程中不能接收更多的数据时,它不会发出ACK信号。这样,主机就可以意识到这点,从而发出一个P信号终止传输或者重新发出一个S信号开始新的传输。
3.主机接收器在接收到最后一个字节后,也不会发出ACK信号。于是,从机发送器释放SDA线,以允许主机发出P信号结束传输。
IIC协议规定:SDA上传输的数据必须在SCL为高电平期间保持稳定,SDA上的数据只能在SCL为低电平期间变化。IIC期间在脉冲上升沿把数据放到SDA上,在脉冲下降沿从SAD上读取数据。这样的话,在SCL高电平期间,SDA上的数据是稳定的。在脉冲下降沿之后的保持时间以后,SDA上的数据可以变化,直到脉冲上升沿之前。
i2c信号的NACK
我们平时在调试I2C的时候可能很少去关注NACK信号,只知道如果Master发送数据,MSB先发,LSB后发,连续发送一个字节(8个bit),之后Slave会回复一个ACK信号,但是有时I2C slave可能会发出NACK信号,下面让我们来看看NACK信号存在的情况。
每个字节后会跟随一个ACK信号。ACK bit使得接收者通知发送者已经成功接收数据并准备接收下一个数据。所有的时钟脉冲包括ACK信号对应的时钟脉冲都是由master产生的
ACK信号:发送者在ACK时钟脉冲期间释放SDA线,接收者可以将SDA拉低并在时钟信号为高时保持低电平。
NACK信号:当在第9个时钟脉冲的时候SDA线保持高电平,就被定义为NACK信号。Master要么产生STOP条件来放弃这次传输,或者重复START条件来发起一个新的开始。
实例:
可以看到如下波形,Master发送01101100(0x6c,MSB先发),在第9个时钟的时候SDA为高电平,表示Slave发送了NACK信号,之后整个I2C通信就结束了。这是一次失败的I2C通信,原因可能是I2C设备那边出的问题,或者访问I2C设备的地址与I2C设备实际的地址不对应,导致没接收到Master的数据从而返回NACK。
下面我拿一个OV8825 Sensor的I2C来说明
OV8825的Slave Write Address为0x6c,OV8825的ID register Address为0x300a,0x300b,ID register里面存的Value是0x88,0x25
正常的I2C波形如下:
1)设定I2C写的地址:01101100(0x6c) 00110000(0x30) 00001010(0x0a)
Slave Write Address:0x6c,ID register address:0x300a
2)设定I2C读的地址:01101101(0x6d) 10001000(0x88)
Slave Read Address:0x6d,ID register value:0x88
看到这里有点奇怪,i2c write是以ack+stop结束通信,而i2c read是以nack+stop结束通信的,原因如下:
i2c write的时候,master在写完最后一个字节之后slave会回ACK,然后master发送stop信号结束通信
i2c read的时候,master在接收完slave发送的最后一个字节之后会回NAK,因为这个时候master已经接收到足够的字节,NAK告诉slave不要在发送数据了。
项目代码ADC
#include "adc.h"
#include "delay.h"
#include "stm32f10x_adc.h"
//初始化ADC
//这里我们仅以规则通道为例
//我们默认将开启通道0~3
void Adc_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1,ENABLE);
//使能ADC1通道时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
//PA1 作为模拟通道输入引脚
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; //模拟输入引脚
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_DeInit(ADC1);//复位ADC1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE;//模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;/模数转换工作在单次转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1;//顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure);//根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_ResetCalibration(ADC1); //使能复位校准
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
ADC_StartCalibration(ADC1); //开启AD校准
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
// ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
}
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)
{
//设置指定ADC的规则组通道,一个序列,采样时间
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}