一、简介

(一)、学习的知识

下文将讲解如何使用STM32F103C8T6最小芯片板,完成一个简单的机械臂制作。使用STM32标准库进行编程,将学习到的主要内容会有:①电源选择与电路接线②定时器③PWM波控制舵机④PSP摇杆与AD通信⑤继电器的使用⑥串口通信⑦数学建模思想

相信通过这个实例会对你学习STM32有整体上的帮助。但是要深入学习还是要学习其他系统性的课程,比如:江协科技的STM32入门教程

注意:零基础的同学观看会很吃力,建议只跟着写代码,对许多名词不了解时先跳过
②代码部分是基于江协科技 “ 4-1 OLED显示屏 ”原文件来写的

这是江协科技的STM32入门教程的链接

代码原文件:获取链接 (建议配合原码食用)

(二)、将学习/使用的软件

①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波示波器的演示图,简单看一下就好了*
这个是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);
	}

	
}

本人实力有限,只能写成这种水平了
今天就写到这里了,以后有时间再补充了

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