STM32平衡小车学习总结

目录

STM32f103c8t6引脚功能图: 

1. stm32——GPIO工作模式

输入浮空:

 输入上拉:

输入下拉:

 模拟输入:

 开漏输出:

 开漏复用功能:

 推挽式输出:

 推挽式复用功能:

2. 编码器

正交编码器:

示例代码:

3. AFIO时钟

4. EXTI和NVIC

5. PID

5.1  P 控制

5.2  I 控制

5.3  D 控制

5.4  PID 控制数学模型

5.4.1 Sk、Dk的确定

5.4.2 位置式PID数学模型

5.4.3 增量式PID

5.5 PID示例代码

位置式PID代码

速度闭环控制--简单的增量式PI控制代码:

 5.6 PID调参

PID调参之试凑法

6. 直立环、速度环、串级PID

7. 位带操作(不常用)

实例代码:

8. IIC

IIC驱动代码

9. keil MDK编译器内存分配


STM32f103c8t6引脚功能图: 

STM32平衡小车学习总结_第1张图片

1. stm32——GPIO工作模式

外设 GPIO_MODE
LED GPIO_Mode_OUT_PP,推挽输出
KEY GPIO_Mode_IPU,输入上拉
ADC GPIO_Mode_AIN,模拟输入
PWM GPIO_Mode_AF_PP,复用推挽式输出
USART_TX GPIO_Mode_AF_PP
USART_RX GPIO_Mode_IN_FLOATING,输入浮空
IIC GPIO_Mode_Out_PP
EXTI GPIO_Mode_IPU
USB GPIO_Mode_IPD,输入下拉
超声波模块 GPIO_Mode_Out_PP
电机 GPIO_Mode_Out_PP

STM32的8种GPIO输入输出模式深入详解

 应用总结:

  • (1)上、下拉输入可以用来检测外部信号,如:按键。
  • (2)浮空输入,由于输入阻抗较大,一般用于通信协议I2C、USART的接收端。
  • (3)普通推挽输出模式一般应用在输出电平为0和3.3V的场合。
  • (3)普通开漏输出模式一般应用在电平不匹配的场合,如需要输出5V高电平,就要在外部加一个上拉电阻,电源为5V,把GPIO设为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出5V电平。
  • (4)对于相应的复用模式,则是根据GPIO的复用功能来选择。如GPIO的引脚用作串口输出(USART/SPI/CAN),则使用复用推挽输出。
  • (5)在使用任何一种开漏模式时,都要接上拉电阻。

输入模式:

  1. 输入浮空:GPIO_Mode_IN_FLOATING
  2. 输入上拉:GPIO_Mode_IPU,IO内部上拉电阻输入
  3. 输入下拉:GPIO_Mode_IPD,IO内部下拉电阻输入
  4. 模拟输入:GPIO_Mode_AIN

输出模式:

  1. 开漏输出:GPIO_Mode_Out_OD
  2. 开漏复用功能:GPIO_Mode_AF_OD,片内外设功能(TX1,MOSI,MISO,SCK,SS)
  3. 推挽式输出:GPIO_Mode_Out_PP
  4. 推挽式复用功能:GPIO_Mode_AF_PP,片内外设功能(I2C的SCL,SDA)

输入浮空:

逻辑器件与引脚既不接高电平,也不接低电平,呈高阻态。浮空最大的特点就是电压的不确定性。

STM32平衡小车学习总结_第2张图片

 输入上拉:

上拉就是将不确定的信号通过一个电阻嵌位在高电平,电阻同时起到限流的作用。

STM32平衡小车学习总结_第3张图片

输入下拉:

就是把电压拉低至GND。

STM32平衡小车学习总结_第4张图片

 模拟输入:

模拟输入是指传统方式的输入,数字输入是输入PCM数字信号,即0,1的二进制数字信号,通过数模转换转换成模拟信号,经前级放大进入功率放大器,功率放大器还是模拟的。

STM32平衡小车学习总结_第5张图片

推挽输出和开漏输出

 开漏输出:

输出端相当于三极管的集电极,开漏引脚不连接外部上拉电阻时,只能输出低电平;要得到高电平状态需要上拉电阻才行。

STM32平衡小车学习总结_第6张图片

STM32平衡小车学习总结_第7张图片

 开漏复用功能:

可以理解为GPIO口被用作第二功能时的配置情况(并非作为通用IO口使用)。端口必须配置成复用功能输出模式(推挽或开漏)。

STM32平衡小车学习总结_第8张图片

 推挽式输出:

推挽电路示例:

