此次使用的标准库非hal库进行的编程
包括以下几个方面的学习:
目录
大概时间:
10.7-10.20写完按键,pwm,测速并进行串口速度的读取,在此期间还写了一个用于调档的函数,由于最后是需要pid调节的,当时觉得麻烦就又删掉了。
10.20-10.25受到学长启发,在控制版上焊接集成了stm32,按键,电池电源开关,接地排针群等。后来在调试期间由于板子线路问题以及电容的损坏驱动板不工作不得不在嘉立创重新打板。
11.4-11.9结合oled与上位机的使用,可以更便捷的调节pid,由于自身焊接的失误导致驱动板线路部分虚焊,又拖了几天。因为pid等其实在10.25日左右就已经完成,所以最后几天只是在琢磨硬件的问题所在。
按键
按键只需要设置设置上拉输入,同时接地,在读取的过程中记得消抖就ok了。
调试过程中出现某个引脚一直拉高,以为是虚焊或者是按键的问题,但是这种概率很小,上网查说某些引脚确实更改不了,但我的是出现在后来写完串口代码的,所以就当是积累了。
其实是因为我把按键初始化代码给注释了。。。
(33条消息) STM32F1的IO口不能输出高低电平的情况_五月525的博客-CSDN博客
pwm
因为是驱动一个电机,所以我将其中一个接口固定为高电平或者低电平,另一个接口进行pwm波的输出,这样通过按键的读取可以实现档速的调整。
因为是减速电机,转速在调到20%占空比时其实就有点没力气了。
相关公式:
PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
PWM占空比: Duty = CCR / (ARR + 1)
PWM分辨率: Reso = 1 / (ARR + 1)
代码如下
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
/*复用推挽输出:对于普通的推挽/开漏输出,引脚的控制权是来自于输出数据寄存器的,想让定时器来控制引脚,
使用复用,输出控制权转移给片上外设--TIM2的CH1通道*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,此时是1分频。
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 36 - 1; //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //高级定时器的重复计数器
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
TIM_OC3Init(TIM2, &TIM_OCInitStructure); //Tim2的oc3通道
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompare3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare);
}
测速
这个我用了一个定时器和一个外部中断,定时器用来每秒读取ccr的值,外部中断读取脉冲。
同时在定时器函数里面加入了pid的调节,每个周期内都进行pid。
此次采用的是M法测速,误差在低速状态下会比较大,这也是导致pid调节不能完全趋于平稳的原因之一。建议是用T法测速这种减速电机,本人在日后会更新。
#include "stm32f10x.h" // Device header
uint16_t CountSensor_Count;//读取脉冲数
void Detect_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource11);//AFIO的引脚选择
EXTI_InitTypeDef EXTI_InitStructrue;
EXTI_InitStructrue.EXTI_Line = EXTI_Line11;
EXTI_InitStructrue.EXTI_LineCmd = ENABLE;
EXTI_InitStructrue.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructrue.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructrue);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//整个工程里面只要配置一次就行了
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI15_10_IRQHandler(void) //外部中断线0服务函数
{
if(EXTI_GetITStatus(EXTI_Line11) == SET) //这里判断检测到的是否是上升沿
{
CountSensor_Count ++;
EXTI_ClearITPendingBit(EXTI_Line11);//清除LINE上的中断标志位
}
}
/*
封装脉冲读取函数,同时清零读取数
*/
int Read_Number(void)
{
int Number;
Number=CountSensor_Count;
CountSensor_Count=0;
return Number;
}
#include "stm32f10x.h" // Device header
#include "Detect.h"
#include <stdio.h>
#include "PID.h"
extern uint16_t number;//读取脉冲数
extern int8_t S;//基础占空比
extern int8_t change;//pid增量
extern uint8_t SetSpeed;
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //时钟分频,此时是1分频。
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //ARR 自动重装
TIM_TimeBaseInitStructure.TIM_Prescaler = 2700- 1; //PSC 计数标准频率 72M/72=1MHz
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //高级定时器的重复计数器
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
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)
{
if(TIM_GetITStatus(TIM3, TIM_IT_Update)==SET)
{
number = Read_Number();
// printf("%f",number*3*3.14*1.69/20);//当前速度为:
printf("%d",number);//当前读取脉冲数:
// printf("s=%d\r\n",S);
if(S!=0)
{
change = PID_realize(number*3*3.14*1.69/20,SetSpeed);//有电35,没电30
}
else
change = 0;
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// printf("change:%d\r\n",change);
// printf("Kp=%f\r\n",pid.Kp);
// printf("Ki=%f\r\n",pid.Ki);
// printf("Kd=%f\r\n",pid.Kd);
}
}
串口的配置
学习刚开始想输出几个字符却发现老是输出乱码,很郁闷,但是跟着江科大自化协的视频配置好串口的初始化也能打印出东西了
有时候会数据失真,但是调波特率到别的值再调回来又能打印正确的值。。暂时不知道这个原理,在烧录时如果先复位再烧录会大大降低失真概率。
利用的是PA9和PA10。
[9-3] 串口发送&串口发送+接收_哔哩哔哩_bilibili
(33条消息) 单片机串口通信中换行的两种方法_si_zhou_qun_84342712的博客-CSDN博客_串口换行符
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include "PID.h"
extern int8_t S;
extern uint8_t SetSpeed;
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //这边usart1是APB2的外设,其他的都是APB1的
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 = 115200; //波特率的数值
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; //停止位
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); //数据寄存器空标志位,标志位在下次输入数据前会自动清0;
}
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for(i=0; i < Length; i++)
{
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String)
{
uint8_t i;
for(i=0; String[i] != '\0'; i++)
{
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while(Y--)
{
Result *= X;
}
return Result;
}
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');
}
}
void Serial_LinkBreak(void)
{
Serial_SendByte('\r');
Serial_SendByte('\n');
}
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
// Serial_SendByte('A');
// uint8_t Array[] = {'B','C','D','E'};
// Serial_SendArray(Array,4);
// Serial_SendString("Hello World!\r\n");//换行符2个缺一不可
// Serial_SendNumber(2903, 4);
// Serial_LinkBreak();
void StateChange(void) //扫描方式读取串口,上位机的相关配置
{
u8 res;
res= USART_ReceiveData(USART1);
// USART_SendData(USART1,res);
if(res=='a') SetSpeed+=10;//期望值++
else if(res=='b') SetSpeed-=10;//期望值--
else if(res=='c') pid.Kp+=0.01;//P++
else if(res=='d') pid.Ki+=0.0001;//I++
else if(res=='e') pid.Kd+=0.01;//D++
else if(res=='f') pid.Kp-=0.01;//P--
else if(res=='g') pid.Ki-=0.0001;//I--
else if(res=='h') pid.Kd-=0.01;//D--
else if(res=='i') S=90;//启动电机
else if(res=='k') S=0;//关闭电机
}
void USART1_IRQHandler(void) //串口1中断服务程序
{
if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
{
StateChange();
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
其实上面的还用的少,最好是将fputc重定向能够使用printf比较方便,
在keil中想添加.h文件时会没有反应,需要手动添加,不知道是什么bug导致的。
oled模块的配置
(参考江科大自化协STM32入门教程-2022持续更新中_哔哩哔哩_bilibili)
此处省略
PID的编写
一开始先加大比例P,p小了会达不到目标速度,会差很多,所以再加大p,知道电机出现‘嗒嗒嗒’的抖动或者观察上位机的波形剧烈抖动的时候,这时候p就过大了,实际上p可以不是很大,比如0.52,调试时候增加幅值可以设为0.01,I稍微来点就可以,平衡车的工程经验是ki=kp/200,但是这里我给的是0.004,还要看具体情况,积分参数过大,实际速度和目标速度的静差会很大。
因为pid结构体是要用于全局变量,所以需要是在.h文件里面进行extern声明
#include "stm32f10x.h" // Device header
typedef struct _pid
{
float SetSpeed; //定义设定值
float err; //定义偏差值
float err_next; //定义上一个偏差值
float err_last; //定义最上前的偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
}PID;
PID pid;
void PID_init(void)
{
pid.SetSpeed=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.err_next=0.0;
pid.Kp=0.52;
pid.Ki=0.00475;
pid.Kd=0.18;
}
int8_t PID_realize(float ActualSpeed,float speed)
{
pid.SetSpeed=speed;
pid.err=pid.SetSpeed-ActualSpeed;
float
incrementSpeed=pid.Kp*(pid.err-pid.err_next)+pid.Ki*pid.err+pid.Kd*(
pid.err-2*pid.err_next+pid.err_last);
pid.err_last=pid.err_next;
pid.err_next=pid.err;
return (int8_t)incrementSpeed;
}
最后的波形: