开篇介绍
由于项目开发过程中用到了一款开源的伺服步进电机,步进电机的开源代码里面涉及了步进电机的编码器校正(或电机标定),并已经把编码器校正后的值存到了主控芯片stm32F030的后32Kflash里面。然而自身项目使用中需要知道步进电机的上机零点还有电机各个时刻所转的角度,因此,需要对电机的编码器校正值进行一个地址偏移处理,于是地址偏移的大小则是上机零点的编码器值。由于过程中涉及了大量的flash读写操作以及一些衍生的问题,由此产生了该篇博客,意在讨论一下嵌入式平台的Flash读写操作以及修改,还有局部变量、全局变量、代码块在存储器的分布和堆栈的使用
一、硬件介绍
所采用的芯片为HK32F030x8,该芯片与stm32F0为同级芯片,主频最高能达到72MHz,存储空间方面具有64KBytes的Flash和10KBytes的SRAM,其余片上资源如下图所见:
二、编码值角度校正
由于步进电机行进一步角度为1.8°,则步进电机旋转一圈(360°)能检测到200个编码器值。所采用的旋转编码器是MT6816,其采用自适应多速率磁编码技术对0°~360°进行全范围角度检测,具有14位分辨率。因此在电机起电工作过程中,铁芯就相当于一个磁铁,通过磁编码器能读出电机主轴的编码器值。
标定的思路:
(1)通过步进电机正反各旋转一圈,先正转一圈,在每一步后读取十次编码值进行取平均,然后继续旋转下一步,直到200步完成;随后反转亦如此,最后通过正反转的值取它们两者的中值,即得到了电机旋转一圈在每一步的编码器值。
(2)由于磁编码器有14位分辨率,将这14位分辨率进行所有编码值的量化操作,也就是在每一步编码值之间进行线性插值,并且找到了磁编码器的编码零点。最后从编码零点为起点,将14位分辨率的编码器值存到了主控芯片HK32F030的后32Kflash当中。
改进功能:由于项目开发原因,需要不定时的都能读取到电机的旋转角度,因此我们还缺少一个基准,这就是电机的起点零点。当清楚了电机的起点零点后,以此编码值为基准,则我们可以读取到电机旋转到哪,哪里才是旋转了一圈,因此,理解为可以把芯片后32K的flash的编码值进行偏移,偏移量是电机上电零点的编码值。
编码器标定源码如下:
dir=1; //开始正转
Output(0,80);//开始时固定在整步位置,如果校正时电机太大把80的设置电机加到120左右或者更大些
for(uint8_t m=0;m<4;m++)
{
LED_H;
LL_mDelay(250);
LED_L;
LL_mDelay(250);
}
//逆时针跑一圈记下各整步位的值
for(int16_t x=0;x<=199;x++)
{
encoderReading=0;
LL_mDelay(20);
lastencoderReading=ReadAngle();
//连续读10次
for(uint8_t reading=0;reading<10;reading++)
{
currentencoderReading=ReadAngle();
if(currentencoderReading-lastencoderReading<-8192)//正转
currentencoderReading+=16384;
else if(currentencoderReading-lastencoderReading>8192)//反转
currentencoderReading-=16384;
encoderReading+=currentencoderReading;
LL_mDelay(10);
lastencoderReading=currentencoderReading;
}
encoderReading=encoderReading/10;
if(encoderReading>16384)
encoderReading-=16384;
else if(encoderReading<0)
encoderReading+=16384;
fullStepReadings[x]=encoderReading;
OneStep();
LL_mDelay(50);
}
dir=0; //开始反转
OneStep();
LL_mDelay(1000);
//顺时针跑一圈记下各整步位的值和之前逆时针保存的值求平均
for(int16_t x=199;x>=0;x--)
{
encoderReading=0;
LL_mDelay(20);
lastencoderReading=ReadAngle();
for(uint8_t reading=0;reading<10;reading++)
{
currentencoderReading=ReadAngle();
if(currentencoderReading-lastencoderReading<-8192)
currentencoderReading+=16384;
else if(currentencoderReading-lastencoderReading>8192)
currentencoderReading-=16384;
encoderReading+=currentencoderReading;
LL_mDelay(10);
lastencoderReading=currentencoderReading;
}
encoderReading=encoderReading/10;
if(encoderReading>16384)
encoderReading-=16384;
else if(encoderReading<0)
encoderReading+=16384;
fullStepReadings[x]=(fullStepReadings[x]+encoderReading)/2;
OneStep();
LL_mDelay(50);
}
LL_TIM_OC_SetCompareCH1(TIM3,0);
LL_TIM_OC_SetCompareCH2(TIM3,0);
//找到iStart和jStart,使得(fullStepReadings[i]+j)%16384=0
for(uint8_t i=0;i<200;i++)//算出200整步间的插值数
{
ticks=fullStepReadings[(i+1)%200]-fullStepReadings[i%200];
if(ticks<-15000)
ticks+=16384;
else if(ticks>15000)
ticks-=16384;
//找到使得stepNo=0的iStart和jStart
for(int32_t j=0;j<ticks;j++)
{
stepNo=(fullStepReadings[i]+j)%16384;
if(stepNo==0)
{
iStart=i;
jStart=j;
}
}
}
FlashUnlock();
FlashErase32K();
//以步进电机整步为基准(小于0.08度)开始对编码器进行校正插值并将校正后的值存入FLASH
//从i开始到(i+1)%200,遍历fullStepReadings[200]数组
//FLASH里保存的是angle,表示index=ReadAngle()位置时,对应的lookupAngle=81.92*(i+j/ticks)%16384编码器角度值
//起始flash地址address=0x08008000;存储位置为:0x08008000+2*index,最大范围到0x08008000+2*16384
//需要访问flash的函数有:
//void EXTI2_3_IRQHandler(void)电机使能信号外部中断函数
//void TIM6_IRQHandler(void) TIM6位置环控制周期中断函数
//void SetModeCheck(void);调用两次
//
for(int32_t i=iStart;i<(iStart+200+1);i++)
{
ticks=fullStepReadings[(i+1)%200]-fullStepReadings[i%200];
if(ticks<-15000)
ticks+=16384;
if(i==iStart)
{
for(int32_t j=jStart;j<ticks;j++)
{
lookupAngle=(8192*i+8192*j/ticks)%1638400/100;
FlashWriteHalfWord(address,(uint16_t)lookupAngle);
address+=2;
}
}
else if(i==(iStart+200))
{
for(int32_t j=0;j<jStart;j++)
{
lookupAngle=((8192*i+8192*j/ticks)%1638400)/100;
FlashWriteHalfWord(address,(uint16_t)lookupAngle);
address+=2;
}
}
else
{
for(int32_t j=0;j<ticks;j++)
{
lookupAngle=((8192*i+8192*j/ticks)%1638400)/100;
FlashWriteHalfWord(address,(uint16_t)lookupAngle);
address+=2;
}
}
}
FlashLock();
改进部分代码如下:
//读取fullStepReadings[0]索引位置的flash数据,设置零点为启动位置
int16_t temp=*(volatile uint16_t*)(fullStepReadings[0]*2+0x08008000);//读出矫正后fullStepReadings[0]索引位置编码器的角度位置值
address=0x08008000;
//依次读出flash数据,并减去temp,再写入flash
for(int32_t j=0;j<16384;j++)
{
lookupAngle=*(volatile uint16_t*)(address);
if(lookupAngle<temp)
lookupAngle-=temp;
else
lookupAngle=16384+lookupAngle-temp;
//写入flash
FlashWriteHalfWord(address,(uint16_t)lookupAngle);
address+=2;
}
三、问题出现
若将上述两段代码拼到一起,在项目中执行,是会有些小问题的。首先flash的读写操作是规律限制的。==Flash在执行编程操作时,只能将每个比特位从’1’写为’0’,而不能反过来将’0’写为’1’,这就意味着Flash不能执行覆盖写操作。==擦除操作的作用,是将闪存中每个位恢复为’1’的状态,因此,每个块只有先擦除,其中的页才能写入数据。
于是,我有诞生出了另一个想法,建立一个缓冲区,把flash里面的编码值先取出来,进行偏移值计算,最后把要重写的flash擦除,进行重写,其代码如下所示:
int16_t temp=*(volatile uint16_t*)(fullStepReadings[0]*2+0x08008000);//读出矫正后fullStepReadings[0]索引位置编码器的角度位置值
address = 0x08008000;
int16_t calibratedAngle[16384];
//依次读出flash数据,并减去temp,再写入flash
for(int32_t j=0;j<16384;j++)
{
calibratedAngle[j]=*(volatile uint16_t*)(address);
calibratedAngle[j]=(uint16_t)((calibratedAngle[j]-temp)%16384);
address+=2;
}
FlashErase32K();
address = 0x08008000;
for (int32_t j=0;j<16384;j++)
{
FlashWriteHalfWord(address,(uint16_t)calibratedAngle[j]);
address+=2;
}
FlashLock();
接着又诞生了另外一个问题,上面代码定义了一个16384大小的16位数据缓冲区来存储新的编码器值,总共大小为32KBytes的数据,由于局部变量都是通过堆栈的形式来进行空间分配的,而最后还是受限制于片上的RAM资源大小,而本平台的RAM资源最大只有10KBytes。
如果按照上述代码进行,系统在一段时间运行后,可能会有堆栈溢出的问题,最终会导致系统跑飞或崩溃,这样的代价对于项目来说太大了。因此我对此思路进行了又一个改进:
直接将步进电机的起电零点存到flash里面,当在每次需要读取角度时,直接进行编码值偏移运算,最后转换成角度,于是问题从原来的改写32K的flash,变成了存储一个编码器值即可
uint16_t temp=*(volatile uint16_t*)(fullStepReadings[0]*2+0x08008000);//读出步进电机上电零点编码值
FlashErasePage(0x08007C00);
FlashWriteHalfWord(0x08007C00,(uint16_t)temp);
FlashLock();
当每次需要读取电机角度时,通过如下代码实现:
uint16_t angle;
uint16_t initial;
initial = *(volatile uint16_t*)0x08007C00;
angle=*(volatile uint16_t*)(ReadAngle()*2+0x08008000);
angle=((angle-initial)%16384)*360/16384;//unit:degree
四、问题最终解决
经过多次上电实验发现,在Flash地址0x08007C00上存储的上电零点始终为0,也就是说磁编码器每次上电标定点都为编码器自身零点。
五、总结心得
在做应用开发的时候,无论是嵌入式开发还是软件开发,要注重各种变量之间的运算,同类型变量之间运算基本很少出现问题,当不同类型变量运算时,要注意强制转换的问题,还有堆栈溢出,系统资源限制等等综合问题,要想代码问题少,每个小细节都不能轻易放过。