注意上面是N型,下面是P型。
当Vin电压为V+时,上面的N型三极管控制端有电流输入,Q3导通,于是电流从上往下通过,提供电流给负载。这就叫推

当Vin电压为V-时,下面的三极管有电流流出,Q4导通,Q4有从上到下的电流流过。经过下面的P型三极管提供电流给负载,这就叫挽

STM32平衡小车学习总结_第9张图片

可以输出高、低电平;推挽结构一般是指两个三极管或MOSFET(NMOS、PMOS)分别受到互补信号的控制,总是在一个导通时另一个截止。

推拉式输出既可以提高电路的负载能力,又提高开关速度。

STM32平衡小车学习总结_第10张图片

 推挽式复用功能:

可以理解为GPIO口被用作第二功能时的配置情况(并非作为通用IO口使用)。

STM32平衡小车学习总结_第11张图片

2. 编码器

STM32定时器配置为编码器模式(看这个就行)

STM32定时器---正交编码器模式详解

STM32编码器模式详解(一)---理论

 速度闭环控制就是根据单位时间获取的脉冲数测量电机的速度信息,并与目标值进行比较,得到控制偏差,然后通过对偏差的比例、积分、微分进行控制,使偏差趋近于0的过程。

由于需要知道速度,所以就需要用到编码器,可通过单片机定时器的捕获模式来得到速度,之后在单片机内部进行PID运算,得到输出需要的速度,通过控制占空比来输出PWM波,控制电机的速度。

正交编码器:

一般是5根线连接,信号线分别为A,B,Z,Vcc,GND。A、B两个信号相位差为90°。

  • A、B脉冲输出。编码器的输出一般是开漏的,所以单片机的IO是上拉输入状态。
  • Z零点信号。当编码器旋转到零点时,Z信号会发出一个脉冲表示现在是零位置。
  • Vcc通常为24V或5V。

编码器线数:旋转一圈A或B会输出多少个脉冲。

转一圈 A 和 B 发出的脉冲数是一样的,不过存在90°的相位差。

编码器通常都是360线的,线数越高代表编码器能够反应的位置精度越高。

360线的,A、B一圈各为360个,Z信号为一圈一个。

可以根据两个信号的先后来判断方向;根据每个信号脉冲数量的多少及整个编码轮的周长就可以计算出当前行走的距离;如果再加上定时器还可以计算出速度。 

STM32平衡小车学习总结_第12张图片

编码器有转速上限,超过这个上限是不能正常工作的,这个是硬件的限制。原则上线数越多转速就越低。

编码器接口模式下,定时器寄存器相关:

定时器初始化好以后,任何时候计数器TIMx_CNT的值就是编码器的位置信息。正转加,反转减。

初始化时给的TIM_Period值赋给TIMx_ARR寄存器,即计数器只在0到TIM_Period之间连续计数。

根据两个输入信号(TI1&TI2)的跳变顺序,产生了计数脉冲和方向信号,依据两个输入信号的跳变顺序,计数器向上或向下计数,同时硬件对TIMx_CR1寄存器的DIR位进行相应的设置。

不管计数器是依靠TI1计数、依靠TI2计数或者同时依靠TI1和TI2计数,任一输入端(TI1或者TI2)的跳变都会重新计算DIR位。
 

每个定时器的输入脚可以通过软件设定滤波。

通过数据手册,定时器1,2,3,4,5,8有编码器功能。编码器输入信号TI1、TI2经过输入滤波,边沿检测产生TI1FP1、TI2FP2

TImFPn:m代表滤波和边沿检测前的输入通道号,n代表经过滤波和边沿检测后将要接入或者说要映射到的捕捉通道号。

eg:

(1)TI1FP1,来自通道TI1,经滤波器后将接到捕捉比较信号通道IC1。

(2)TI1FP1和TI1FP2:二者都来自TI1通道,经滤波和边沿检测后产生具有相同特征的信号, 然后映射到不同的输入捕捉通道,本质上还是一路信号。如果没有滤波和变相,则TI1FP1 = TI1FP2 = TI1。

 一般编码器都有AB两相,需要接到定时器的两个通道上。对STM32而言只有TIMx_CH1和TIMx_CH2支持编码器模式(如下定时器的时钟框图所示)。正交编码接口用到的信号是TI1FP1和TI2FP2,因此编码器模式下定时器通道的选择上一定要注意。

STM32平衡小车学习总结_第13张图片

下面来看定时器编码接口函数: 

//使用编码器模式3:上升下降都计数
TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);

STM32平衡小车学习总结_第14张图片

STM32平衡小车学习总结_第15张图片

