目录
8. Keil 中的Code,RO-data,RW-data,ZI-data
STM32f103c8t6引脚功能图:
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 |
应用总结:
- (1)上、下拉输入可以用来检测外部信号,如:按键。
- (2)浮空输入,由于输入阻抗较大,一般用于通信协议I2C、USART的接收端。
- (3)普通推挽输出模式一般应用在输出电平为0和3.3V的场合。
- (3)普通开漏输出模式一般应用在电平不匹配的场合,如需要输出5V高电平,就要在外部加一个上拉电阻,电源为5V,把GPIO设为开漏模式,当输出高阻态时,由上拉电阻和电源向外输出5V电平。
- (4)对于相应的复用模式,则是根据GPIO的复用功能来选择。如GPIO的引脚用作串口输出(USART/SPI/CAN),则使用复用推挽输出。
- (5)在使用任何一种开漏模式时,都要接上拉电阻。
输入模式:
- 输入浮空:GPIO_Mode_IN_FLOATING
- 输入上拉:GPIO_Mode_IPU,IO内部上拉电阻输入
- 输入下拉:GPIO_Mode_IPD,IO内部下拉电阻输入
- 模拟输入:GPIO_Mode_AIN
输出模式:
- 开漏输出:GPIO_Mode_Out_OD
- 开漏复用功能:GPIO_Mode_AF_OD,片内外设功能(TX1,MOSI,MISO,SCK,SS)
- 推挽式输出:GPIO_Mode_Out_PP
- 推挽式复用功能:GPIO_Mode_AF_PP,片内外设功能(I2C的SCL,SDA)
输入浮空:
逻辑器件与引脚既不接高电平,也不接低电平,呈高阻态。浮空最大的特点就是电压的不确定性。
输入上拉:
上拉就是将不确定的信号通过一个电阻嵌位在高电平,电阻同时起到限流的作用。
输入下拉:
就是把电压拉低至GND。
模拟输入:
模拟输入是指传统方式的输入,数字输入是输入PCM数字信号,即0,1的二进制数字信号,通过数模转换转换成模拟信号,经前级放大进入功率放大器,功率放大器还是模拟的。
开漏输出:
输出端相当于三极管的集电极,开漏引脚不连接外部上拉电阻时,只能输出低电平;要得到高电平状态需要上拉电阻才行。
开漏复用功能:
可以理解为GPIO口被用作第二功能时的配置情况(并非作为通用IO口使用)。端口必须配置成复用功能输出模式(推挽或开漏)。
推挽式输出:
推挽电路示例:
注意上面是N型,下面是P型。
当Vin电压为V+时,上面的N型三极管控制端有电流输入,Q3导通,于是电流从上往下通过,提供电流给负载。这就叫推。
当Vin电压为V-时,下面的三极管有电流流出,Q4导通,Q4有从上到下的电流流过。经过下面的P型三极管提供电流给负载,这就叫挽。
可以输出高、低电平;推挽结构一般是指两个三极管或MOSFET(NMOS、PMOS)分别受到互补信号的控制,总是在一个导通时另一个截止。
推拉式输出既可以提高电路的负载能力,又提高开关速度。
推挽式复用功能:
可以理解为GPIO口被用作第二功能时的配置情况(并非作为通用IO口使用)。
2. 编码器
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信号为一圈一个。
可以根据两个信号的先后来判断方向;根据每个信号脉冲数量的多少及整个编码轮的周长就可以计算出当前行走的距离;如果再加上定时器还可以计算出速度。
编码器有转速上限,超过这个上限是不能正常工作的,这个是硬件的限制。原则上线数越多转速就越低。
编码器接口模式下,定时器寄存器相关:
定时器初始化好以后,任何时候计数器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,因此编码器模式下定时器通道的选择上一定要注意。
下面来看定时器编码接口函数:
//使用编码器模式3:上升下降都计数
TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
相对信号电平:不考虑反向的情况下,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
中断向量表是一个包含中断服务程序(Interrupt Service Routine, ISR)地址的特定内存区域。向量表实际上是一个32位(WORD)整数数组,其中每个数组元素(通常称为“向量”)都指向一个特定的异常或中断处理程序的入口地址。当CPU接收到外部或内部产生的中断请求时,中断向量表会被用来定位并跳转到相应的中断服务程序以进行处理。
以下是中断向量表的主要作用和中断在运行时的行为:
-
中断向量表的作用:
- 连接中断号与中断服务程序:中断向量表是一个连接中断号与该中断号相应的中断服务程序入口地址之间的连接表。CPU通过查询中断向量表,可以找到与特定中断号对应的中断服务程序的入口地址。
- 快速响应中断:由于中断向量表通常位于内存的固定位置,CPU可以快速地从中断向量表中获取中断服务程序的地址,从而实现对中断的快速响应。
-
中断在运行时的行为:
- 当一个中断事件发生时(如外部设备触发、定时器溢出等),硬件设备或内部逻辑会发出中断请求信号给中断控制器。
- 中断控制器接收到中断请求后,会根据中断的优先级和屏蔽状态来决定是否响应该中断。
- 如果中断被响应,中断控制器会向CPU发送一个中断信号,并将中断号传递给CPU。
- CPU在接收到中断信号后,会保存当前的程序计数器(Program Counter, PC)和状态寄存器(Status Register)的值,以便在中断处理完成后能够恢复原来的程序执行。
- CPU然后会跳转到中断向量表中与中断号对应的中断服务程序的入口地址,开始执行中断服务程序。
- 中断服务程序会处理中断事件,可能包括读取设备状态、更新数据、发送响应等操作。
- 一旦中断服务程序执行完毕,CPU会恢复之前保存的PC和状态寄存器的值,并继续执行原来的程序。
以下是一些 Cortex-M3 核中常见的异常和中断向量表的例子:
- 复位(Reset)向量:
- 复位向量是向量表中的第一个元素(通常位于地址0x00000000),它包含了复位处理程序的入口地址。当处理器复位时,它会从复位向量指定的地址开始执行。
- NMI(Non-Maskable Interrupt)向量:
- NMI 是一种特殊的中断,它不能被其他的中断屏蔽。NMI 向量指向 NMI 处理程序的入口地址。NMI 通常用于处理一些紧急的、需要立即响应的事件。
- 硬故障(HardFault)向量:
- 当 Cortex-M3 检测到某些硬件故障(如总线故障、内存管理故障等)时,它会跳转到硬故障处理程序。硬故障向量指向这个处理程序的入口地址。
- SVC(Supervisor Call)向量:
- SVC 异常通常用于从用户模式调用特权模式的代码。SVC 向量指向 SVC 处理程序的入口地址。
- PendSV(Pending SVCall)向量:
- PendSV 是一种特殊的 SVC 调用,它可以在中断服务例程(ISR)之间或系统任务之间进行上下文切换时使用。PendSV 向量指向其处理程序的入口地址。
- 外部中断(IRQ)向量:
- Cortex-M3 支持大量的外部中断(IRQ),具体数量取决于具体的 Cortex-M3 实现和微控制器。每个 IRQ 都有一个对应的向量在向量表中,该向量指向该 IRQ 的处理程序的入口地址。
示例代码:
/**************************************************************************
函数功能:外部中断初始化
入口参数:无
返回 值:无
作 用:是用来配置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
工程中, P必然存在,在P的基础上又有如PD控制、PI控制、PID控制。
比例项:提高响应速度,减小静差。
积分项:消除稳态误差。即只要有误差,我就进行积分运算,直到偏差变为0。
微分项:减小震荡以及超调。
5.1 P 控制
其中,
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:在过去时间,大多数是超标的。
则积分控制器公式为:
即,历史上的整体偏差情况如果都是不达标的,那么我积分控制器就会有一个正的输出量使得你输出继续增强;若你历史上整体偏差情况是超标的,那么我就输出一个负的输出量使你降低输出。
这些正正负负的历史偏差全部加起来,会慢慢正负抵消,直至抵消至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越小越陡。
则微分控制器公式为:
微分控制器同样可通过加维持输出out0,以消除零输出系统失控的现象。
5.4 PID 控制数学模型
由上述得到简单的PID数学模型为:
5.4.1 Sk、Dk的确定
(1)Sk的确定:
其中,
Ti---积分时间常数,数学上指积分项运行的时间,物理上指考察历史数据的范围大小;
T---采样周期/计算周期,即多长时间算一次PID,更新PID的间隔时间。
Ti越大,则积分项输出就越小。
(2)Dk的确定:
其中,
Td---微分时间常数,微分项运行的时间;
T---采样周期/计算周期,即多长时间算一次PID,更新PID的间隔时间。
① Td越大,则微分项输出就越大。
② 偏差不变的情况下,T越大,偏差变化的斜率越小,导致系统对变化趋势不敏感,因此计算周期T不能过大。
5.4.2 位置式PID数学模型
此时经过PID计算得出的 PIDout 就是本周期的控制输出,也是加载到执行机构上的脉宽值。
比如,满PWM为1000的情况下,本次输出PIDout=200,则本周期的控制占空比=200/1000。
5.4.3 增量式PID
它计算出来的是控制量的改变值,即
又因为,
则得出,
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.c10ms<pid.T)
{
return;
}
pid.Ek = pid.Sv - pid.Pv; //得到当前偏差值
pid.Pout = pid.Kp * pid.Ek; //比例输出
pid.SEk+=pid.Ek; //得到历史偏差总和
DEK = pid.Ek - pid.Ek_1; //最近两次偏差之差
ti = pid.T/pid.Ti;
ki = ti * pid.Kp;
pid.Iout = ki * pid.SEk; //积分输出
td = pid.Td/pid.T;
kd = pid.Kp * td;
pid.Dout = kd * DEK; //微分输出
out = pid.Pout + pid.Iout + pid.Dout + pid.OUT0;
//防止out过大、小于0
if(out>pid.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调参
Kp:Kp太小,系统存在稳态误差(静差);而Kp过大,则会使系统有超调,并且出现震荡(系统惯性)。
比例控制是一种立即控制,只要有偏差,就立即输出控制量。
Ki:可以消除稳态误差。Ki过大容易引起震荡,造成超调。
积分控制是一种修复控制,只要有偏差,就会逐渐去往消除偏差的方向去控制。
Kd:微分项作为超前控制的主要输出,可以抑制震荡、限制超调、减少调节时间。Kd增大可以加快系统响应,减小超调量,太大的话系统响应又会特别慢。
微分控制是一种提前控制,以偏差的变化率为基准进行控制。
实际的反馈信号往往有噪声,微分控制有失稳的风险,所以用微分时一定要慎重。
一般实际工程控制中,微分控制使用确实较少。假如我们不能加微分调节,还有没有其他办法能让响应曲线更好呢?答案肯定是有的,先介绍其中一种。
把目标转速1000rpm进行一个低通滤波后,再给到PID控制器,选取Kp=0.005,Ki=0.015(参数调试过程同上),仿真结果如下图,响应虽不及PID控制快速,但是与PI控制器相比,超调量大大减小,稳定性有所提高。
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
串级控制系统
7. 位带操作(不常用)
在CM3中, 有两个区中实现了位带。其中一个是SRAM区的最低1MB范围,第二个则是片内外设区的最低1MB范围。这两个区中的地址除了可以像普通的RAM一样使用外,还都有自己的“位带别名区”,位带别名区把每个比特膨胀成一个32位的字。当你通过位带别名区访问这些字时,就可以达到访问原始比特的目的。--《CM3权威指南》
实例代码:
//位带操作,实现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. Keil 中的Code,RO-data,RW-data,ZI-data
Program Size: Code=37220 RO-data=1836 RW-data=1356 ZI-data=8172
Code :程序代码部分,即指令
RO-data:程序定义的常量
RW-data:已初始化的变量(全局和局部变量)
ZI-data:未初始化的变量(全局和局部变量)
(1)Flash占用:Code+RO-data+RW-data
(2)RAM占用:RW-data+ZI-data
单片机的启动过程涉及到多个环节,其中Code、RO-data、RW-data和ZI-data各自在启动过程中扮演着不同的角色。以下是这些元素在单片机启动过程中的大致工作过程:
- 复位阶段:当单片机的电源开启后,单片机首先会进入复位状态。在复位状态下,单片机会将所有寄存器和引脚初始化,并且设置默认的时钟模式。
- 系统时钟初始化阶段:单片机的时钟是控制单片机整个运行的重要部分。在系统时钟初始化阶段,单片机会将外部时钟源的信号导入到单片机内部,并且通过PLL(锁相环)对时钟进行放大和分频。
- 启动代码执行阶段:启动代码通常烧写在Flash中,它是系统一上电就执行的一段程序,运行在任何用户C代码之前。启动代码会建立中断向量表,并初始化一些必要的硬件和软件环境。
接下来,具体到Code、RO-data、RW-data和ZI-data的工作过程:
- Code:Code是单片机程序的主体部分,包含了程序的主要逻辑和指令。在启动过程中,单片机会从Flash中读取Code,并逐条执行其中的指令。
- RO-data(只读数据):RO-data包含了程序中定义的常量、字符串字面量等只读数据。这些数据在程序编译时就已经确定,并且在程序运行过程中不会被修改。在启动过程中,RO-data会被加载到相应的内存区域中,供程序在运行时读取。
- RW-data(读写数据):RW-data包含了程序中定义的需要在运行时进行读写的变量。在启动过程中,RW-data会被加载到RAM中的指定区域,以便程序在运行时对其进行读写操作。
- ZI-data(零初始化数据):ZI-data是程序中未明确初始化的变量,它们在程序启动时会被自动初始化为零。在启动过程中,单片机会为ZI-data在RAM中分配相应的空间,并将其初始化为零,不需要从Flash加载。
具体来说,单片机在启动时会按照以下步骤处理这些数据:
- 从Flash中读取启动代码并执行。
- 启动代码会初始化一些必要的硬件和软件环境,如系统时钟、中断向量表等。
- 启动代码会将RO-data从Flash中加载到相应的内存区域中。
- 启动代码会为RW-data和ZI-data在RAM中分配空间,并将RW-data从Flash中加载到RAM中。对于ZI-data,启动代码会将其初始化为零。
- 初始化完成后,启动代码会跳转到用户程序的入口点,将控制权交给用户程序。
9. GPIO模拟USART
使用两个GPIO口,一个用作串口发送TX,一个用作串口接收RX,采用的是定时器模拟时序。
对于发送,计算好不同波特率对应的延时时间进行数据发送。
对于接收,稍微复杂。通过外部中断检测接收管脚的下降沿,检测到起始信号后开启定时器,定时器按照波特率 设定好时间,每隔一段时间进入定时器中断接收数据,完成一个字节后关闭定时器.
UART的通信方式是由1个起始位,8个数据位,包含一个奇偶校验位,和结束位构成 。在本次的设计中默认为波特率为9600,停止位为1位,8位数据位,无奇偶校验位。
串口通讯是低位先行。
起始位:通信线路上空闲时为1,当检测到0即下降沿时,认为数据传输开始。
数据位:
奇偶校验位:校验传输数据中1的个数。
停止位:数据传输结束,传输线恢复。
波特率:单位时间内传送码元符号的个数,波特率9600,也就是1s内传送9600个bit,一个bit所需要的时间为 1000000us / 9600 = 104.166 us,也就是104us。
程序实现
(1)头文件参数定义
#ifndef _S_UART_H_
#define _S_UART_H_
#include "sys.h"
#include "delay.h"
//定义通信波特率
#define BaudRate_9600 104 //1000000us/9600=104.1666,发送1个位所需要的时间
//GPIO定义
#define S_Uart_Tx PCout(3) //模拟串口TX端
#define S_Uart_Rx PCin(4) //模拟串口RX端
typedef enum{
State_Start = 0, //起始状态
State_transfer, //传输状态
State_Stop, //停止状态
}UartState;
#define SUartLength 200 //模拟串口缓冲区长度
extern u8 SUartCnt; //模拟串口缓冲区位置
extern u8 SUartBuff[SUartLength]; //模拟串口缓冲区
void S_Uart_GPIO_Init(void);
void S_Uart_Send_Buff(u8 *buff,u8 length);
u8 S_Uart_Rx_Handler(u8 *buf,u8 *length);
#endif
(2)GPIO初始化
首先通过GPIO_EXTILineConfig
函数将GPIOB
的Pin_14
连接到外部中断线EXTI_Line14
。然后,配置EXTI_InitStruct
结构体来设置外部中断线的参数,如中断模式(中断)和触发方式(下降沿触发)。
然后配置了NVIC(嵌套向量中断控制器),以便当EXTI_Line14
(属于EXTI15_10_IRQn
中断组)产生中断时,CPU能够响应。
//输出管脚配置
void GPIO_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
EXTI_InitTypeDef EXTI_InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC, ENABLE); //使能PB,PC端口时钟
//SoftWare Serial TXD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOC, &GPIO_InitStructure);
GPIO_SetBits(GPIOC,GPIO_Pin_13);
//SoftWare Serial RXD
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);
EXTI_InitStruct.EXTI_Line = EXTI_Line14;
EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling; //下降沿触发中断
EXTI_InitStruct.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStruct);
NVIC_InitStructure.NVIC_IRQChannel= EXTI15_10_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
(3)模拟串口发送
//发送1个字节
void IO_TXD(u8 ch)
{
u8 i = 0;
GPIO_ResetBits(GPIOC,GPIO_Pin_9);
delay_us(BuadRate_9600);
for(i = 0; i < 8; i++)
{
if(ch&0x01)
GPIO_SetBits(GPIOC,GPIO_Pin_9);
else
GPIO_ResetBits(GPIOC,GPIO_Pin_9);
delay_us(BuadRate_9600);
ch = ch>>1;
}
GPIO_SetBits(GPIOC,GPIO_Pin_9);
delay_us(BuadRate_9600);
}
//发送字符串
void USART_Send(u8 *buf, u8 len)
{
u8 t;
for(t = 0; t < len; t++)
{
IO_TXD(buf[t]);
}
}
(4)模拟串口接收(定时器方式)
这段代码的主要功能是初始化定时器TIM4,并配置其更新中断以及NVIC的中断优先级,以便在定时器达到指定的计数周期时能够产生中断。
void TIM4_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //时钟使能
//定时器TIM4初始化
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据指定的参数初始化TIMx的时间基数单位
TIM_ClearITPendingBit(TIM4, TIM_FLAG_Update);
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE ); //使能指定的TIM3中断,允许更新中断
//中断优先级NVIC设置
NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn; //TIM4中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级1级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //从优先级1级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化NVIC寄存器
}
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetFlagStatus(EXTI_Line14) != RESET)
{
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10) == 0)
{
if(recvStat == COM_STOP_BIT)
{
recvStat = COM_START_BIT;
TIM_Cmd(TIM4, ENABLE);
}
}
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
void TIM4_IRQHandler(void)
{
if(TIM_GetFlagStatus(TIM4, TIM_FLAG_Update) != RESET)
{
TIM_ClearITPendingBit(TIM4, TIM_FLAG_Update);
recvStat++;
if(recvStat == COM_STOP_BIT)
{
TIM_Cmd(TIM4, DISABLE);
USART_buf[len++] = recvData;
return;
}
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10)) //接收为高电平
{
recvData |= (1 << (recvStat - 1));
}
else
{
recvData &= ~(1 << (recvStat - 1));
}
}
}
以下是对这两个函数的详细解释:
-
EXTI15_10_IRQHandler:
- 当外部中断线14(EXTI_Line14,通常与某个GPIO引脚相连)触发中断时,此函数会被调用。
- 首先,它检查EXTI_Line14的状态是否为触发状态(
EXTI_GetFlagStatus(EXTI_Line14) != RESET
)。 - 然后,它读取GPIOA的Pin_10的输入状态(
GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_10)
)。 - 如果Pin_10的输入为低电平(
== 0
),并且recvStat
的状态是COM_STOP_BIT
(可能表示前一个数据包的停止位),那么它会:- 将
recvStat
设置为COM_START_BIT
(表示开始接收新的数据包)。 - 启动TIM4定时器(
TIM_Cmd(TIM4, ENABLE)
)。
- 将
- 最后,它清除EXTI_Line14的中断标志位,以便该中断可以再次被触发。
-
TIM4_IRQHandler:
- 当TIM4定时器产生更新中断(例如,当达到预设的时间间隔时)时,此函数会被调用。
- 它首先检查TIM4的更新标志位(
TIM_GetFlagStatus(TIM4, TIM_FLAG_Update) != RESET
)。 - 然后,它清除该更新标志位,以便TIM4可以继续计数。
- 接着,它递增
recvStat
的值,这可能表示正在接收的数据包的当前位位置。 - 如果
recvStat
等于COM_STOP_BIT
,则:- 停止TIM4定时器。
- 将接收到的数据(
recvData
)存储在USART_buf
数组中,并递增len
的值。 - 函数返回,不再继续处理后续的逻辑。
- 如果GPIOA的Pin_10为高电平,它将
recvData
的相应位设置为1(表示接收到的该位是高电平)。 - 如果GPIOA的Pin_10为低电平,它将
recvData
的相应位设置为0(表示接收到的该位是低电平)。