基于STM32和FreeRTOS的智能追光云台(代码开源)

上篇博客,我将FreeRTOS移植到STMF103RCT6上,实现了简单的两个LED灯以不同频率去闪烁的小任务,最近找了个项目练练手,用STM32F103C8T6作为主控,搭载FreeRTOS,制作了一个智能追光云台。

参考博客:

将FreeRTOS移植到STM32F103RCT6(江协模版,标准库)_stm32f103rct6 rtos-CSDN博客

基于STM32F1的自动追光云台(代码开源)_smt32开源-CSDN博客

一、控制SG90舵机

要想控制舵机,电机等这些设备,就不得不提到PWM,这里简要介绍一下PWM波的相关知识

PWM(Pulse Width Modulation)脉冲宽度调制。

在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域。

PWM参数: 

频率 = 1 / TS            占空比 = TON / TS           分辨率 = 占空比变化步距

(图片选自江协科技的课程PPT资料)

STM32中利用通用定时器输出PWM波的基本思路:

(图片选自江协科技的课程PPT资料)

下面对SG90舵机进行简单介绍:

(图片选自江协科技的课程PPT资料)

下面通过代码及注释来对上图进行解释:

1、舵机PWM输出

#include "stm32f10x.h"                  // Device header

void PWM_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;						//定义PWM波输出GPIO结构体
	
	/*---初始化云台上下舵机---*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);		//开启GPIOA时钟
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;				//选择复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;					//选择GPIO6
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;			//速度,一般都设置这个值
	GPIO_Init(GPIOA,&GPIO_InitStructure);						//初始化结构体
	
	/*---初始化云台左右电机---*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);		//开启GPIOA时钟
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;				//选择复用推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;					//选择GPIO7
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;			//速度,一般都设置这个值
	GPIO_Init(GPIOA,&GPIO_InitStructure);						//初始化结构体

	/*---配置舵机输出PWM波,500Hz---*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);			//开启TIM3时钟
	TIM_TimeBaseInitTypeDef TimeBaseInitStructure;				//定义一个时基单元结构体
	TIM_OCInitTypeDef TIM_OCInitStructure;						//定义输出比较结构体
	/*---配置时基单元---*/
	TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;		//选择不分频
	TimeBaseInitStructure.TIM_Period = 20000-1;					//PWM周期20ms
	TimeBaseInitStructure.TIM_Prescaler = 72-1;					//将72M主频进行72分频
	TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;	//向上计数
	TIM_TimeBaseInit(TIM3,&TimeBaseInitStructure);				//初始化结构体
	/*---配置输出比较通道---*/
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;			//选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;	//高电平有效			
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//输出比较使能
	TIM_OCInitStructure.TIM_Pulse = 0;							//CCR先设置为0
	TIM_OC1Init(TIM3,&TIM_OCInitStructure);						//初始化输出比较通道1
	
	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;							
	TIM_OC2Init(TIM3,&TIM_OCInitStructure);						//初始化输出比较通道2
	
	TIM_OC1PreloadConfig(TIM3,TIM_OCPreload_Enable);   			//CH1使能预装载寄存器
	TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable);   			//CH2使能预装载寄存器
	
	TIM_ARRPreloadConfig(TIM3,ENABLE);							//使能ARR预装载
	
	TIM_Cmd(TIM3,ENABLE);										//启动定时器
}

对于GPIO口的引脚选择,可以参考下图STM32F103C8T6引脚定义表

(图片选自江协科技的课程资料)

2、舵机控制函数编写

前面图片提到舵机的PWM波频率要求,已经PWM占空比与其转动角度的对应关系。针对这些要求可以编写舵机控制函数

#include "stm32f10x.h"                  // Device header
#include "PWM.h"

void Servo1_MOVE(float Angle)
{
	TIM_SetCompare1(TIM3,Angle/180*2000+500);
}

void Servo2_MOVE(float Angle)
{
	TIM_SetCompare2(TIM3,Angle/180*2000+500);
}

/*
角度与PWM高电平时间对应关系可以理解为
0度   ---   500us即0.5ms
45度  ---   1000us即1ms
90度  ---   1500us即1.5ms
135度 ---   2000us即2ms
180度 ---   2500us即2.5ms

角度/180得到希望输出角度与180度的比值关系,再*2000+500即可完成角度与PWM占空比之间的线性转换
*/

 二、ADC+DMA对传感器数据进行采集

(图片选自江协科技的课程PPT资料)

(图片选自江协科技的课程PPT资料)

(图片选自江协科技的课程PPT资料)

(图片选自江协科技的课程PPT资料)