相对信号电平:不考虑反向的情况下,TI1FP1的相对信号就是TI2,TI2FP2的相对信号就是TI1。

eg:

假设选择正交编码器要同时对TI1和TI2进行计数的模式,不做滤波和换相。

那么当TI1FP1的上升沿到来时,如果此时其相对信号TI2的电平为低,则计数器向上计数;如果此时其相对信号TI2的电平为高,则计数器向下计数。

示例代码:

/**************************************************************************
函数功能:把TIM4初始化为编码器接口模式
入口参数:无
返回  值:无
**************************************************************************/
void Encoder_Init_TIM4(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;  
    TIM_ICInitTypeDef TIM_ICInitStructure;
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//使能TIM4的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//使能PB端口时钟
    //PB6:TIM4_CH1 PB7:TIM4_CH2
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;	//端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
    GPIO_Init(GPIOB, &GPIO_InitStructure);					      //根据设定参数初始化GPIOB

    TIM_TimeBaseStructInit(&TIM_TimeBaseStructure);
    TIM_TimeBaseStructure.TIM_Prescaler = 0x0; // 预分频器 
    TIM_TimeBaseStructure.TIM_Period = ENCODER_TIM_PERIOD; //设定计数器自动重装值
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;//选择时钟分频:不分频
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM向上计数  
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);
    TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);//使用编码器模式3:上升下降都计数
    
    TIM_ICStructInit(&TIM_ICInitStructure);   //将结构体中的内容缺省输入
    TIM_ICInitStructure.TIM_ICFilter = 10;    //选择输入比较滤波器
    TIM_ICInit(TIM4, &TIM_ICInitStructure);   //将TIM_ICInitStructure中的指定参数初始化为TIM3
    
    TIM_ClearFlag(TIM4, TIM_FLAG_Update);      //清除TIM4的更新中断标志位
    TIM_ITConfig(TIM4, TIM_IT_Update, ENABLE); //配置更新中断标志位
    //Reset counter
    TIM_SetCounter(TIM4,0); //清零定时器计数值
    TIM_Cmd(TIM4, ENABLE); 
}


/**************************************************************************
函数功能:编码器速度读取函数,单位时间读取编码器计数
入口参数:定时器
返回  值:速度值
**************************************************************************/
int Read_Speed(u8 TIMx)
{
    int Encoder_TIM;
    switch(TIMx)
    {
        case 2: 
            Encoder_TIM = (short)TIM_GetCounter(TIM2);   //采集编码器的计数值并保存
            TIM_SetCounter(TIM2, 0); //将定时器的计数值清零
          break;
        case 3: 
            Encoder_TIM = (short)TIM_GetCounter(TIM3);
            TIM_SetCounter(TIM3, 0);
          break;
        case 4: 
            Encoder_TIM = (short)TIM_GetCounter(TIM4);
            TIM_SetCounter(TIM4, 0);
          break;
        default:
            Encoder_TIM = 0;
    }
    return Encoder_TIM;
}

/**************************************************************************
函数功能:TIM4中断服务函数
入口参数:无
返回  值:无
**************************************************************************/
void TIM4_IRQHandler(void)
{
    if(TIM_GetITStatus(TIM4, TIM_IT_Update) != 0)
    {
        TIM_ClearITPendingBit(TIM4, TIM_IT_Update);
    }
}

3. AFIO时钟

STM32 中的大部分 GPIO 都有复用功能,所以对于有复用功能的 I/O 引脚,还要开启其复用功能时钟。如 GPIO 的 pin4 可以用作 ADC1 的输入引脚,当我们把它作为 ADC1 使用时,需要开启 ADC1 的时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

另外, STM32 的所有 GPIO 都引入到 EXTI 外部中断线上,使得所有的 GPIO 都能作为外部中断的输入源。所以如果把 GPIO 用作 EXTI 外部中断时,还需要开启 AFIO 时钟:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

也就是说,当需要配置 AFIO 的这些寄存器时,就需要把 RCC_APB2ENR 寄存器的AFIO位 “置1”,打开AFIO时钟。

eg: AFIO_EXTICRX 用于选择 EXTIx 外部中断的输入源。

4. EXTI和NVIC

STM32--EXTI和NVIC的关系

STM32平衡小车学习总结_第16张图片

 示例代码:

