前言
最近在准备带电赛,顺带做一做上年e题,仅实现关键部分,按键复位等功能没有花时间去弄,兄弟们给按键加一个外部中断就行了,注意优先级。顺带聊一聊今年电赛预测,网上已经开始推测今年指定用德州仪器的板子,按理来说也该轮到德州仪器了,毕竟已经好几年没有出相关的题了,所以今年大概率会指定德州仪器的开发板;仅个人推测!!!!!!!
正文开始
铅笔框部分
由于openmv的精度问题,无法识别框的四个角点,我们采取的是将铅笔框的四个角点用一个数组存起来,需要时将数据传给云台就行。
黑色矩形框
有两种解法,一是通过反正切函数去算球坐标系和平面坐标系的映射,得到云台需要转动的角度,但这个解法由于不是线性运算,存在1.75cm的误差,这个误差太大是我们不允许的。所以我采用第二种解法,直接上pid,缺点就是调试过于枯燥。
代码部分
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "Serial.h"
#include "PWM.h"
#include "Resolve_C.h"
#include "Key.h"
#include "Servo.h"
extern uint16_t CountSensor_Count1;
int main(void)
{
/*模块初始化*/
Serial_Init(); //串口初始化
PWM_Init();
/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//测试代码
PWM_SetCompare_x(1166);
PWM_SetCompare_y(1250);
while (1)
{
/*读取状态位,确保数据接收完整*/
if (Serial_GetRxFlag() == 1) //如果接收到数据包
{
//解析串口坐标数据;
receive_coordinate(Serial_RxPacket);
//pid数据传入舵机;
Servo_Init();
//调试代码,检查串口解析数据是否正确;
Serial_SendByte1(x);
Serial_SendByte1(y);
}
}
}
串口代码
兄弟们大部分都师出同门,我也是,就基于江科大的代码,改了一部分逻辑,与openmv通信协议为FF x,y FE ,这个格式有点怪是因为没有与openmv端沟通通信协议,所以没有左右括号,我就手动在缓冲区里添加了左右括号,方便解析坐标时方便拆分;
Serial.c
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
uint16_t Serial_TxPacket[20]; //定义发送数据包数组,数据包格式:AA .... CC
char Serial_RxPacket[20]; //定义接收数据包数组
uint8_t Serial_RxFlag; //定义接收数据包标志位
int pRxPacket ; //定义表示当前接收数据位置的静态变量
/**
* 函 数:串口初始化
* 参 数:无
* 返 回 值:无
*/
void Serial_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //开启USART1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
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); //将PA9引脚初始化为复用推挽输出
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); //将PA10引脚初始化为上拉输入
/*USART初始化*/
USART_InitTypeDef USART_InitStructure; //定义结构体变量
USART_InitStructure.USART_BaudRate = 9600; //波特率
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //硬件流控制,不需要
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; //模式,发送模式和接收模式均选择
USART_InitStructure.USART_Parity = USART_Parity_No; //奇偶校验,不需要
USART_InitStructure.USART_StopBits = USART_StopBits_1; //停止位,选择1位
USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长,选择8位
USART_Init(USART1, &USART_InitStructure); //将结构体变量交给USART_Init,配置USART1
/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启串口接收数据的中断
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure; //定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure); //将结构体变量交给NVIC_Init,配置NVIC外设
/*USART使能*/
USART_Cmd(USART1, ENABLE); //使能USART1,串口开始运行
}
/**
* 函 数:串口发送一个字节
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:电赛专用
* 参 数:Byte 要发送的一个字节
* 返 回 值:无
*/
void Serial_SendByte1(uint16_t Byte)
{
USART_SendData(USART1, Byte); //将字节数据写入数据寄存器,写入后USART自动生成时序波形
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); //等待发送完成
/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}
/**
* 函 数:串口发送一个数组
* 参 数:Array 要发送数组的首地址
* 参 数:Length 要发送数组的长度
* 返 回 值:无
*/
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:电赛专用
* 参 数:Array 要发送数组的首地址
* 参 数:Length 要发送数组的长度
* 返 回 值:无
*/
void Serial_SendArray1(uint16_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:串口发送一个字符串
* 参 数:String 要发送字符串的首地址
* 返 回 值:无
*/
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
{
Serial_SendByte(String[i]); //依次调用Serial_SendByte发送每个字节数据
}
}
/**
* 函 数:次方函数(内部使用)
* 返 回 值:返回值等于X的Y次方
*/
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1; //设置结果初值为1
while (Y --) //执行Y次
{
Result *= X; //将X累乘到结果
}
return Result;
}
/**
* 函 数:串口发送数字
* 参 数:Number 要发送的数字,范围:0~4294967295
* 参 数:Length 要发送数字的长度,范围:0~10
* 返 回 值:无
*/
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++) //根据数字长度遍历数字的每一位
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0'); //依次调用Serial_SendByte发送每位数字
}
}
/**
* 函 数:使用printf需要重定向的底层函数
* 参 数:保持原始格式即可,无需变动
* 返 回 值:保持原始格式即可,无需变动
*/
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch); //将printf的底层重定向到自己的发送字节函数
return ch;
}
/**
* 函 数:自己封装的prinf函数
* 参 数:format 格式化字符串
* 参 数:... 可变的参数列表
* 返 回 值:无
*/
void Serial_Printf(char *format, ...)
{
char String[100]; //定义字符数组
va_list arg; //定义可变参数列表数据类型的变量arg
va_start(arg, format); //从format开始,接收参数列表到arg变量
vsprintf(String, format, arg); //使用vsprintf打印格式化字符串和参数列表到字符数组中
va_end(arg); //结束变量arg
Serial_SendString(String); //串口发送字符数组(字符串)
}
/**
* 函 数:串口发送数据包
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,Serial_TxPacket数组的内容将加上包头(FF)包尾(FE)后,作为数据包发送出去
*/
void Serial_SendPacket(void)
{
Serial_SendByte(0xFF);
Serial_SendArray1(Serial_TxPacket, 4);
Serial_SendByte(0xFE);
}
/**
* 函 数:获取串口接收数据包标志位
* 参 数:无
* 返 回 值:串口接收数据包标志位,范围:0~1,接收到数据包后,标志位置1,读取后标志位自动清零
*/
uint8_t Serial_GetRxFlag(void)
{
if (Serial_RxFlag == 1) //如果标志位为1
{
Serial_RxFlag = 0;
return 1; //则返回1,并自动清零标志位
}
return 0; //如果标志位为0,则返回0
}
/**
* 函 数:USART1中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void USART1_IRQHandler(void)
{
static uint8_t RxState = 0; //定义表示当前状态机状态的静态变量
if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET) //判断是否是USART1的接收事件触发的中断
{
uint16_t RxData = USART_ReceiveData(USART1); //读取数据寄存器,存放在接收的数据变量
/*使用状态机的思路,依次处理数据包的不同部分*/
/*当前状态为0,接收数据包包头*/
if (RxState == 0)
{
if (RxData == 0xFF) //如果数据确实是包头
{
RxState = 1; //置下一个状态
pRxPacket = 0; //数据包的位置归零
}
}
/*当前状态为1,接收数据包数据*/
else if (RxState == 1)
{
Serial_RxPacket[pRxPacket + 1] = RxData; //将数据存入数据包数组的指定位置
pRxPacket ++;
if (RxData == 0xFE) //如果数据确实是包尾部
{
//串口通信协议包装
Serial_RxPacket[0] = 0x28; //将数据存入数据包数组的指定位置
Serial_RxPacket[pRxPacket] = 0x29;
Serial_RxPacket[pRxPacket+1] = 0x00;
RxState = 0; //状态归0
Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包
}
}
USART_ClearITPendingBit(USART1, USART_IT_RXNE); //清除标志位
}
}
pid部分
pid要根据实际情况来调,如果不太理解,可以去b站了解一下pid相关知识后再来看
pid.c
#include "stm32f10x.h"
//x轴pid
float Kp_x=2.2,
Ki_x=0,
Kd_x=2.1;
float www,zzz;
int pwm_xpid(int xerror)
{
int pid_ActualPwm;
static float pid_Integral,pid_Voltage,error_Last;
pid_Integral+=xerror;
if (pid_Integral<-6000) pid_Integral=-6000;//积分限幅
if (pid_Integral>6000) pid_Integral=6000;//积分限幅
pid_Voltage=Kp_x*xerror+Kd_x*(xerror-error_Last);
error_Last=xerror;
pid_ActualPwm = pid_Voltage;
if (pid_ActualPwm<-1000) pid_ActualPwm=-500;//pwm的范围是500到2500,这里要对pwm进行限幅
if (pid_ActualPwm>1000) pid_ActualPwm=1000;
return pid_ActualPwm;
}
//y轴pid
float Kp_y=1.1,
Ki_y=0,
Kd_y=2;
int pwm_ypid(int yerror)
{
int pid_ActualPwm;
static float pid_Integral,pid_Voltage,error_Last;
pid_Integral+=yerror;
if (pid_Integral<-6000) pid_Integral=-6000;//积分限幅
if (pid_Integral>6000) pid_Integral=6000;//积分限幅
pid_Voltage=Kp_y*yerror+Kd_y*(yerror-error_Last);
error_Last=yerror;
pid_ActualPwm = pid_Voltage;
if (pid_ActualPwm<-500) pid_ActualPwm=-500;//pwm的范围是500到2500,这里要对pwm进行限幅
if (pid_ActualPwm>500) pid_ActualPwm=500;
return pid_ActualPwm;
}
写入ccr
这部分代码就是将pid计算的值给云台,让云台动起来
Servo.c
#include "stm32f10x.h" // Device header
#include "Resolve_C.h"
#include "PWM.h"
#include "pid.h"
#include "Serial.h"
void Servo_Init(void)
{
int pid_xerror,pid_yerror,xpwm,ypwm;
//Cx = -167;//-167对应0°,167对应180°
pid_xerror = x;//0为画面中心点的横坐标,这行代码是计算红色物体中心点横坐标离画面中心点横坐标的偏差值
xpwm = pwm_xpid(pid_xerror);//通过pid计算得到x轴舵机运动的pwm值
//PWM_SetCompare2(1166-xpwm );//270°舵机在1166时在90°处。
PWM_SetCompare_x( 1166 - xpwm);
Serial_SendByte1(xpwm);
//Cy = 150;//-150 0°,150 90°;
pid_yerror=y;//0为画面中心点的纵坐标,这行代码是计算红色物体中心点纵坐标离画面中心点纵坐标的偏差值
ypwm = pwm_ypid(pid_yerror);//通过pid计算得到y轴舵机运动的pwm值
PWM_SetCompare_y(1000 + ypwm);
}
PWM部分
大家师出同门,我就不用多讲了,唯一的区别就是增加了一个通道
PWM.c
#include "stm32f10x.h" // Device header
/**
* 函 数:PWM初始化
* 参 数:无
* 返 回 值:无
*/
void PWM_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //开启TIM2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA1引脚初始化为复用推挽输出
//受外设控制的引脚,均需要配置为复用模式
/*配置时钟源*/
TIM_InternalClockConfig(TIM2); //选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
/*时基单元初始化*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; //定义结构体变量
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
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_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure); //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
/*输出比较初始化*/
TIM_OCInitTypeDef TIM_OCInitStructure; //定义结构体变量
TIM_OCStructInit(&TIM_OCInitStructure); //结构体初始化,若结构体没有完整赋值
//则最好执行此函数,给结构体所有成员都赋一个默认值
//避免结构体初值不确定的问题
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值
TIM_OC2Init(TIM2, &TIM_OCInitStructure); //将结构体变量交给TIM_OC2Init,配置TIM2的输出比较通道2
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
/*TIM使能*/
TIM_Cmd(TIM2, ENABLE); //使能TIM2,定时器开始运行
}
/**
* 函 数:PWM设置CCR
* 参 数:Compare 要写入的CCR的值,范围:0~100
* 返 回 值:无
* 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
* 占空比Duty = CCR / (ARR + 1)
*/
void PWM_SetCompare_x(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare); //设置CCR2的值
}
void PWM_SetCompare_y(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare); //设置CCR2的值
}
坐标解析部分
由于视觉部分传过来的坐标是字符形式,所以需要通过ASCII码值来解析,我们将得到的值通过已逗号为标志位将数组进行拆分,左右两边的界限为左右括号的数组下标值,从而得到坐标,正负部分我们可以判断左括号和逗号后第一个值的ASCII码表对应的值是不是负号 ,从而判断x和y的正负。
Resolve_C.c
#include "stm32f10x.h" // Device header
#include "stdint.h"
#include "Serial.h"
#include "stm32f10x.h"// Device header
#include "math.h"
#include <string.h>
extern int pRxPacket ;
//*************尽量少用全局变量,可以用return 将值返回,全局变量必须在函数体里赋值**********************//
//*************不然进行运算时上一次的值就会带入下一次,从而影响每一次的结果。***************************//
int x = 0;
int y = 0;
char asc_exchange_ten(char Asc)
{
//ASCII转换成数字的函数
char value;
switch (Asc)
{
case 0x30: //0的ASC码时0x30,十进制48
value = 0;
break;
case 0x31:
value = 1;
break;
case 0x32:
value = 2;
break;
case 0x33:
value = 3;
break;
case 0x34:
value = 4;
break;
case 0x35:
value = 5;
break;
case 0x36:
value = 6;
break;
case 0x37:
value = 7;
break;
case 0x38:
value = 8;
break;
case 0x39:
value = 9;
break;
}
return value;
}
void receive_coordinate(char *array)
{
x = 0;
y = 0;
int i = 0;
int comma_Index; // 逗号标志位
int len = pRxPacket; // 获取字符串长度
for (i = 0; i < len; i++)
{
if (array[i] == ',')
{
comma_Index = i;
break;
}
}
int isXNegative = array[1] == '-';
int isYNegative = array[comma_Index + 1] == '-';
// 解析 x 坐标
for (i = isXNegative ? 2 : 1; i < comma_Index; i++)
{
x += asc_exchange_ten(array[i]) * pow(10, comma_Index - i - 1);
}
x = isXNegative ? -x : x;
// 解析 y 坐标
for (i = comma_Index + (isYNegative ? 2 : 1); i < len; i++)
{
y += asc_exchange_ten(array[i]) * pow(10, len - i - 1);
}
y = isYNegative ? -y : y;
}
这篇文章是写给刚入门的兄弟看的,大部分入手stm32的兄弟都是用的f103的最小系统板,也是看江科大的视频过来,所以这篇文章最能解决兄弟们的需求,佬的话就请指出问题不足。