上篇博客,我将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四层板绘制,项目本身功能不难,实验室师兄之前做过类似项目,遂通过这个小项目来学习操作系统和四层板的绘制,期间也请教师兄相关功能的实现,现在自己做出来后,将制作历程写成博客分享给大家,也作为自己后续学习的笔记。
创作不易,希望大家多多交流,点赞,收藏,评论加关注!谢谢大家!!!