1.项目需求
- IO扩展板上的2个按键和旋转编码器的3个输入端口是通过R-2R电阻网络的方式连接在一起,生成一个模拟电压量。按下任何一个按键都会改变这个模拟电压量的值。
- IO扩展板上的LCD屏幕为128*128分辨率的1.44寸彩色屏幕,通过SPI总线进行访问
要求:本任务需要通过MSP430核心板的ADC监测IO板模拟输出管脚的变化,判断哪一个按键按下或编码器是否旋转,进而控制1.44寸LCD屏幕的菜单显示,要求实现主菜单和至少二级菜单。
2.完成的功能及达到的性能
2.1 ADC采集
通过A_Out(P6.0)引脚采集电压值,判断按键是否按下以及按键是哪个按下。如上图所示,不同的按键按下后A_Out的电压值会发生不同的变化,理论上旋转编码器旋转使AC闭合,电压变化1/32*3.3V,使BC闭合,电压变化1/16*3.3V,旋转编码器上的按键按下,电压变化1/8*3.3V;K1按下,电压变化1/4*3.3V,K2按下,电压变化1/2*3.3V。用MSP430F5529内部的12位ADC进行电压采集,计算按键按下前后电压变化的大小,进行按键检测,从而实现按键检测功能。
2.2 LCD屏幕显示
扩展板上用的是1.44寸128*128LCD,进行菜单显示,可以显示16位彩色的图片。我设计了一个简单的二级菜单,首先上电显示的是主菜单,通过按键操纵,进入下一级菜单。
3 实现思路
1.ADC对输入通道进行采样,采样由软件触发,采样后进入ADC中断服务函数,对采样值进行处理,计算出前后两次采样的差值的绝对值,通过电压的变化大小,判断按键到底是由哪个按键按下。
2.用LCD的显示函数设计一级菜单界面和二级菜单界面。
3.通过旋转编码器旋转,控制主菜单中的光标移动,按键K1按下,进入下一级菜单,按键K2按下,返回上一级菜单。
4 实现过程
4.1程序流程图
4.2 ADC对数据进行采样
ADC初始化函数(定义位置:adc.c,调用位置:main.c):
void ADC_Init(void)
{
//P6.0 ADC option select
GPIO_setAsPeripheralModuleFunctionInputPin(
GPIO_PORT_P6,
GPIO_PIN0
);
//Initialize the ADC12_A Module
/*
* Base address of ADC12_A Module
* Use internal ADC12_A bit as sample/hold signal to start conversion
* USE MODOSC 5MHZ Digital Oscillator as clock source
* Use default clock divider of 1
*/
ADC12_A_init(ADC12_A_BASE,
ADC12_A_SAMPLEHOLDSOURCE_SC,
ADC12_A_CLOCKSOURCE_ADC12OSC,
ADC12_A_CLOCKDIVIDER_1);
ADC12_A_enable(ADC12_A_BASE);
/*
* Base address of ADC12_A Module
* For memory buffers 0-7 sample/hold for 64 clock cycles
* For memory buffers 8-15 sample/hold for 4 clock cycles (default)
* Disable Multiple Sampling
*/
ADC12_A_setupSamplingTimer(ADC12_A_BASE,
ADC12_A_CYCLEHOLD_64_CYCLES,
ADC12_A_CYCLEHOLD_4_CYCLES,
ADC12_A_MULTIPLESAMPLESDISABLE); //软件触发
//Configure Memory Buffer
/*
* Base address of the ADC12_A Module
* Configure memory buffer 0
* Map input A5 to memory buffer 0
* Vref+ = AVcc
* Vr- = AVss
* Memory buffer 0 is not the end of a sequence
*/
ADC12_A_configureMemoryParam param = {0};
param.memoryBufferControlIndex = ADC12_A_MEMORY_0;
param.inputSourceSelect = ADC12_A_INPUT_A0;
param.positiveRefVoltageSourceSelect = ADC12_A_VREFPOS_AVCC;
param.negativeRefVoltageSourceSelect = ADC12_A_VREFNEG_AVSS;
param.endOfSequence = ADC12_A_NOTENDOFSEQUENCE; //单通道采集
ADC12_A_configureMemory(ADC12_A_BASE ,¶m);
//Enable memory buffer 0 interrupt
ADC12_A_clearInterrupt(ADC12_A_BASE,
ADC12IFG0);
ADC12_A_enableInterrupt(ADC12_A_BASE,
ADC12IE0);
}
开启ADC采集函数(定义位置:adc12_a.c,调用位置:main.c):
baseAddress : ADC12只有一个基地址
startingMemoryBufferIndex : 设置需要开始转换的信号。因为我们之前将ADC采集的信号存入MEMORY_0,所以这里选择ADC12_A_MEMORY_0,可选参数为0~15
conversionSequenceModeSelect : 这个是选择ADC开启的模式。有四种模式,如下。
说白了就是一个多通道和单通道,多通道可以多个通道一起进行ADC电压信号采集,比如P6.0和P6.1同时进行ADC电压采集,而单通道只有一个引脚进行ADC电压信号采集。
然后单次采样和多次采样的区别是,如果我们选择的单次采样,那么每次进行ADC采样都需要一次触发。但是如果是多次采样,那么我们只需要一次触发,那么它就会持续采样转换,如果此次电压转换之后的信号不马上读取,会被下一次转换之后的信号覆盖。因为我们这里选择的软件触发,我个人建议选择单次采样,这样我们的电压采样转换的信号就不会被覆盖,同时可以降低功耗。
void ADC12_A_startConversion (uint16_t baseAddress,
uint16_t startingMemoryBufferIndex,
uint8_t conversionSequenceModeSelect)
{
//Reset the ENC bit to set the starting memory address and conversion mode
//sequence
HWREG8(baseAddress + OFS_ADC12CTL0_L) &= ~(ADC12ENC);
//Reset the bits about to be set
HWREG16(baseAddress + OFS_ADC12CTL1) &= ~(ADC12CSTARTADD_15 + ADC12CONSEQ_3);
HWREG8(baseAddress + OFS_ADC12CTL1_H) |= (startingMemoryBufferIndex << 4);
HWREG8(baseAddress + OFS_ADC12CTL1_L) |= conversionSequenceModeSelect;
HWREG8(baseAddress + OFS_ADC12CTL0_L) |= ADC12ENC + ADC12SC;
}
获取当前ADC采样之后的值的函数(定义位置:adc12_a.c,调用位置:main.c):
uint16_t ADC12_A_getResults (uint16_t baseAddress, uint8_t memoryBufferIndex)
{
//(0x20 + (memoryBufferIndex * 2)) == offset of ADC12MEMx
return ( HWREG16(baseAddress + (0x20 + (memoryBufferIndex * 2))) );
}
ADC中断服务函数(定义位置:main.c,调用位置:main.c)
每采集到一次电压,进入中断服务函数中,计算ADC变化的绝对值,用于判断哪个按键按下
#pragma vector=ADC12_VECTOR
__interrupt
void ADC12_A_ISR (void)
{
switch (__even_in_range(ADC12IV,34))
{
case 0: break; //Vector 0: No interrupt
case 2: break; //Vector 2: ADC overflow
case 4: break; //Vector 4: ADC timing overflow
case 6: //Vector 6: ADC12IFG0
Measured_last=Measured;
Measured = ADC12_A_getResults(ADC12_A_BASE, ADC12_A_MEMORY_0);
change = fabs(Measured-Measured_last);//ADC变化的绝对值
botton(change);//获取按键位置
__bic_SR_register_on_exit(LPM0_bits);
case 8: break; //Vector 8: ADC12IFG1
case 10: break; //Vector 10: ADC12IFG2
case 12: break; //Vector 12: ADC12IFG3
case 14: break; //Vector 14: ADC12IFG4
case 16: break; //Vector 16: ADC12IFG5
case 18: break; //Vector 18: ADC12IFG6
case 20: break; //Vector 20: ADC12IFG7
case 22: break; //Vector 22: ADC12IFG8
case 24: break; //Vector 24: ADC12IFG9
case 26: break; //Vector 26: ADC12IFG10
case 28: break; //Vector 28: ADC12IFG11
case 30: break; //Vector 30: ADC12IFG12
case 32: break; //Vector 32: ADC12IFG13
case 34: break; //Vector 34: ADC12IFG14
default: break;
}
}
4.3 按键读取
用ADC读取到的电压变化的绝对值,判断哪个按键按下
获取键值函数:(定义位置:menu.c,调用位置:main.c):
extern uint16_t key; //按键键值
extern uint16_t cursor;//光标位置
extern uint16_t page; //用于标记不同界面,完成界面的切换
extern uint16_t flag_picture; //用与“picture”中的图片切换
void botton(uint16_t change)
{
if(change<2000&&change>1800)//k2 返回 1924
{
LCD_Fill(0,0,128,128,WHITE); //用于清屏操作,防止界面刷新后出现残留
key=2;
}
else if(change<1100&&change>900)//k1 确认 982
{
LCD_Fill(0,0,128,128,WHITE);
key=1;
}
else if(change<600&&change>420)//k3 500
{
LCD_Fill(0,0,128,128,WHITE);
key=3;
flag_picture++;
if(flag_picture==2)
{
flag_picture = 0;
}
}
else if(change<420&&change>70)//旋转 370
{
key=4;
cursor++;
if(cursor==4)
{
cursor=0;
}
}
}
4.4 按键操作
读取到按键键值后,就可以对二级菜单进行相应操作
获取界面函数:(定义位置:menu.c,调用位置:main.c):
void get_page(void)
{
if(cursor==0&&key==1)
{
page=21; //0
}
else if(cursor==1&&key==1)
{
page=22; //1
}
else if(cursor==2&&key==1)
{
page=23; //2
}
else if(cursor==3&&key==1)
{
page=24; //3
}
else if(key==2)
{
page=1; //主界面
}
else if(key==0)//初始状态key=0
{
page=1;
}
}
4.5 LCD菜单显示(定义位置:menu.c,调用位置:main.c):
extern const unsigned char gImage_1[]; //四张图片,本来想多存几张的,但是发现内存不够了
extern const unsigned char gImage_2[];
extern const unsigned char gImage_picture[];
extern const unsigned char gImage_app[];
//主菜单
void GUI_main(void)
{
LCD_ShowString(0,0," Ranbe ",BLACK,WHITE,32,0);
LCD_ShowString(2,32," Picture ",BLACK,WHITE,16,0); //0
LCD_ShowString(2,48," MSP430f5529 ",BLACK,WHITE,16,0);//1
LCD_ShowString(2,64," Study plan ",BLACK,WHITE,16,0); //2
LCD_ShowString(2,80," App ",BLACK,WHITE,16,0); //3
LCD_ShowString(2,96," ....... ",BLACK,WHITE,16,0); //4
switch(cursor)
{
case 0:
LCD_ShowString(1,32,"->",RED,WHITE,16,0);
break;
case 1:
LCD_ShowString(1,48,"->",RED,WHITE,16,0);
break;
case 2:
LCD_ShowString(1,64,"->",RED,WHITE,16,0);
break;
case 3:
LCD_ShowString(1,80,"->",RED,WHITE,16,0);
break;
}
}
//二级界面
void GUI_21(void)
{
switch(flag_picture)
{
case 0:
LCD_ShowPicture(0,0,128,128,gImage_1 );//显示图片
break;
case 1:
LCD_ShowPicture(0,0,128,128,gImage_2 );//显示图片
break;
}
}
void GUI_22(void)
{
LCD_ShowString(0,0," MSP430f5529 ",BLACK,WHITE,16,0);
LCD_ShowString(2,18,"TI",BLACK,WHITE,16,0); //0
LCD_ShowString(2,36,"25MHz",BLACK,WHITE,16,0); //1
LCD_ShowString(2,54,"Flash 128KB",BLACK,WHITE,16,0); //2
LCD_ShowString(2,72,"RAM 8K",BLACK,WHITE,16,0); //3
LCD_ShowString(2,90,".......",BLACK,WHITE,16,0); //4
}
void GUI_23(void)
{
LCD_ShowString(0,0,"STUDY PLAN",BLACK,WHITE,16,0);
LCD_ShowString(2,18,"Higher mathematics",BLACK,WHITE,16,0); //0
LCD_ShowString(2,36,"7--9AM",BLACK,WHITE,16,0); //1
LCD_ShowString(2,54,"MSP430f5529",BLACK,WHITE,16,0); //2
LCD_ShowString(2,72,"9.20--11.20",BLACK,WHITE,16,0); //3
LCD_ShowString(2,90,".......",BLACK,WHITE,16,0); //4
}
void GUI_24(void)
{
LCD_ShowPicture(0,0,120,128,gImage_app);//显示图片 40*40
}
void GUI_display(void)
{
switch(page)
{
case 1:
GUI_main();
break;
case 21:
GUI_21();
break;
case 22:
GUI_22();
break;
case 23:
GUI_23();
break;
case 24:
GUI_24();
break;
}
}
4.6 菜单图片
由于某种未知原因,上传了N遍上传不了,大家可以去看我在哔哩哔哩发的视频,里面有展示菜单界面,实在抱歉。
4.7 主函数代码
//定义全局变量
uint16_t Measured;
uint16_t Measured_last;
uint16_t change;
uint16_t page=1;//界面等级
uint16_t page_1=1;
uint16_t key=0;
uint16_t cursor=0;//光标位置
uint16_t flag_picture=0;
int main( void )
{
// Stop watchdog timer to prevent time out reset
WDT_A_hold(WDT_A_BASE);
clock_init(20);//20Mhz
LCD_Init(); //LCD初始化
ADC_Init(); //ADC初始化
//interrupts enabled
__bis_SR_register(GIE);
Close_heat(); //关闭电阻加热模块
LCD_Fill(0,0,128,128,WHITE); //全屏填充白色
while(1)
{
ADC12_A_startConversion(ADC12_A_BASE, ADC12_A_MEMORY_0, ADC12_A_SINGLECHANNEL);
__bis_SR_register(LPM0_bits + GIE);
get_page(); //获取要显示的界面
GUI_display(); //显示界面
}
}
5 遇到的主要难题
5.1 ADC采集
项目中我没有对采集到的电压进行滤波,采集到的电压值不准确,不稳定。
具体优化方案:
1、限幅滤波法(又称程序判断滤波法)
A、方法:
根据经验判断,确定两次采样允许的最大偏差值(设为A)
每次检测到新值时判断:
如果本次值与上次值之差<=A,则本次值有效
如果本次值与上次值之差>A,则本次值无效,放弃本次值,用上次值代替本次值
B、优点:
能有效克服因偶然因素引起的脉冲干扰
C、缺点
无法抑制那种周期性的干扰
平滑度差
2. 中位值滤波算法
A、方法:
连续采样N次(N取奇数)
把N次采样值按大小排列
取中间值为本次有效值
B、优点
能有效克服因偶然因素引起的波动干扰
对温度、液位的变化缓慢的被测参数有良好的滤波效果
C、缺点
对流量、速度等快速变化的参数不宜
3、算术平均滤波法
A、方法:
连续取N个采样值进行算术平均运算
N值较大时:信号平滑度较高,但灵敏度较低
N值较小时:信号平滑度较低,但灵敏度较高
N值的选取:一般流量,N=12;压力:N=4
B、优点:
适用于对一般具有随机干扰的信号进行滤波
这样信号的特点是有一个平均值,信号在某一数值范围附近上下波动
C、缺点:
对于测量速度较慢或要求数据计算速度较快的实时控制不适用
比较浪费RAM
由于这个项目的主要目的不是采集ADC电压,而是从电压值判断哪个按键按下,所以只要
采集到的电压有波动,对整个系统的影响,应该不大
5.2 按键检测
从视频中,你们也可以发现,我每次按下的时间都较长,短按的话,不一定有反应,
说明代码还需优化。
而且我的代码不能识别旋转编码器旋转的方向,我只能判断出旋转编码器旋转半圈。
具体的优化步骤 :
我认为可以以一定的频率对ADC进行采样,绘出一定时间内的ADC变化曲线,通过曲线的变化
来判断按键是否按下,这样就可以判断正转和反转。这部分的代码应该稍微有些复杂,主要是
我的C语言学的较差,所以就没有尝试使用这种方法。
5.3 LCD显示
二级菜单进行切换时,它的刷新率还是比较慢的,可能是每次界面切换的时候都要清屏导致的。但是如果不清屏的话,有些界面之间的切换,会出现残留现象。
旋转编码器旋转时,光标的移动也很慢,需要较长的时间才能响应。提高刷新率也是代码继续优化的一个重要方向。
6 未来的计划建议
该项目已经成功实现了二级菜单的功能。但是有些地方还可以提高,可以把这个二级菜单做的更加精美。
1.提高刷新率;
2.缩短旋转编码器旋转后的响应时间;
3.提高显示的画质(不过图片显示受制于硬件,软件部分提高的空间不大,升级硬件应该可以
大幅提高);
4.旋转编码器的顺时针和逆时针旋转无法区分,可以采集更多的ADC值,得出ADC变化曲线,来判断旋转方向;
5.C语言还需要深入学习,硬件方面也要加强。