要完成这样一个任务,对电动车的电池和电机状态做测量和显示。
电池(12V*4,60Ah)方面,要对电量做比较精确地计量,能显示电池的电量百分比,电池消耗的功率。
电机方面要能测出转速,时速,记录里程累积。
电量计量方案:因为电流很大,50A的典型值,用电阻采样需要很小的精密电阻,和复杂的调理电路,或者专用芯片——比如TI的bq26200(这是个可能可行的方案),最终选用了霍尔电流传感器+AD采样的方法。
测量思路:在足够短的时间△t 内认为电流恒定,测量电流大小I,计算 t 内的电量变化
△Q=I△t(I有正负)
Q=Q0+△Q(Q为实时电量,Q0为初始电量)
功率测量方案:测电流的同时测电压,跟据平均功率是瞬时功率的平均值,可以算每秒的平均功率,近似该秒的瞬时功率。
转速测量方案:电机装有霍尔传感器,每转一圈会发出若干个脉冲,用MSP430的捕获功能记录脉冲间隔时间就可以算出转速(电机和车轮速比1:1),在通过轮子的周长就可以算出时速和里程。
下面是用MSP430 f5438 实现的程序代码:
/*
电动车 电量计量、功率计量、转速测量 子程序
老沈 2011.1.1
说明:主程序为用法示例
函数说明:
initCLK() 将时钟配置在MCLK=SMCLK=12.288MHz,ACLK=XT1=32768Hz
initADC() 初始化AD,每10ms进行一次序列采样,采集 CH1(电池电压)和 CH2(电流信号的霍尔电压)
initTB() 配置TB:CCR0用作定时——周期匹配,CCR1用作比较输出——触发AD,
CCR6用作捕获,CCR2输出仅作捕获测试信号
display() 用于显示当前电压和电量,1s更新一次
storeinfo() 用于将 里程和电量 累计值存入flash,放置CPU掉电失去信息。(10min更新一次)
restoreinfo() 用于从flash里恢复 里程和电量 (需要另外写程序把最初的Q和mileage存入flash)
变量说明:
全局变量:
Isample 电流的霍尔电压采样值 (抬高后)
Vsample 电池电压采样值
VIbattery 电流的霍尔电压值(抬高前)mV
Ibattery 电流值 A (应配置霍尔方向使电流输出为正)
Vbattery 电池电压值 V
Percent 电量百分比 %
Q 当前电量 C
deltaQ 1s内减少的电量 mC
Power 电池输出平均功率 Power= (ΣVbattery*Ibattery)/ 100 = P/100
interval 两次捕获脉冲的间隔 ms
speed 电机转速 r/min
velocity 时速 km/h
mileage 里程累积
参数常量:
Q0 216000 电池充满时的电量
KVI 20.0 霍尔比例系数 霍尔电压/电流
KVV 0.047619 电池电压衰减比例 由调理电路决定
ts 10 采样间隔 10ms
circle 1000.0 轮子周长 单位mm
AVCC 3300 霍尔输出调理电路的供电电压
PULSEof1000r 12000 每转如果输出12个脉冲,那么1000转有12000脉冲
PULSEof1r 12 每转的脉冲数
*/
#include <msp430x54x.h>
#define Q0 216000 //60Ah=216000C
#define KVI 20.0 //4000mV/200A
#define KVV 0.047619 //10/210=0.047619
#define ts 10
#define circle 1000.0 //轮子周长 单位mm
#define AVCC 3300 // 霍尔输出调理电路的供电电压
#define PULSEof1000r 12000 // 每转如果输出12个脉冲,那么1000转有12000脉冲
#define PULSEof1r 12
void initIO();
void initCLK();
void initTB();
void initADC();
unsigned int Isample,Vsample,VIbattery,Percent,k=0;
float Vbattery,Ibattery;
float Q=Q0,deltaQ=0,P=0,Power=0;
unsigned int timecap=0,pretimecap=0,period=0,number_of_pulse=0;
float interval=0,speed=0,velocity=0,mileage=0;
//事实上,Q和mileage 应每隔一段时间存入flash, 程序复位时从flash读入
void main()
{
initIO();
initCLK();
//restoreinfo();
initADC();
initTB();
_EINT();
while(1)
{
LPM0;
//display();//显示变量float speed,float Power,int Percent,float Ibattery, float mileage, float velocity
//if(min%10==0) storeinfo(); //存储mileage 和 Q到 flash
}
}
void initTB()
{
// CCR0用作定时——周期匹配,CCR1用作比较输出——触发AD,CCR6用作捕获,CCR2输出仅作捕获测试信号
P4SEL |= BIT6; //P4.6/TB0.6 作为捕获信号的输入引脚 49号
P4DIR &= ~BIT6;
P4SEL |= BIT1 + BIT2; //TB1输出观察点 44号, TB2 45 号
TBCCR0 =15360; //10ms
TBCCR1 =5; //几微秒的时间,只是一个脉冲而已
TBCCR2 =5000; //产生100Hz的方波输出,用于调试捕获功能
TBCTL = ID_3 + TBSSEL_2 + MC_1 + TBCLR; // SMCLK/8=1536k, MC1:up mode, clear TBR
TBCCTL0 = CCIE; // TBCCR0 interrupt enabled
TBCCTL1 |= OUTMOD_7; //reset-set 模式
TBCCTL2 |= OUTMOD_7; //reset-set 模式
TBCCTL6 = CM_1 + CCIS_0 + SCS + CAP + CCIE; //上升沿捕获,输入信号为CCI6A(TB的CCIxA和CCIB接在了一个引脚,TA则分开了)
}
void initADC()
{
int i;
ADC12CTL0 &= ~ADC12ENC;
P6SEL = BIT1|BIT2; //使能AD输入 A1=P6.1(98号),A2=P6.2(99号)
ADC12CTL0 = ADC12ON + ADC12MSC + ADC12SHT0_4 + ADC12REFON + ADC12REF2_5V; // Turn on ADC12, extend sampling time(64*ADCclk=5.33us)
for(i=0;i<100;i++) ; //稳定参考源
ADC12CTL1 = ADC12CSTARTADD_1 + ADC12SHS_3 + ADC12CONSEQ_1 + ADC12SSEL_3 + ADC12SHP; // 触发源用TB1,单序列采样
ADC12MCTL1 = ADC12INCH_1 + ADC12SREF_1; //channel = A1 测电压
ADC12MCTL2 = ADC12INCH_2 + ADC12SREF_1 + ADC12EOS; // channel = A2 测电流
ADC12IE = BIT2; // Enable ADC12IFG.2
ADC12CTL0 |= ADC12ENC; // Enable conversions
}
void initCLK(void)
{
P7SEL |= 0x03; // Select XT1
UCSCTL6 &= ~(XT1OFF); // XT1 On
UCSCTL6 |= XCAP_3; // Internal load cap
UCSCTL3 = 0; // FLL Reference Clock = XT1
// XT1起振
UCSCTL1 = DCORSEL_6; // 选择DCO的范围
UCSCTL2 = 374; // 设置DCO频率为12M (12,288k)
UCSCTL4 = SELM_4 + SELA_0 + SELS_4; // 设置 MCLK = DCOC,SMCLK =DCO,ACLK=XT1(32768)
// Loop until XT1 & DCO stabilizes
while ( (SFRIFG1 &OFIFG))
{
UCSCTL7 &= ~(XT1LFOFFG + DCOFFG); // Clear XT1,DCO fault flags
SFRIFG1 &= ~OFIFG; // Clear fault flags
}
UCSCTL6 &= ~(XT1DRIVE_3); // Xtal is now stable, reduce drive strength
}
void initIO()
{
WDTCTL = WDTPW+WDTHOLD; // Stop watchdog timer
PADIR = 0xFFFF; // Tie unused ports
PAOUT = 0;
PASEL = 0;
PBDIR = 0xFFFF;
PBOUT = 0;
PBSEL = 0;
PCDIR = 0xFFFF;
PCOUT = 0;
PCSEL = 0;
PDDIR = 0xFFFF;
PDOUT = 0;
PDSEL = 0;
PEDIR = 0xFFFF;
PEOUT = 0;
PESEL = 0;
P11DIR = 0xFF;
P11OUT = 0;
P11SEL = 0;
PJDIR = 0xFF;
PJOUT = 0;
}
#pragma vector=ADC12_VECTOR
__interrupt void ADC12(void)
{
Isample=ADC12MEM2;
Vsample=ADC12MEM1;
ADC12CTL0 &= ~ADC12ENC; // 开启下一个序列必须重给一个ENC上升沿
ADC12CTL0 |= ADC12ENC; // Enable conversions
//10ms内的累加
VIbattery=(long)Isample*2500/4095;
VIbattery=AVCC-VIbattery*2;
Ibattery= VIbattery/KVI;
// Ibattery=60;
deltaQ += Ibattery * ts;
Vbattery =Vsample*2.5/4095/KVV;
// Vbattery=48;
P += Vbattery * Ibattery;
k++;
//1s后的累加
if(k==100)
{
k=0;
Q=Q-deltaQ/1000; //当前电量
deltaQ=0;
Percent = (int)(100*Q/Q0); //剩余电量比例
Power=P/100; //当前功率
P=0;
LPM0_EXIT;
}
}
//CCR0用作定时——周期计数
#pragma vector=TIMERB0_VECTOR
__interrupt void TB_Peroid (void)
{
period++;
}
//CCR6用作捕获——计录每转时间,求速度 (引脚TB0.6/P4.6 (49号) 作为输入)
#pragma vector=TIMERB1_VECTOR
__interrupt void TB_CAP(void)
{
switch( TBIV )
{
case 2: break; // CCR1 中断服务程序
case 4: break; // CCR2 中断服务程序
case 6: break; //CCR3
case 8: break; //CCR4
case 10: break; //CCR5
case 12: //CCR6
timecap = TBCCR6;
//TBCCTL6 &=~COV;
interval = period*10.0 + (timecap-pretimecap)*10.0/15360; //以ms为单位记录两次捕获的间隔
speed = 60000 /interval /PULSEof1r ; //转速单位 r/min (关系式speed*interval/60000=1/PULSEof1r)
velocity = speed /16667 *circle ; //速度单位 km/h (关系式:转速*周长*60= 速度 mm/h)
number_of_pulse++;
if(number_of_pulse == PULSEof1000r)
{
mileage += circle/1000; //单位km (1000*circle mm = circle m)
number_of_pulse =0;
}
pretimecap = timecap;
period=0;
break;
case 14:break; // TAIFG 中断服务程序
}
}
本程序关于MSP430的两个关键点:
我自以为用430用的可以了,没想到调程序时还是被卡住了。
这是因为我尝试了两个新功能,第一、用TB1OUT触发AD采样,而没用SC命令,第二、我第一次用MSP430的捕获功能。
刚写完程序我还夸430设计的好来着,因为AD的采样时间和转换时间完全由自己掌握,可以自己选择尽量快的采样时间,而转换只需要13个ADC12CLK,而AD的时钟可以给的很快。用定时器触发AD采样不用经过CPU,直接在后台开始,可以使采样相当准时。另外430的捕获功能不仅可以捕获,还可以直接读出当前的电平。
结果AD采一次就不能再采了,捕获根本就不进中断。
我检查了引脚的PxSEL已经确定选了第二功能,也确定开了模块中断和总中断。
这就奇怪了。
于是仔细研究user guide
结果从这幅图上发现了问题:
一个序列结束后竟然回到的wait for enable而不是wait for trigger
往常用的SC触发AD采样,是走左边一条路直接跳过wait for trigger(我本觉得SC也算trigger的一种的)开始Sample,如今用TB1out触发,就必须再来一次ENC的下降沿,才能进入wait for trigger,然后我的定时触发才有效。怪不得我的AD只采一次呢!可是TI的例程上全是SC触发,于是从来都只使能一次ENC就不用管了,这思维定势也不能怪我吧。。。只希望对同学们少走弯路有帮助。
那么捕获的问题呢,在IO那一章,430user guide要求把PxSEL设1选第二功能的时候说,有的模块第二功能要配合设置正确的IO方向,而我初始化系统时都把引脚设成输出了(不用的引脚悬空设成输出——为了低功耗嘛)AD输入引脚就不用管PxDIR,只要PxSEL=1,而捕获就偏偏PxDIR和PxSEL都要设置,我想你user guide怎么不说清楚呢,怎么这么不周到呢!!!而且引脚默认PxDIR=0,肯定有很多人不配置IO方向也照样能用捕获,所以网上查的程序里十有**没有专门写PxDIR&=~BITx这一句。我加上这句,程序就好了。。。不知道谁在误人子弟。
罗嗦了这么多,不好意思,在程序里就是这样两句
ADC12CTL0 &= ~ADC12ENC; // 开启下一个序列必须重给一个ENC上升沿
ADC12CTL0 |= ADC12ENC; // Enable conversions
和
P4DIR &= ~BIT6;
有了这两句,我的程序正常了。
最后希望对大家有用,尤其是以后要用430参加电赛的同学。 |