/**************************************************************************
函数功能:外部中断初始化
入口参数:无
返回  值:无 
作    用:是用来配置MPU6050引脚INT的,每当MPU6050有数据输出时,引脚INT有相应的电平输出。
					依次来触发外部中断作为控制周期。保持MPU6050数据的实时性。
**************************************************************************/
void MPU6050_EXTI_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    EXTI_InitTypeDef EXTI_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;
    //
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;	            //端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;           //上拉输入
    GPIO_Init(GPIOB, &GPIO_InitStructure); 
    
    //将GPIO管脚与外部中断线连接,用于配置EXTI外部中断/事件的GPIO中断源,
    //实际是设定外部中断配置寄存器的AFIO_EXTICRx值,
    //第一个参数指定GPIO端口源,第二个参数为选择对应GPIO引脚源编号。
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource5);
    
    EXTI_InitStructure.EXTI_Line = EXTI_Line5;     //中断/事件线
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;         //EXTI使能
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;   //触发类型:产生中断或者产生事件 
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发
    EXTI_Init(&EXTI_InitStructure);
    
    //外部中断5优先级配置也就是MPU6050 INT引脚的配置.
    ///因为是控制中断,故此优先级应是最高。
    NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;     //使能外部中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;  //抢占优先级0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01;         //子优先级1
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;   //使能外部中断通道          
    NVIC_Init(&NVIC_InitStructure);
}

5. PID

PID控制器

PID调参之“慢慢来,会很快”

工程中, P必然存在,在P的基础上又有如PD控制、PI控制、PID控制。

比例项:提高响应速度,减小静差。

积分项:消除稳态误差。即只要有误差,我就进行积分运算,直到偏差变为0。

微分项:减小震荡以及超调。

5.1  P 控制

P_{out}= K_p*E_k+out_{0}

其中,

Kp---放大系数,可以认为是一个放大器或衰减器,其作用就是用于整个比例控制的增益。

Ek---当前偏差值。

out0---维持输出,避免偏差为0时系统零输出导致失控。

特点:

比例控制遵循的原则是:有偏差我才控制,没有偏差就不控制了。

PWM的无级调节:可以理解为PID输出是多少就加载到负载上多少。

eg:

T为周期,Ton为导通时间,Toff为关断时间,T=Ton+Toff。

假设周期T=1000ms,若Ton=300,就代表执行元件导通300ms,断开700ms,则此时的输出就是Uout = (300/1000) * U。

映射到比例控制中就是,Ek越大,则Pout越大,脉宽越宽,输出电压越高。

5.2  I 控制

{X(1)、X(2)、X(3)、X(4)……………X(k-2)、X(k)}    

上述数列为,从开机以来,传感器的所有采样点的数据序列。X(1)为开机第一秒的反馈值,X(k)为当前时刻的反馈值。
我们将开机以来每个反馈值与用户期望值Sv相减,即可得出对应的偏差序列:

{E(1)、E(2)、E(3)、E(4)…………E(k-2)、E(k-1)、E(k)}    //历史所有采样点的偏差序列。

将这些历史偏差求代数和:

Sk=E(1)+E(2)+E(3)+......+E(k-1)+E(k);   //反映的是历史上偏差值的整体情况。

由上分析可得:

  • Sk>0:在过去时间,大多数是未达标的,即整体是未达标的。
  • Sk=0:在过去时间,控制效果整体是比较合理的。
  • Sk<0:在过去时间,大多数是超标的。

则积分控制器公式为:

I_{out}= K_p*S_k+out_{0}

即,历史上的整体偏差情况如果都是不达标的,那么我积分控制器就会有一个正的输出量使得你输出继续增强;若你历史上整体偏差情况是超标的,那么我就输出一个负的输出量使你降低输出。

这些正正负负的历史偏差全部加起来,会慢慢正负抵消,直至抵消至0。

特点:

积分控制遵循的原则是:你现在是不是好的我不管,我只看你之前整体怎么样。

还有一个作用就是:通过历史上的数据,预测出控制对象有可能会发生的变化,提前做出控制,将问题控制在没有发生之前。

5.3  D 控制

{X(1)、X(2)、X(3)、X(4)……………X(k-2)、X(k)}    

上述数列为,从开机以来,传感器的所有采样点的数据序列。X(1)为开机第一秒的反馈值,X(k)为当前时刻的反馈值。
我们将开机以来每个反馈值与用户期望值Sv相减,即可得出对应的偏差序列:

{E(1)、E(2)、E(3)、E(4)…………E(k-2)、E(k-1)、E(k)}    //历史所有采样点的偏差序列。

可得,当前时刻的偏差与前一时刻的偏差之差为:

Dk = E(k) - E(k-1);     //差值的差,反映的是两个时刻的偏差的变化趋势。