下面通过代码及注释来对上图进行解释:

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];                    //定义一个数组存放传感器的数据

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);     //打开ADC1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);    //打开GPIOA时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);       //打开DMA时钟
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);    //指定ADC的工作频率是主频72MHz/6
	
	GPIO_InitTypeDef GPIO_InitStructure; //定义四个采集传感器的GPIO口,配置为模拟输入
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
    /*---ADC规则组配置,ADC1,通道0-3,转换顺序1 2 3 4,采样时间---*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
	
    //初始化ADC结构体
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;     //ADC工作在独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//不使用外部信号启动ADC转换
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;     //ADC连续模式
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;           //ADC扫描模式
	ADC_InitStructure.ADC_NbrOfChannel = 4;                //需要4个转换通道
	ADC_Init(ADC1, &ADC_InitStructure);                    //初始化结构体
	
    //初始化DMA结构体
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //DMA从ADC1的数据寄存器板运数据
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据大小为半个字即两个字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  //读取外设地址不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;  //数据存放地址为一开始创建的数组
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据大小字节
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;    //数据存放地址自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;         //数据从外设到内存
	DMA_InitStructure.DMA_BufferSize = 4;                      //指定数据缓冲区大小
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;            //DMA设置为循环模式
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;               //不启用内存到内存传输
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;      //DMA优先级为中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);               //初始化结构体
	    
	DMA_Cmd(DMA1_Channel1, ENABLE);    //DMA使能
	ADC_DMACmd(ADC1, ENABLE);          //ADC与DMA数据转运使能
	ADC_Cmd(ADC1, ENABLE);             //ADC使能
	
	ADC_ResetCalibration(ADC1);        //复位ADC1校准寄存器
	while (ADC_GetResetCalibrationStatus(ADC1) == SET); //等待复位完成
	ADC_StartCalibration(ADC1);        //开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET); //等待校准完成
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);    //软件触发ADC转换
}

三、功能实现

控制程序大致思路:通过太阳能板四个顶角的光敏传感器的数据,经过一系列运算,对两个云台舵机进行控制。但如何一共有四个数据,如何控制两个舵机呢?这里在我的参考博客里找到了答案。四个传感器数据分别代表着左上角(左上)光敏值、左下角(左下)光敏值、右上角(右上)光敏值和右下(右下角)光敏值。两个舵机一个控制左右方向,一个控制上下方向,那如果左边比右边亮,控制左右方向的舵机向左转,反之向右转;如果上面比下面亮,则控制上下方向的舵机向上抬,反之向下降。有了这个思路,程序就好写了

(左上 + 右上)/ 2 = 太阳能面板上方的一个光敏值;

(左下 + 右下)/ 2 = 太阳能面板下方的一个光敏值;

(左上 + 左下)/ 2 = 太阳能面板左方的一个光敏值;

(右左 + 右上)/ 2 = 太阳能面板右方的一个光敏值。

再设定一个亮度误差临界值,当上下之间的亮度误差或左右之间的亮度小于这个值的时候,代表太阳能板在正对光源的方向,若大于这个值,则就要往亮度更亮的地方去移动,从而控制舵机移动到合适的位置。

下面直接上代码:

1、主函数程序

#include "stm32f10x.h"                  // Device header
#include "freertos.h"
#include "task.h"
#include "OLED.h"
#include "AD.h"
#include "LED.h"
#include "FreeRTOS_Programs.h"
#include "PWM.h"
#include "Servo.h"
#include "Serial.h"
#include "Motor.h"
#include "Delay.h"

float Angle1 = 0;	//左右舵机变化的角度值
float Angle2 = 0;	//上下舵机变化的角度值
uint16_t zs,zx,ys,yx; //左上、左下、右上、右下光敏值
int16_t dup,dxia,dleft,dright,devsx,devzy; //上下左右的光敏值以及上下亮度之差和左右亮度之差
float limit_up = 30,limit_down = -30,limit_left = 80,limit_right = -60; //舵机上下左右的运动角度限幅
int8_t value = 2; // 亮度误差临界值,该值越小,整个系统的追光精度越高
int main()
{
	OLED_Init();    //OLED初始化,用作调试
	AD_Init();      //ADC数据采集初始化
	LED_Init();     //LED初始化
	PWM_Init();     //PWM初始化
	Motor_Init();   //小电机初始化,驱动一个小风扇转动
	Serial_Init();  //串口初始化,用作调试
	Servo1_MOVE(70); //上电后,将左右方向的舵机设定在一个预先角度
	Servo2_MOVE(90); //上电后,将上下方向的舵机设定在一个预先角度
	Delay_ms(1000);  //延时1s后开始进行追光
	freertos_demo(); //这里将控制程序封装在一个函数里,用FreeRTOS来开发
}

2、控制代码 freertos_demo()

#include "stm32f10x.h"                  // Device header
#include "freertos.h"
#include "task.h"
#include "LED.h"
#include "OLED.h"
#include "AD.h"
#include "PWM.h"
#include "Servo.h"
#include "Serial.h"
#include "Motor.h"

extern float Angle1;
extern float Angle2;
extern uint16_t zs, zx, ys, yx;
extern int16_t dup, dxia, dleft, dright, devsx, devzy;
extern int8_t value;
extern float limit_up, limit_down, limit_left, limit_right;

/*---创建任务句柄---*/
static TaskHandle_t myTaskHandler_0;
static TaskHandle_t myTaskHandler_1;
static TaskHandle_t myTaskHandler_2;
static TaskHandle_t myTaskHandler_3;
static TaskHandle_t myTaskHandler_4;
static TaskHandle_t myTaskHandler_5;
	
