一、简介
(一)、学习的知识
下文将讲解如何使用STM32F103C8T6最小芯片板,完成一个简单的机械臂制作。使用STM32标准库进行编程,将学习到的主要内容会有:①电源选择与电路接线②定时器③PWM波控制舵机④PSP摇杆与AD通信⑤继电器的使用⑥串口通信⑦数学建模思想
相信通过这个实例会对你学习STM32有整体上的帮助。但是要深入学习还是要学习其他系统性的课程,比如:江协科技的STM32入门教程
注意: ①零基础的同学观看会很吃力,建议只跟着写代码,对许多名词不了解时先跳过
②代码部分是基于江协科技 “ 4-1 OLED显示屏 ”原文件来写的
代码原文件:获取链接 (建议配合原码食用)
(二)、将学习/使用的软件
①Keil5 ②Visual Studio 2022 ③串口助手
(三)、对机械臂的简单介绍
通过这一个小实践可以学习到的东西还是很多的,适合初学者的学习
要实现的功能:
①使用遥控控制机械臂
②通过串口通信控制同时也能使用遥控控制
③实现电磁铁拿取棋子(棋子带铁片)
下面是机械臂的预览
(四)、零配件购买参考
下面将列出机械臂使用了的主要零器件(串口转USB、ST-Link 不用说大家都有吧)
名称 | 数量 | 备注 |
---|---|---|
必备耗材、杜邦线、面包板等 | 略 | |
舵机MG90S | x1 | |
舵机MG996 | x2 | |
STM32C8T6最小系统板 | x1 | |
电磁铁 | x1 | 买工作电压12v的 |
PS2摇杆 | x1 | |
DC电源模块3.3V 5V 12V多路输出![]() | x1 | 因为既要12V给电磁铁,又要输出5V给舵机 |
12v锂电池 | x1 | |
继电器 3.3v供电 | x1 | 继电器要买3.3v就可以工作的 |
二、电路接线
三、位置解算
由于我们要完成的目标是能控制机械臂到达任意的(x,y)坐标。因此我们要建立一个数学模型,对数学模型进行求解得到一个算法。
下文提到的“图一”均指下面这幅图片
这里说明一下:Ang1和Ang2的单位是“度”,而其他的为弧度制。所以在最后我们要转化单位
四、代码部分
(一)、整体框架和思路
(1)、使用数学模型进行位置解算
(2)、使用VIsual Studio 2022 验证算法
(3)、在Keil5中进行模块化编程
①PWM模块驱动电机(PWM波生成模块,舵机驱动模块,磁铁部分模块)
②位置解算模块
③定时器和AD通信实现摇杆控制
④串口通信模块
⑤在主函数中各模块的整合以及测试
(二)、位置算法编程部分
(1)、使用Visual Studio 2022对 “二、位置解算” 部分进行编程和验证
(默认都有C语言基础,故只讲解思路)
**验证思路:**将解出的角度输入到一个逆解函数中去,看返回的X,Y值与输入的是否一样
**注意事项:**①这里长度单位我给的是cm,同时定义浮点数使其精确到小数点后两位
主函数
#include<stdio.h>
#include<math.h>
#include<windows.h>
#include "Position_Angle_Translate.h"
int main()
{
Angle_Init Angle;//初始化结构体的目标角度
C_AStruct Cx_y;//初始化用于验证(x,y)的结构体
float x, y;
float Cx, Cy;
while (1)
{
scanf("%f %f", &x, &y);//输入(x,y)坐标
printf("输入了:X:%.2f Y:%.2f \n", x, y);//打印刚才输入的值
Angle = Position_Angle_Translate(x, y);// ( x , y )//调用函数进行解算
printf("Angle1: %.3f Angle2: %.3f \n", Angle.Ang1, Angle.Ang2);//打印解算出来的角度
Cx_y = Check_Angle(Angle.Ang1, Angle.Ang2);//调用函数验证解算的值
printf("输出:X:%.2f Y:%.2f \n\n", Cx_y.x, Cx_y.y);//打印使用解算角度算出来的(x,y)坐标
}
}
算法函数的头文件
#ifndef __POSITION_ANGLE_TRANSLATE_H
#define __POSITION_ANGLE_TRANSLATE_H
typedef struct//用于在函数中传输角度值
{
float Ang1;
float Ang2;
}Angle_Init;
typedef struct Check_AngleStruct//用于在函数中传解出的(x,y)并验证
{
float x;
float y;
}C_AStruct;
Angle_Init Position_Angle_Translate(float x, float y);
C_AStruct Check_Angle(float Angle1, float Angle2);
#endif
算法函数模块
#include<stdio.h>
#include<math.h>
#include "Position_Angle_Translate.h"
#define PI 3.1415926 //定义一个全局的π
float L1 = 9.6f; //L1 和 L2 的值根据需求填写
float L2 = 11.0f;
float L, Ia, Ib, Ic, a, b, c, n, m;//这里的符号与图一(“二、位置解算”部分)一致,其中Ia,Ib,Ic分别代表COS(a)、COS(b)、COS(c)
float Angle_ChangeType(float x)//这个函数将“弧度制”单位转为以“度”作单位
{
float temp = x * (180 / PI);
return temp;
}
Angle_Init Position_Angle_Translate(float x, float y)
{
Angle_Init Angle;//初始化结构体
Angle.Ang1 = 0;
Angle.Ang2 = 0;
L = sqrt(x * x + y * y);
//下面这个if是用来判断特殊位置的
/*
当L1和L2共线时,继续使用图一算法会出错。
*/
if ((L1 + L2) * (L1 + L2) == (x * x + y * y))
{
Angle.Ang1 = atan2(y , x)*(180/PI);
Angle.Ang2 = 0;
}
else
{
//下面的代码按照图一的思路写就好了
Ib = (L1 * L1 + L * L - L2* L2) / (2 * L1 * L);
Ic = (L * L + L2 * L2 - L1 * L1) / (2 * L * L2);
a = atan2(y, x);
b = acos(Ib);
c = acos(Ic);
n = a - b;
m = b + c;
Angle.Ang1 = Angle_ChangeType(n);
Angle.Ang2 = Angle_ChangeType(m);
}
return Angle;//回传的角度是没有经过补偿的
}
//下面这个函数是用来验证解算的
/*
原理图一有。返回结构体的值(x,y)
*/
C_AStruct Check_Angle(float Angle1, float Angle2)
{
C_AStruct Cx_y;
Cx_y.x = L1 * cos(Angle1 * (PI / 180)) + L2 * cos((Angle1 + Angle2) * (PI / 180));
Cx_y.y = L1 * sin(Angle1 * (PI / 180)) + L2 * sin((Angle1 + Angle2) * (PI / 180));
return Cx_y;
}
(三)、Keil5模块编程部分
如果你是STM32零基础的同学可以跟着我一步步来搭建
(1)PWM模块驱动电机
下面我们会学习到GPIO的初始化,定时器和PWM波 这里简单解释一下:①GPIO就是STM32上的针脚,通过配置这些针脚可以完成你想要的功能②定时器通常和中断一起使用,可以理解为设定了一个闹钟到时就出发相应的事情 ③PWM波就是有特定的高低电平占空比的信号。
(提示:STM32入门门槛大,如果遇到不懂的没关系先照着一步步写。也就是先用,等后面对各种概念都有所了解后理解起来会更容易)
这个是PWM波示波器的演示图,简单看一下就好了*
需要学习掌握的标准库函数:
下面的函数要求就是认识,知道其代表的含义、名字和其能做的东西就可以了。
如果下面某些名词不懂或没听过,可以使用AI搜索一下。
GPIO初始化函数:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);//初始化GPIO
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//将GPIO口设置为高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//将GPIO口设置为低电平
/*
下面的函数可以用来读写GPIO口的寄存器。GPIO的寄存器有两种一个是每个针脚的寄存器(个体),
一个是每个针脚都接上了的寄存器(整体)。可以理解为一个是小路,一个是主干路;
那么下面带有Bit的就是小路,单个针脚的寄存器。
*/
//读
//读输入信号
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);
//读输出信号
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);
//写
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);
下面这个是用来初始化GPIO的结构体,类似这样的结构体在STM32的标准库很常见。你可以将其理解为能配置某个模块参数的东西
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//模式选择
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;//你想要配置的针脚(GPIO口)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//GPIO口传输的速度
GPIO_Init(GPIOA, &GPIO_InitStructure);
PWM和定时器初始化函数:
/*下面这两个函数是用来开启总线的时钟的,APB1和APB2总线分表控制着不同的
外设、模块,他们可能是GPIO,TIM定时器等。只有打开了这些总线时钟,相应
的模块才会工作*/
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);//初始化的函数
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);/*这个是,打开内部时钟。定时期可以是外部计数,也可以用
STM32内部的时钟作为基准计数,这个篇幅有限只介绍内部的*/
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);//清除标志位
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);//清除中断标志位
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);//获取中断标志位
/*
下面这些函数是TIM定时器连接到GPIO的通道,他们对应着相应的GPIO口
*/
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);//打开通道1
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
void TIM_OC4Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);//获取通道1的值
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);//初始化定时器的输入捕获功能
void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);//初始化定时器的PWM功能
TIM_Cmd(TIM3, ENABLE);//定时器的开关
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);//NVIC的优先级分组
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);//初始化NVIC
写PWM控制舵机模块的思路:
①初始化GPIO用于输出PWM波。不过要先确定引脚,这里我选择PinA1 ~ A3,从引脚定义中可知其对应CH2 ~ CH4,定时器是TIM2
②初始化定时器输入捕获同道,用来产生PWM波
③写舵机模块,将PWM波设置成舵机所需要的波型
代码展示:
PWM头文件
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);//初始化函数
void PWM_SetCompare2(uint16_t Compare);//设置通道2的PWM波
void PWM_SetCompare3(uint16_t Compare);//设置通道3的PWM波
void PWM_SetCompare4(uint16_t Compare);//设置通道4的PWM波
#endif
PWM模块源文件
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//开启定时器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIO的时钟
//配置GPIO口
GPIO_InitTypeDef GPIO_InitStructure;
/*GPIO模式设置为复用推挽输出,复用是因为由外设定时器产
生PWM波,推挽是因为要输出高低电平*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;//配置引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//这里速度不作要求,随便设置
GPIO_Init(GPIOA, &GPIO_InitStructure);//别忘了这个初始化函数
TIM_InternalClockConfig(TIM2);//打开STM32的内部时钟
//配置定时器
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//计数器的时钟源的分频模式
/* TIM_CLOCKDIVISION_DIV1:不分频,即定时器的时钟频率与输入时钟频率相同。
TIM_CLOCKDIVISION_DIV2:将时钟频率降低为一半。
TIM_CLOCKDIVISION_DIV4:将时钟频率降低为四分之一*/
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//计数模式
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1; //ARR自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值,如果设置了TIM_RepetitionCounter,那么计数器需要额外完成指定数量的计数周期,才会再次触发更新事件或中断
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置定时器输出比较功能
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0;/*CCR决定PWM信号的占空比,即高
电平持续的时间与整个周期时间的比例。这里先给零,后面会有个函数来设置它*/
//初始化相应通道
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
TIM_OC4Init(TIM2, &TIM_OCInitStructure);
TIM_Cmd(TIM2, ENABLE);
}
/*下面的函数就是通过TIM_SetCompare2函数来设置CCR,注意看他们分别对应着相应的CH通道*/
void PWM_SetCompare2(uint16_t Compare)//函数名自己起,我为了对应就这样起
{
TIM_SetCompare2(TIM2, Compare);
}
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare);
}
void PWM_SetCompare4(uint16_t Compare)
{
TIM_SetCompare4(TIM2, Compare);
}
舵机头文件
#ifndef __SERVO_H
#define __SERVO_H
void Servo_Init(void);
void Servo_SetAngle_1(float Angle);
void Servo_SetAngle_2(float Angle);
void Servo_SetAngle_3(float Angle);
void Servo_SetSmooth(uint16_t dt_ms,float Angle1,float La_Angle1);
#endif
舵机模块源文件
#include "stm32f10x.h" // Device header
#include "PWM.h" /*注意下面要用到PWM模块的函数*/
#include "Delay.h"
void Servo_Init(void)
{
PWM_Init();
}
//下面角度输入的单位是“度”
/*下面的计算公式的原来是((Angle) / 180 * 2000 + 500),我根据自己的机械结构加了点补偿*/
void Servo_SetAngle_1(float Angle)//对应这Ang1
{
PWM_SetCompare2((Angle - 4) / 180 * 2000 + 500);
}
void Servo_SetAngle_2(float Angle)//对应这Ang2
{
PWM_SetCompare3((Angle + 30 - 5) / 180 * 2000 + 500);
}
void Servo_SetAngle_3(float Angle)//这个是控制磁铁的舵机升降的
{
PWM_SetCompare4(Angle / 180 * 2000 + 500);
}
(二)位置解算模块
见上文:“(二)、位置算法编程部分”
(三)定时器和AD通信实现摇杆控制
AD就是:模拟-数字转换器(ADC) STM32C8T6已经集成了这个外设
编程思路
①打开相应时钟:GPIO的时钟,和选择的ADC1外设的时钟
②配置GPIO:AD通信使用的是模拟信号,GPIO口要配置为模拟输入。由于要读取两个值X,Y故配置两个Pin口
③配置ADC的结构体(初始化),并打开ADC
④等待ADC校准和启动的完成
AD头文件
#ifndef __AD_H
#define __AD_H
void AD_Init(void);
uint16_t AD_GetValue(uint8_t ADC_Channel);
#endif
AD源文件
#include "stm32f10x.h" // Device header
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置ADC(模拟-数字转换器)系统的时钟的函数
//配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;//模拟输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,一般默认向右;向右是:0000 0001;向左是:1000 0000
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//是否选择外部触发源,这里不选
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//ADC的模式,单个ADC工作选独立
ADC_InitStructure.ADC_NbrOfChannel = 1;//ADC的通道,上面选了ADC1,故这里写1
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_Init(ADC1,&ADC_InitStructure);
ADC_Cmd(ADC1,ENABLE);//打开ADC
ADC_ResetCalibration(ADC1);//重置ADC校准寄存器的函数
while(ADC_GetResetCalibrationStatus(ADC1)==SET);//等待校准
ADC_StartCalibration(ADC1);//启动ADC
while(ADC_GetCalibrationStatus(ADC1)==SET);//等待启动
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//软件启动ADC
}
/*下面这个函数会在主函数用到。他的参数是AD——GPIO的通道,
也就是PA6 对应 ADC_Channel_6,PA7对应ADC_Channel_7*/
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);//ADC_Channel_x , x can be 1-16
//while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1);
}
通过上面的代码我们就可以读取到摇杆的(x,y)值了,但是这些值只是“固定”的值。我们想要实现的是摇杆向左x一直减,向右x一直加。就需要用到定时器,同时用定时器在主函数里触发中断计数。所以我们还要写Timer.c和Timer.h这两个文件。
我们通过配置定时器每 ,N uS/ms/s ,X或Y的值就变化多少,这里会用到上面提到过的公式:定时频率=72MHz/(PSC+1)/(ARR+1),1MHz = 1x 10^6Hz,PSC:prescaler ,ARR:period。当把ARR设置为10000-1,PSC设置为7200 - 1时,定时器每0.1s触发一次中断
定时器头文件
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
uint16_t CountNum_Get(void);
#endif
定时器源文件
定时器触发中断编程思路:
①打开相应时钟
②选择是内部时钟还是外部时钟
③配置TIM结构体
④清除定时器标志位,并打开定时器中断的开关
⑤配置NVIC结构体
⑥打开定时器的开关
#include "stm32f10x.h" // Device header
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_BaseStructure;
TIM_BaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_BaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1;
TIM_BaseStructure.TIM_Period = 10000 - 1;
TIM_BaseStructure.TIM_Prescaler = 7200 - 1;
TIM_BaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_BaseStructure);
TIM_ClearFlag(TIM3,TIM_FLAG_Update);//清除定时器标志位
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//打开定时器中断
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM3, ENABLE);
}
下面这些是写在主函数的中断函数
下面这个中断函数名字“void TIM3_IRQHandler(void)”是固定的我们可以在下图的地方找到
/*这个名字是固定的,当我们选了TIM3时,想触发中断函数就只能使用这个名称。其他定时器和他们的定时器中断函数也是一一对应的*/
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
{
if( AD_X_V <= 2.0 )
{
X = X + 1;
}
else if(AD_X_V >= 3.0)
{
X = X - 1;
}
if( AD_Y_V <= 2.0 )
{
Y = Y + 1;
}
else if(AD_Y_V >= 3.0)
{
Y = Y - 1;
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
}
(四)串口通信模块
串口这部分比较复杂建议直接去看相关系统的教程,这里我只简单说一下。
串口通信使用了两根线一个是RXD接受,一个是TXD发送,数据形式以HEX16进制为主。
编程思路
发送的编程思路就是先先发送一个字节的函数,再写发送数组的函数
接受的编程思路是设置一个标志RxState 表示传输状态:
①RxState = 0 为等待包头,接受到了包头令RxState = 1
②RxState = 1为传输数据,等数据接受完后令RxState = 2
③RxState = 2 时接受结束等待包尾 ,接受到包尾后令RxState = 0
接受时还要有一个标志Serial_GetRxFlag来判断是否在接受数据
进行串口通信前我们要自己规定一个通信的协议,下面是我定的一个串口协议这里会经常使用10进制和16进制的转换(HEX表示16进制),大家可以用VIsual Studio写一个进制转换代码,输入(x,y)坐标和磁铁拿取状态,就可以得到我们想要的HEX数据包,方便测试
串口通信头文件
#ifndef __SERIAL_H
#define __SERIAL_H
extern uint8_t Serial_RxPacket[7];
extern uint8_t Serial_RxFlag;
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArrary(uint8_t *Arrary, uint8_t Length);
uint8_t Serial_GetRxFlag(void);
#endif
串口通信源文件
#include "stm32f10x.h" // Device header
uint8_t Serial_RxPacket[7];
uint8_t Serial_RxFlag;
#define RxPacket_Length 7 //这个指的是RxPacket的数组长度
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStructure);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
USART_Cmd(USART1,ENABLE);
}
//发送数据部分
void Serial_SendByte(uint8_t Byte)//发送一个字节的数据
{
USART_SendData(USART1,Byte);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
void Serial_SendArrary(uint8_t *Arrary, uint8_t Length)
{
for(uint8_t i =0;i<Length;i++)
{
Serial_SendByte(Arrary[i]);
}
}
//接收数据部分
uint8_t Serial_GetRxFlag(void)
{
if(Serial_RxFlag == 1)
{
Serial_RxFlag = 0;
return 1;
}
return 0;
}
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0;
static uint8_t pRxPacket = 0;
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
uint8_t RxData = USART_ReceiveData(USART1);
if(RxState == 0)
{
if(RxData == 0xFF)
{
RxState = 1;
pRxPacket = 0;
}
}
else if(RxState == 1)
{
Serial_RxPacket[pRxPacket] = RxData;
pRxPacket ++;
if(pRxPacket >= RxPacket_Length)
{
RxState = 2;
}
}
else if(RxState == 2)
{
if(RxData == 0xFE)
{
RxState = 0;
Serial_RxFlag = 1;
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
(五)在主函数中各模块的整合以及测试
在主函数中:①OLED显示器模块和Key按键模块我没有介绍了
②下面还有关串口数据包解包的部分,这个可以现在Visual Studio里写,测试好了再放上去
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
#include "Key.h"
#include "AD.h"
#include "Position_Angle_Translate.h"
#include "Servo.h"
#include "magnet.h"
#include "Serial.h"
float X = 0.0;
float Y = 18.0;
int16_t AD_Y;
float AD_Y_V;
int16_t AD_X;
float AD_X_V;
uint16_t Flag_UpDown = 1;
int main(void)
{
OLED_Init();
Timer_Init();
Key_Init();
AD_Init();
Magnet_Init();
Servo_Init();
Serial_Init();
OLED_ShowString(1,1,"X:00.00");
OLED_ShowString(1,9,"Y:00.00");
OLED_ShowString(2,1,"A1:");OLED_ShowString(2,6,"A2:");OLED_ShowString(2,11,"A3:");
OLED_ShowString(4,1,"Magnet:");
OLED_ShowString(4,8,"OFF");
while (1)
{
//串口通信控制系统
//对数据进行解包
/*
第一位 第二位 第三位 第四位 第五位 第六位 第七位
X的符号 Y的符号 X的整数 X的小数 Y的整数 Y的小数 控制磁铁
*/
float Serial_Arrary[7];
Serial_Arrary[6] = 0.0;//控制磁铁的逻辑数据
if(Serial_GetRxFlag() == 1)
{
//显示接受的原始数据
OLED_ShowHexNum(3, 1,Serial_RxPacket[2] , 2);
OLED_ShowHexNum(3, 4, Serial_RxPacket[3], 2);
OLED_ShowHexNum(3, 7, Serial_RxPacket[4], 2);
OLED_ShowHexNum(3, 10, Serial_RxPacket[5], 2);
OLED_ShowHexNum(3, 13, Serial_RxPacket[6], 2);
//使用数组接受数据
for(int i = 0 ;i < 7 ;i++ )
{
Serial_Arrary[i] = Serial_RxPacket[i];
}
//解码算法,精度到小数点后两位
int x_n,x_m,y_n,y_m;
x_n = Serial_Arrary[2];
x_m = Serial_Arrary[3];
y_n = Serial_Arrary[4];
y_m = Serial_Arrary[5];
X = x_n + ((float)x_m / 100);
Y = y_n + ((float)y_m / 100);
//确定X、Y的符号
if(Serial_Arrary[0] == 1)
{
X = -1 * X;
}
if(Serial_Arrary[1] == 1)
{
Y = -1 * Y;
}
}
//使用摇杆和按键控制系统
//控制上下左右
//使用摇杆获得目标 X , Y
AD_Y = AD_GetValue(ADC_Channel_7);
AD_Y_V = (float)AD_Y / 4095 * 5;
Delay_ms(10);
AD_X = AD_GetValue(ADC_Channel_6);
AD_X_V = (float)AD_X / 4095 * 5;
//X,Y解算控制舵机的角度
Angle_Init Angle;
Angle = Position_Angle_Translate(X ,Y );
//设置舵机
Servo_SetAngle_1(Angle.Ang1);
Servo_SetAngle_2(Angle.Ang2);
//控制磁铁
uint16_t KeyNum = Key_GetNum();
if(KeyNum == 1)//继电器控制磁铁开关
{
Magnet_Pin_Turn();
}
if(KeyNum == 2)
{
Magnet_MoveUD(Flag_UpDown);
Flag_UpDown++;
if(Flag_UpDown > 2)
{
Flag_UpDown = 1;
}
}
//串口控制磁铁
/*
Magnet_Flag = 0 ——》 不操作
Magnet_Flag = 1 ——》下降-吸-抬升
Magnet_Flag = 2 ——》下降-放-抬升
*/
uint8_t Magnet_Flag = Serial_Arrary[6];
if(Magnet_Flag == 1)
{
Magnet_MoveUD(2);
Delay_ms(300);
Magnet_Pin_ON();
OLED_ShowString(4,8,"ON ");
//Delay_ms(100);
Magnet_MoveUD(1);
}
if(Magnet_Flag == 2)
{
Magnet_MoveUD(2);
Delay_ms(300);
Magnet_Pin_OFF();
OLED_ShowString(4,8,"OFF");
//Delay_ms(100);
Magnet_MoveUD(1);
}
//显示一些参数
//坐标值
OLED_ShowFNum(1, 3, X, 4, 2);
OLED_ShowFNum(1, 11, Y, 4, 2);
//解算后的角度值
OLED_ShowNum(2,4,Angle.Ang1,2);
OLED_ShowNum(2,9,Angle.Ang2,2);
}
}
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update) == SET)
{
if( AD_X_V <= 2.0 )
{
X = X + 1;
}
else if(AD_X_V >= 3.0)
{
X = X - 1;
}
if( AD_Y_V <= 2.0 )
{
Y = Y + 1;
}
else if(AD_Y_V >= 3.0)
{
Y = Y - 1;
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
}
}
本人实力有限,只能写成这种水平了
今天就写到这里了,以后有时间再补充了