由上分析可得:

  • Dk > 0:这个时刻的偏差比上一时刻的偏差还要大,所以偏差为增大趋势,系统状态越来越偏离目标。此时可以试想一下,上次偏差小,本次偏差大,若此时建立以时间t为x轴、偏差E为y轴的坐标系,便能发现两次偏差的斜率为正,且Dk越大越陡。
  • Dk = 0:两个时刻的偏差相对没有改变,两次偏差值相等。系统无变化。
  • Dk < 0:这个时刻的偏差比上一时刻的偏差小,所以偏差为减小趋势,系统状态越来越接近目标。斜率为负,且Dk越小越陡。

则微分控制器公式为:

D_{out}= K_p*D_k+out_{0}

微分控制器同样可通过加维持输出out0,以消除零输出系统失控的现象。

5.4  PID 控制数学模型

由上述得到简单的PID数学模型为:

PID_{out}= K_p*(E_k+S_k+D_k)+out_{0}

5.4.1 Sk、Dk的确定

(1)Sk的确定:

S_{k}= \frac{1}{T_{i}}*T*\sum_{k=0}^{n}E_k

其中,

Ti---积分时间常数,数学上指积分项运行的时间,物理上指考察历史数据的范围大小;

T---采样周期/计算周期,即多长时间算一次PID,更新PID的间隔时间。

Ti越大,则积分项输出就越小。

(2)Dk的确定:

D_{k}= T_{d}*\frac{E_{k}-E_{k-1}}{T}

其中,

Td---微分时间常数,微分项运行的时间;

T---采样周期/计算周期,即多长时间算一次PID,更新PID的间隔时间。

① Td越大,则微分项输出就越大。

② 偏差不变的情况下,T越大,偏差变化的斜率越小,导致系统对变化趋势不敏感,因此计算周期T不能过大。

STM32平衡小车学习总结_第17张图片

5.4.2 位置式PID数学模型

PID_{out}=(K_p*E_k)+(K_p* \frac{T}{T_{i}}*\sum_{k=0}^{n}E_k)+(K_p*T_{d}*\frac{E_{k}-E_{k-1}}{T})+out_{0}

此时经过PID计算得出的 PIDout 就是本周期的控制输出,也是加载到执行机构上的脉宽值。

比如,满PWM为1000的情况下,本次输出PIDout=200,则本周期的控制占空比=200/1000。

5.4.3 增量式PID

它计算出来的是控制量的改变值,即

\Delta out=out_{k}-out_{k-1}

又因为,

out_{k-1}=(K_p*E_{k-1})+(K_p* \frac{T}{T_{i}}*\sum_{k=0}^{n-1}E_k)+(K_p*\frac{T_{d}}{T}*(E_{k-1}-E_{k-2}))+out_{0}

则得出,

\Delta out=(K_p*(E_{k}-E_{k-1}))+(K_p* \frac{T}{T_{i}}*E_k)+(K_p*\frac{T_{d}}{T}*(E_{k}-2E_{k-1}+E_{k-2}))

5.5 PID示例代码

位置式PID代码

#ifndef __PID_H
#define __PID_H

#include "stm32f10x_conf.h"

typedef struct
{
    float Sv;    //用户设定值
    float Pv;    //当前值
    
    float Kp;    //用户设置的比例系数
    float T;     //PID计算周期--采样周期
    float Ti;    //积分时间常数
    float Td;    //微分时间常数
    
    float Ek;    //本次偏差
    float Ek_1;  //上次偏差
    float SEk;   //历史偏差之和
     
    float Pout;
    float Iout;
    float Dout;
    
    float OUT0;  //
    
    float OUT;   //最终结果
    uint16_t c10ms; //PID运算过程中的定时器
    uint16_t pwmcycle;  //pwm周期
}PID;
extern PID pid;
#endif
#include "pid.h"
#include "stm32f10x.h"

PID pid;

void PID_Init()
{
    // 根据实际情况调整,也可以将这些值初始化为0
    pid.Sv = 120;   //用户设定温度
    pid.Kp = 30;    // 
    pid.T = 500;   //PID计算周期
    pid.Ti = 500000;  //积分时间,取的比较大
    pid.Td = 1000;  //微分时间
    pid.pwmcycle = 200;  //pwm周期为200
    pid.OUT0 = 1;
}

void read_temper()
{
    uint16_t d;
    uint8_t i;
    if(i<20)
        return;
    d = read_max6675();   //6675里读出来的是16位数据,高12位才有效
    pid.Pv = ((d>>4) & 0x0fff)*0.25;  //数值d右移4位,把低4位全去掉
    i = 0;
}

