声明:本人也是初学者,写这篇博客的目的是梳理一下思路,和分享一下自己的想法,对于一些原理我就不阐述了(网站上已经有许多大佬讲了)如有错误和不足希望大家指出。
1.先是adc,毕竟我们要有数据才能进行接下来的操作(我使用的是六路多通道采集)
因为要不停的采集,所以放在定时器中比较好(我定时器12用来干其它了,所以我用的定时器1)
定时器3就选:internal clock 12就在 clock source 中选internal clock
相关配置
1.定时器
adc配置
然后是代码:这里我用了一个二位数组将每个端口的十组数据存到二位数组ADC_Value[a][i]中,这段从for循环中采集10次,即二位数组中储存了每个端口的十组数据,然后通过定时器中断,实现他的连续采集。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
pidController();
Servo_Control(angle);
MotorControl(0, 5000, 5000); //直行
HAL_Delay(200);
for (int a = 0; a < 10; a++)
{
for (i = 0; i < 6; i++)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 50); //等待转换完成
ADC_Value[a][i] = HAL_ADC_GetValue(&hadc1);
}
HAL_ADC_Stop(&hadc1);
}
}
}
2,数据处理
我采用了1.先扫描2.然后限幅3.除去最大值和最小值平均(这三步都是尽量减小误差的影响)方法不是最优,有问题希望大家指出
最后再进行归一化
1.扫描:(其实可以扫描多次更精确,但有点费事)
void saomiao(void)
{
int i, b;
for(i=0;i<6;i++)
{
for(b=0;b<10;b++)
{
if(ADC_collect[b][i]>max[i])
max[i]=ADC_collect[b][i];
if(ADC_collect[b][i]<min[i])
min[i]=ADC_collect[b][i];
}
}
}
2.限幅滤波(除去偶然出现的极值)
void AD_filter(void)
{
int i = 0,b = 0;
for(i=0;i<6;i++)
{
for(b=0;b<10;b++)
{
if(ADC_collect[b][i]>max[i])
ADC_collect[b][i]=max[i];
if(ADC_collect[b][i]<min[i])
ADC_collect[b][i]=min[i];
}
}
}
3.求平均 和归一化(归一化的目的就是使得预处理的数据被限定在一定的范围内(比如[0,1]或者[-1,1]),从而消除奇异样本数据导致的不良影响。)我这里把他放大了一点,以免过小引起的数据失真
归一化的公式:x=(x-Min)/(Max-Min)
void AD_value(void)
{
int i ,b ,c,d,e;
double sum;
uint16_t max[6] = {0},min[6] = {0};
uint16_t Average[6] = {0};
for(c = 0;c<6;c++)
{
max[c] = ADC_collect[0][c];
min[c] = ADC_collect[0][c];
}
for(i=0;i<6;i++)
{
for(b=0;b<10;b++)
{
if(ADC_collect[i][b]>max[b])
{
max[b] = ADC_collect[i][b];
}
if(ADC_collect[i][b]<min[b])
{
min[b] = ADC_collect[i][b];
}
}
}
for (d = 0; d < 6; d++)
{
sum = 0;
for (e = 0; e < 10; e++)
{
if (ADC_collect[e][d] != max[d] && ADC_collect[e][d] != min[d]) //除去最大值和最小值,然后平均
{
sum += ADC_collect[e][d];
}
}
Average[d] = sum/8;
}
for(i=0;i<6;i++)
{
AD[i] =(int)(99*(Average[i]-min[i])/(max[i]-min[i])+1); //归一化
}
}
3.舵机(MG995)
舵机的原理无非就是通过占空比来控制他的转动角度(我这里将角度转换成了占空比,大家也可以直接输入相应的占空比,如果我没有记错的话应该是150-250)
计算公式
舵机原理
代码:
1.先配置定时器来生成pwm
2.然后将角度转换成占空比
void Servo_Control(uint16_t angle)
{
float temp;
temp = angle / 9 * 10 + 50;
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, (uint16_t)temp);
}
4.pid算法
对处理过后的数据进行pid算法,来改变舵机的角度,我这里转弯只使用了舵机,有时间的同学可以在加一个差速转弯,这样可可以更丝滑的转弯。
1.先进行比较可以观测小车偏离中心的大小
void get_channel(void)
{
float rate[6] = {1,1,1,1,1,1};
date = (rate[0] * AD[0]) + (rate[1] * AD[1]) + (rate[2] * AD[2] ) - (rate[3] * AD[3]) - (rate[4] * AD[4])-(rate[5]* AD[5]);
}
2.然后就是pid算法函数(这是还没有调式的代码,大家可以参考一下)
float calculatePID(float date)
{
int data = 1000;
float kp = 0.1, ki = 0.01, kd = 0.05, err = 0;
float target = 0, result =0,kp1 = 0,ki1 = 0,kd1 = 0;
static float sum_err = 0,last_err = 0;
err = date - target;
kp1 = kp*err; //比例项
sum_err += err;
if(sum_err > data)
{
sum_err = data;
}
else if(sum_err < -data)
{
sum_err = -data;
}
ki1 = ki*sum_err; //积分项
kd1 = kd*(err-last_err);
last_err = err;
result = kp1 + ki1 + kd1;
return result;
}
3.通过pid函数控制舵机转弯
void pidController(void)
{
float err = date;
float adjustment = calculatePID(err);
int angle = 90;
angle += adjustment;
}
5.驱动(这里只是最简单的驱动,没有使用差速转弯,只是给了个固定格式,后面有时间我会把在弯道和直线的不同速度代码写一下)
1. tb6612http://t.csdnimg.cn/ziViu
申明一下:我使用的是tb612,emmmm:其实l298n也是相同的,我感觉两个差不多,其实我更推荐大家使用tb612,其要稳定,效率高一点;
功能 | 引脚 | 引脚 | 功能 |
A控制信号输入 | pwma | vm | 电机驱动电压输入端(4.5V-13.5V |
A电机输入端2 | ain2 | vcc | 逻辑电平输入端(2.7V-5.5V) |
A电机输入端1 | ain1 | gnd | 接数字地 |
B控制信号输入 | pwmb | bo1 | B电机输出端1 |
B电机输入端2 | bin2 | bo2 | B电机输出端2 |
B电机输入端1 | bin1 | ao2 | A电机输出端2 |
正常工作、待机状态控制端 | stby | ao1 | A电机输出端1 |
模拟接地 | pgnd1 | pgnd2 | 接模拟地 |
2. 正反转和pwm
1.定时器配置
大家可以在gpio中改一下名字好辩认
2. 正反转
将4个端口配置为输出模式,然后可以改一下名3.
3.这样所有配置就完成了,接下来是驱动的代码
正反转:
void LeftMotor_Go() //左电机正转
{
HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET);
}
void LeftMotor_Back() //左电机反转
{
HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_SET);
}
void LeftMotor_Stop() //左电机停止
{
HAL_GPIO_WritePin(AIN1_GPIO_Port, AIN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(AIN2_GPIO_Port, AIN2_Pin, GPIO_PIN_RESET);
}
void RightMotor_Go() //右电机正转
{
HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_SET);
HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET);
}
void RightMotor_Back() //右电机反转
{
HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_SET);
}
void RightMotor_Stop() //右电机停止
{
HAL_GPIO_WritePin(BIN1_GPIO_Port, BIN1_Pin, GPIO_PIN_RESET);
HAL_GPIO_WritePin(BIN2_GPIO_Port, BIN2_Pin, GPIO_PIN_RESET);
}
pwm(这段是csdn其他大佬写的,我整理了一下,如有侵权,请联系我)
void MotorControl(char motorDirection, int leftMotorPWM, int rightMotorPWM)
{
switch (motorDirection)
{
case 0:
LeftMotor_Go();
RightMotor_Go();
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, leftMotorPWM);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, rightMotorPWM);
break;
case 1:
LeftMotor_Back();
RightMotor_Back();
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, leftMotorPWM);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, rightMotorPWM);
break;
case 2:
LeftMotor_Stop();
RightMotor_Stop();
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_4, 0);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);
break;
default:
break;
}
}
6.oled屏幕
我把oled的标准代码放在百度网盘了,需要的可以自己提取
链接:https://pan.baidu.com/s/1D3wrXnbXgj8hIUDZAxwVzA
提取码:x8b8
7.下面是主函数代码
#include "main.h"
// Device header
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
#include "turn.h"
#include "motor.h"
#include "PID.h"
#include "oled.h"
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/
ADC_HandleTypeDef hadc1;
TIM_HandleTypeDef htim1;
TIM_HandleTypeDef htim2;
TIM_HandleTypeDef htim3;
UART_HandleTypeDef huart1;
/* USER CODE BEGIN PV */
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);
static void MX_TIM1_Init(void);
static void MX_ADC1_Init(void);
static void MX_USART1_UART_Init(void);
static void MX_TIM3_Init(void);
/* USER CODE BEGIN PFP */
void SystemClock_Config(void);
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
uint16_t ADC_Value[10][5] = {0};
uint16_t ADC_collect[10][6] = {0};
int angle = 90;
uint16_t AD[6] = {0};
int i = 0;
char ad1,ad2,ad3,ad4,ad5,ad6;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{
pidController();
Servo_Control(angle);
MotorControl(0, 5000, 5000); //直行
HAL_Delay(200);
for (int a = 0; a < 10; a++)
{
for (i = 0; i < 6; i++)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 50); //等待转换完成
ADC_collect[a][i] = HAL_ADC_GetValue(&hadc1);
}
HAL_ADC_Stop(&hadc1);
}
}
}
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM2_Init();
MX_TIM1_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
OLED_Init();
OLED_Clear();
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
HAL_ADCEx_Calibration_Start(&hadc1); //校准adc
OLED_ShowString(1,1,"ad1:",12);
OLED_ShowString(1,14,"ad2:",12);
OLED_ShowString(1,27,"ad3:",12);
OLED_ShowString(1,40,"ad4:",12);
OLED_ShowString(1,53,"ad5:",12);
OLED_ShowString(1,66,"ad6:",12);
OLED_Refresh();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
AD_value();
OLED_ShowNum(13, 1, AD[0], 4, 12);
OLED_ShowNum(13, 14, AD[1], 4, 12);
OLED_ShowNum(13, 27, AD[2], 4, 12);
OLED_ShowNum(13, 40, AD[3], 4, 12);
OLED_ShowNum(13, 53, AD[4], 4, 12);
OLED_ShowNum(13, 66, AD[5], 4, 12);
}
/* USER CODE END 3 */
}
8.声明
这只是我的初步思路,如有不正确的地方请指正,另外,在上述代码中,扫描和限幅那一块还可以优化,(学长说是屎山代码55555555),大家看看思路就行,我会在比赛后将我的优化代码和功能性代妈(如环岛,干簧管停车等),发出(其实是还没有写)