/*---声明任务---*/
void task_begin(void *arg);
void myTask_Sensor(void *arg);	//对传感器数据进行处理
void myTask_LED(void *arg);		//LED闪烁
void myTask_Servo1(void *arg);	//左右舵机控制
void myTask_Servo2(void *arg);	//上下舵机控制
void myTask_Motor(void *arg);	//小风扇转动
	
/*---创建开始任务,其目的就是为了创建其余的工作任务---*/
void freertos_demo(void)
{
	xTaskCreate(task_begin,"task_begin",128,NULL,1,&myTaskHandler_0);
	vTaskStartScheduler();
}
	
void task_begin(void *arg)
{
	taskENTER_CRITICAL(); //进入临界区,并创建任务
	xTaskCreate(myTask_Sensor,"myTask_Sensor",128,NULL,2,&myTaskHandler_1);
	xTaskCreate(myTask_LED,"myTask_LED",128,NULL,2,&myTaskHandler_2);
	xTaskCreate(myTask_Servo1,"myTask_Servo1",128,NULL,3,&myTaskHandler_3);
	xTaskCreate(myTask_Servo2,"myTask_Servo2",128,NULL,2,&myTaskHandler_4);
	xTaskCreate(myTask_Motor,"myTask_Motor",128,NULL,2,&myTaskHandler_5);
	vTaskDelete(NULL);
	taskEXIT_CRITICAL();  //退出临界区。临界区在这里的作用就是确保任务在创建时不受任务调度器干预,保证每个任务的创建有序进行
}

/*---处理传感器数据---*/
void myTask_Sensor(void *arg)
{
	while(1)
	{
		zs = (float)AD_Value[0] / 4095 * 100; //将传感器数据转为100量程的数值,方便计算
		zx = (float)AD_Value[1] / 4095 * 100;
		ys = (float)AD_Value[2] / 4095 * 100;
		yx = (float)AD_Value[3] / 4095 * 100;
		dup = (zs+ys)/2;
		dxia = (zx+yx)/2;
		dleft = (zs+zx)/2;
		dright = (ys+yx)/2;
		devsx = dup - dxia;    //得到上下偏差
		devzy= dleft - dright; //得到左右偏差
		vTaskDelay(50);
	}
}

/*---LED闪烁---*/
void myTask_LED(void *arg)
{
	while(1)
	{
		LED_ON(13);
		vTaskDelay(1000);
		LED_OFF(13);
		vTaskDelay(1000);
	}
}

/*---左右舵机控制,当左边与右边之差大于临界值的时候,舵机角度++,到达限幅自动停止,反之亦然---*/
void myTask_Servo1(void *arg)
{
	while(1)
	{
		if(-1*value > devzy || devzy > value)
		{
			if(dleft > dright)
			{
				Angle1++;
				if(limit_left < Angle1) Angle1 = limit_left;
			}
			else if(dleft < dright)
			{
				Angle1--;
				if(limit_right > Angle1) Angle1 = limit_right;
			}
			Servo1_MOVE(70+Angle1);
		}
		vTaskDelay(50);
	}
}

/*---上下舵机控制,当上边与下边之差大于临界值的时候,舵机角度++,到达限幅自动停止,反之亦然---*/
void myTask_Servo2(void *arg)
{
	while(1)
	{
		if(-1*value > devsx || devsx > value)
		{
			if(dup > dxia)
			{
				Angle2++;
				if(limit_up < Angle2) Angle2 = limit_up;
			}
			else if(dup < dxia)
			{
				Angle2--;
				if(limit_down > Angle2) Angle2 = limit_down;
			}
			Servo2_MOVE(90+Angle2);
		}
		vTaskDelay(50);
	}
}

/*---驱动小风扇电机转动---*/
void myTask_Motor(void *arg)
{
	while(1)
	{
		TIM1->CCR1 = 50;
		vTaskDelay(50);
	}
}

四、效果展示

智能追光云台

五、代码链接

通过百度网盘分享的文件:智能追光云台.rar
链接:https://pan.baidu.com/s/122LfG_F3eWeHGky8gO-K2w?pwd=7fkz 
提取码:7fkz 
--来自百度网盘超级会员V5的分享

六、总结

最近在学习FreeRTOS和PCB四层板绘制,项目本身功能不难,实验室师兄之前做过类似项目,遂通过这个小项目来学习操作系统和四层板的绘制,期间也请教师兄相关功能的实现,现在自己做出来后,将制作历程写成博客分享给大家,也作为自己后续学习的笔记。

创作不易,希望大家多多交流,点赞,收藏,评论加关注!谢谢大家!!!

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值