void PID_cal()
{
    float DEK;
    float ti,ki;
    float td,kd;
    float out;
    
    if(pid.c10mspid.pwmcycle)
    {
        pid.OUT = pid.pwmcycle;
    }
    else if(out<0)
    {
        pid.OUT = pid.OUT0;
    }
    else
    {
        pid.OUT = out;
    }
    
    pid.Ek_1 = pid.Ek;  //更新偏差
    
    pid.c10ms = 0;     
}

//定时器中断中调用,每1ms执行1次
void PID_out()    //输出PID运算结果到负载
{
    static uint16_t pw;
    pw++;
    if(pw >= pid.pwmcycle)
    {
        pw = 0;              //pw的取值范围为:0 ~ pwmcycle-1
    } 
    if(pw < pid.OUT)
    {
        //加热
    }
    else
    {
        //停止加热
    }
}

速度闭环控制--简单的增量式PI控制代码:

深入浅出PID控制算法(三)————增量式与位置式PID算法的C语言实现与电机控制经验总结_若爱我菲、-CSDN博客_位置式pid与增量式pid区别

根据增量式PID公式,输出增量式PWM。

在速度控制闭环系统中,只使用PI控制,即

pwm += Kp[Ek - E(k-1)] + Ki*E(k)

int Incremental_PI (int Encoder,int Target)
{   
   float Kp=20,Ki=30;   
     static int Bias,Pwm,Last_bias;         //相关内部变量的定义。
     Bias=Encoder-Target;                //求出速度偏差,由测量值减去目标值。
     Pwm+=Kp*(Bias-Last_bias)+Ki*Bias;   //使用增量 PI 控制器求出电机 PWM。
     Last_bias=Bias;                       //保存上一次偏差 
     return Pwm;                         //增量输出
}

位置闭环控制--PID控制

int Position_PID (int Encoder,int Target)
{   
     float Position_KP=80,Position_KI=0.1,Position_KD=500;
     static float Bias,Pwm,Integral_bias,Last_Bias;
     Bias=Encoder-Target;                                  //求出速度偏差,由测量值减去目标值。
     Integral_bias+=Bias;                                    //求出偏差的积分
     Pwm=Position_KP*Bias+Position_KI*Integral_bias+Position_KD*(Bias-Last_Bias);       //位置式PID控制器
     Last_Bias=Bias;                                       //保存上一次偏差 
     return Pwm;                                           //增量输出
}

积分分离的PID控制算法

 5.6 PID调参

STM32平衡小车学习总结_第18张图片

KpKp太小,系统存在稳态误差(静差);而Kp过大,则会使系统有超调,并且出现震荡(系统惯性)。

比例控制是一种立即控制,只要有偏差,就立即输出控制量。

Ki可以消除稳态误差。Ki过大容易引起震荡,造成超调。

积分控制是一种修复控制,只要有偏差,就会逐渐去往消除偏差的方向去控制。

Kd微分项作为超前控制的主要输出,可以抑制震荡、限制超调、减少调节时间。Kd增大可以加快系统响应,减小超调量,太大的话系统响应又会特别慢。

微分控制是一种提前控制,以偏差的变化率为基准进行控制。

实际的反馈信号往往有噪声,微分控制有失稳的风险,所以用微分时一定要慎重。

一般实际工程控制中,微分控制使用确实较少。假如我们不能加微分调节,还有没有其他办法能让响应曲线更好呢?答案肯定是有的,先介绍其中一种。

把目标转速1000rpm进行一个低通滤波后,再给到PID控制器,选取Kp=0.005,Ki=0.015(参数调试过程同上),仿真结果如下图,响应虽不及PID控制快速,但是与PI控制器相比,超调量大大减小,稳定性有所提高。

PID控制参数调节浅谈 - 知乎

PID调参之试凑法

先比例、后积分、再微分。

(1)首先只调比例部分。将Kp由小变大,观察系统响应,直到得到反应快、超调小的响应曲线。

(2)调Ki,来消除稳态误差。首先把Ki设为一较大值,将将Kp微微缩小(原值的4/5),观察系统响应情况。

(3)调Kd。先把Kd设为一较小值,同时Kp微微减小、Ki微微增大,观察响应情况。然后加大Kd,反复调整Kp、Ki。

即,

Kp由小到大,找出超调小的Kp;

Ki由大变小,适当调整Kp;

Kd由小变大,适当调整Kp和Ki。

6. 直立环、速度环、串级PID

速度环:让电机速度趋近0

直立环:让小车角度趋近0

串级控制系统

STM32平衡小车学习总结_第19张图片

 STM32平衡小车学习总结_第20张图片

 STM32平衡小车学习总结_第21张图片

7. 位带操作(不常用)

在CM3中, 有两个区中实现了位带。其中一个是SRAM区的最低1MB范围,第二个则是片内外设区的最低1MB范围。这两个区中的地址除了可以像普通的RAM一样使用外,还都有自己的“位带别名区”,位带别名区把每个比特膨胀成一个32位的字。当你通过位带别名区访问这些字时,就可以达到访问原始比特的目的。--《CM3权威指南》 

STM32平衡小车学习总结_第22张图片

STM32平衡小车学习总结_第23张图片

实例代码:

//位带操作,实现51类似的GPIO控制功能
//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 
//IO口地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 

#define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 

//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出 
#define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入 

其中,

第一行:得到位带别名区域的32位地址,<<5相当于乘32,<<2相当于乘4

那么如何换算位带区里每一位对应的别名区地址呢?有一个公式可用:

别名区地址 = 别名区起始地址 + (位字节地址偏移量*8+n)*4;(以字节为单位时,n∈[0,7];以字为单位时,n∈[0,31])

则由上图可以得出,片上外设区的别名区地址=0x42000000+(A-0x40000000)*32+n*4,

其中,

A---位带区字节地址(GPIOx_BASE+偏移地址);

n---操作位号;

*32---位带区每一位膨胀为别名区里一个32位的字,1字=4字节=32bit;

*4---1字=4字节

第二行:将第一步得到的地址转换成一个指针变量,并且操作这个地址里的值。出于安全考虑加了volatile。

第三行:根据传入的addr和bitnum计算得到32位的地址,然后强制类型转换,使得我们可以操作这个地址里的值。

设置输入、输出方向寄存器后,给GPIOA的pin1置位复位只需如下操作即可:

PAin(1) = 0; PAin(1) = 1;
PAout(1) = 0; PAout(1) = 1;

8. IIC

IIC详解,包括原理、过程,最后一步步教你实现IIC

1. I2C有两根双向信号线,SDA和SCL。总线上可以挂很多设备,多个主设备,多个从设备。

2. 起始信号和终止信号都是由主机发送的

  • 起始信号:SCL为高电平时,SDA由高向低跳变。
  • 终止信号:SCL为高电平时,SDA由低向高跳变。

3. 数据位的有效性规定:

在I2C总线进行数据传送时,SCL为高电平期间,SDA线上的数据必须保持稳定;只有在SCL为低时,SDA上的高电平或低电平状态才允许变化。

4. 数据传送格式:主机发给从机

每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

5. 应答信号

  • 应答信号包括ACK和NACK。
  • 作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;
  • 若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
  • 传输时主机产生时钟,由于每个周期传输一位01数据,每个数据包包括8位,所以在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。

STM32平衡小车学习总结_第24张图片

 6. 总线寻址:

主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/W位将自己确定为发送器还是接收器。

STM32平衡小车学习总结_第25张图片

7. 每次数据传送总是由主机产生终止信号来结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。

8. 在总线的一次数据传输中,可以有以下几种组合方式:

(1)主机向从机发送数据,数据传送方向在整个过程中不变。

STM32平衡小车学习总结_第26张图片

(2)主机在第一个字节后,立即从从机读取数据(传输方向不变)。

STM32平衡小车学习总结_第27张图片

(3)传送过程中,当需要改变传递方向时,起始信号和从机地址都被重复产生一次,但两次读写方向位正好相反。

IIC驱动代码

首先看下iic.h中的定义:

#ifndef __IIC_H
#define __IIC_H

//IO方向设置
//先把bit12,13,14,15清零,然后bit15置1,表示PB3为输入模式,上拉/下拉输入模式
#define SDA_IN()   {GPIOB->CRL&=0xFFFF0FFF;GPIOB->CRL|=8<<12;}  

//先把bit12,13,14,15清零,然后bit13,12置1,表示PB3为通用推挽输出,最大速度50MHz
#define SDA_OUT()  {GPIOB->CRL&=0xFFFF0FFF;GPIOB->CRL|=3<<12;}

//IO操作函数
#define IIC_SCL     PBout(4)  //SCL
#define IIC_SDA     PBout(3)  //SDA
#define READ_SDA    PBin(3)   //SDA

#endif

下面是iic.c:

//初始化 IIC
void IIC_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //使能PB端口时钟
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;      //推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

//产生 IIC 起始信号
void IIC_Start(void)
{
    SDA_OUT(); //sda 线输出
    IIC_SDA=1;
    IIC_SCL=1;
    //开始信号:SCL为高电平时,SDA由高向低跳变。
    delay_us(4);
    IIC_SDA=0; 
    delay_us(4);
    IIC_SCL=0; //钳住 I2C 总线,准备发送或接收数据
}

//产生 IIC 停止信号
void IIC_Stop(void)
{
    SDA_OUT(); //sda 线输出
    IIC_SCL=0;
    IIC_SDA=0; 
    delay_us(4);
    //结束信号:SCL为高电平时,SDA由低向高跳变。
    IIC_SCL=1;
    IIC_SDA=1; 
    delay_us(4);
}

//等待应答信号到来
//返回值:1,接收应答失败
//       0,接收应答成功
u8 IIC_Wait_Ack(void)
{
    u8 ucErrTime=0;
    SDA_IN(); //SDA 设置为输入
    IIC_SDA=1;
    delay_us(1);
    IIC_SCL=1;
    delay_us(1);
    while(READ_SDA)
    {
        ucErrTime++;
        if(ucErrTime>250)
        {
            IIC_Stop();
            return 1;
        }
    }
    IIC_SCL=0;//时钟输出 0
    return 0;
}

//产生 ACK 应答
void IIC_Ack(void)
{
    IIC_SCL=0;    //只有SCL为低,SDA上的高电平或低电平状态才允许变化
    SDA_OUT();
    IIC_SDA=0;
    delay_us(2);
    IIC_SCL=1;
    delay_us(2); 
    IIC_SCL=0;
}

//不产生 ACK 应答
void IIC_NAck(void)
{
    IIC_SCL=0;
    SDA_OUT();
    IIC_SDA=1;
    delay_us(2);
    IIC_SCL=1;
    delay_us(2);
    IIC_SCL=0;
}

//IIC 发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
    u8 t;
    SDA_OUT();
    IIC_SCL=0; //拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {
        IIC_SDA=(txd&0x80)>>7;
        txd<<=1;
        delay_us(2); 
        IIC_SCL=1;
        delay_us(2);
        IIC_SCL=0;
        delay_us(2);
    }
}

//读 1 个字节:ack=1 时,发送 ACK;ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
    unsigned char i,receive=0;
    SDA_IN(); //SDA 设置为输入
    for(i=0;i<8;i++ )
    {
        IIC_SCL=0;
        delay_us(2);
        IIC_SCL=1; 
        receive<<=1;
        if(READ_SDA)
        {
            receive++;
        }
        delay_us(1);
    }
    if (!ack) 
        IIC_NAck(); //发送 nACK
    else
        IIC_Ack();  //发送 ACK
    return receive;
} 

9. keil MDK编译器内存分配

Keil MDK编译器内存分配

Program Size: Code=37220  RO-data=1836  RW-data=1356  ZI-data=8172  

Code:代码区,程序占用flash的大小,存储在flash。

RO-data:只读数据域,程序定义的常量,存储在flash。

RW-data:初始化为非0值的可读写数据。程序刚运行时这些数据初始值非0,运行过程中常驻RAM区,因而应用程序可以改变其内容。例如:全局变量、静态变量,且定义时赋予非0值给该变量进行初始化。

ZI-data:初始化为0(若定义该变量时没有赋予初始值,编译器会把它当ZI-data对待,初始化为0)的可读写数据域。它与RW-data 的区别就是:程序刚运行时这些数据初始值全为0。运行过程中常驻RAM区,因而应用程序可以改变其内容。例如:全局变量、静态变量。

烧写时被占用的FLASH空间为:ROM(flash) size = Code + RO-data + RW-data 。

程序运行时,芯片内部RAM使用的空间为:RAM size = RW-data + ZI-data 。

 RW-data是在RAM中使用的,为什么需要存储到flash?

因为这部分变量都是由初始值的,如果只存在RAM中,掉电后数据就消失了,所以要在flash存一份。

程序组件 所属类别
机器代码指令 Code 存储在FLASH
常量 RO-data
初值非0 的全局变量 RW-data
初值为0 的全局变量 ZI-data 存储在RAM
局部变量 ZI-data 栈空间
使用malloc 动态分配的空间 ZI-data 堆空间

程序在存储状态时,RO节(RO section)及RW节都被保存在ROM区。

当程序开始运行时,内核直接从ROM中读取代码,并且在执行主体代码前,会先执行一段加载代码,它把RW节数据从ROM复制到RAM, 并且在RAM加入ZI节,ZI节的数据都被初始化为0。

加载完后RAM区准备完毕,正式开始执行主体程序。

你可能感兴趣的:(嵌入式,stm32,单片机,arm)