基于MSP430训练平台实现游戏手柄控制LCD上的信息
本项目使用TI公司的MSP430F5529开发板作为主控,硬禾学堂数字系统的输入、输出扩展板作为外设。完成了LCD显示与输入捕获,并实现游戏手柄控制LCD上的信息。
标签
嵌入式系统
显示
MSP430
2023寒假在家练
IamJiangXu
更新2023-03-27
南京邮电大学
526

基于MSP430训练平台实现游戏手柄控制LCD上的信息

本项目使用TI公司的MSP430F5529开发板作为主控,硬禾学堂数字系统的输入、输出扩展板作为外设。使用Code Composer Studio 12.2.0(以下简称为CCS)作为开发环境,意在通过完成本项目,了解MSP430的基本使用方法,学习TFT显示屏的驱动方法,学习使用MSP430完成输入捕获,熟练使用CCS进行开发。

本项目更加面向MSP430使用初学者,特别是23年电赛准备的同学,介绍了一些从零开始使用MSP430的过程中遇到的问题,以及解决方法,详细介绍MSP430的基本入门。由于本人同样刚刚入门MSP430,本项目虽然基本完成了任务,但是代码可能存在一些可读性差、结构不完善之类的问题,希望项目的一些思路能够为大家提供参考。

一、任务要求

  • IO扩展板上有一个用X、Y二轴电位计制作的游戏手柄,这两个电位计串接在一个振荡电路中,两个电位计的变化会改变阻值,从而改变生成的PWM信号的频率和占空比。

  • 通过单片机的IO端口测量这个PWM信号的频率和占空比的变化,就能够判断出电阻的变化,进而判断出游戏手柄的方向变化。

  • 本任务需要用MSP430板测量IO扩展板上的PWM信号,在LCD上以图形化的方式显示游戏摇杆的变化,通过游戏摇杆的拨动,能够触及LCD的全屏幕。

二、实现的功能

通过MSP430单片机的IO端口,测量游戏手柄PWM信号的频率和占空比,并将频率与占空比信息转换为XY坐标信息,将信息显示在LCD屏幕上,在XY坐标处绘制一个圆圈以图形化的方式显示游戏摇杆的变化,通过游戏摇杆的拨动,能够使圆圈触及LCD的全屏幕。

程序的效果如下:

三、环境配置

 

1.下载并安装CCS

我们编写MSP430F5529的程序,需要用到CCS这个软件。我们进入官网之后,下载CCS并安装即可。

2.导入MSP430Ware

下载MSP430Ware

MSP430Ware里面有很多例程和库函数使用手册,我们可以查看学习。非常重要

打开CCS——>view——>Resource Explorer

点击MSP430——>Embedded Software

点击MSP430Ware后面有三个点——>Install

之后我们就开始下载了,查看下载进度上方会有一个时钟的图标,点击即可查看下载进度

下载完成之后,MSP430Ware后面会出现勾。

查看例程

view——>Resource Explorer

MSP430——>Embedded Software ——>MSP430Ware——>Development Tools——>MSP-EXP430F5529LP——>Peripheral Examples

Driver Library 是库函数例程,Register Level是寄存器例程

本项目在库函数例程的基础上编写实现。

四、设计思路

利用MSP430F5529的定时器进行硬件PWM输入捕获,将中景园ST7735S屏幕的其他单片机例程移植到MSP430中完成屏幕显示,利用串口便于开发过程中的调试信息输出。

主程序结构

输入捕获结构

五、核心代码

 

1.关闭加热

因为扩展版上的加热部分连接到了MSP430单片机的P1.4引脚,MOS管处于通电状态容易导致烫伤,所以我们需要使用以下代码先关闭MOS管。

// 加热MOS管控制低电平
GPIO_setAsOutputPin(GPIO_PORT_P1,GPIO_PIN4);
GPIO_setOutputLowOnPin(GPIO_PORT_P1,GPIO_PIN4);

2.设置时钟

因为MSP430的默认频率只有1M,在我们使用软件SPI驱动屏幕时,会有明显的卡顿情况,所以我们可以通过修改时钟的方式将频率调整为25M

具体的时钟初始化函数如下:

void SystemClock_Init(void)
{
    PMM_setVCore(PMM_CORE_LEVEL_3);     //高主频工作需要较高的核心电压
    //XT1引脚复用
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P5, GPIO_PIN4);
    GPIO_setAsPeripheralModuleFunctionOutputPin(GPIO_PORT_P5, GPIO_PIN5);

    //起振XT1
    UCS_turnOnLFXT1(UCS_XT1_DRIVE_3,UCS_XCAP_3);

    //XT2引脚复用
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P5, GPIO_PIN2);
    GPIO_setAsPeripheralModuleFunctionOutputPin(GPIO_PORT_P5, GPIO_PIN3);

    //起振XT2
    UCS_turnOnXT2(UCS_XT2_DRIVE_4MHZ_8MHZ);

    //XT2作为FLL参考时钟
    UCS_initClockSignal(UCS_FLLREF, UCS_XT2CLK_SELECT, UCS_CLOCK_DIVIDER_8);
    UCS_initFLLSettle(25000, 50);

    //XT1作为ACLK时钟源 = 32768Hz
    UCS_initClockSignal(UCS_ACLK, UCS_XT1CLK_SELECT, UCS_CLOCK_DIVIDER_1);

    //DCOCLK作为MCLK时钟源
    UCS_initClockSignal(UCS_MCLK, UCS_DCOCLK_SELECT, UCS_CLOCK_DIVIDER_1);

    //DCOCLK作为SMCLK时钟源
    UCS_initClockSignal(UCS_SMCLK, UCS_DCOCLK_SELECT, UCS_CLOCK_DIVIDER_1);

    //设置外部时钟源的频率,使得在调用UCS_getMCLK, UCS_getSMCLK 或 UCS_getACLK时可得到正确值
    UCS_setExternalClockSource(32768, 4000000);

}

3. 串口初始化

由于项目中需要使用串口打印调试信息,方便我们的调试,所以我们开启UART1并设置波特率为115200bps,该串口连接到开发板上方的芯片,我们可以直接通过USB来查看该串口信息。

串口时钟配置时,需要使用Ti公司提供的MSP430串口波特率计算网站进行计算,并根据官方手册填入初始化函数。本项目所使用的时钟频率为25M

MSP430 USCI/EUSCI UART Baudrate Calculator (ti.com)

具体的初始化函数如下:

//115200
void Usart1_Init()
{
    //P4.4=UCA1TXD      P4.5=UCA1RXD
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P4, GPIO_PIN5+GPIO_PIN4);

    USCI_A_UART_initParam param1 = {0};
    param1.selectClockSource = USCI_A_UART_CLOCKSOURCE_SMCLK;
    param1.clockPrescalar = 13;
    param1.firstModReg = 9;
    param1.secondModReg = 0;
    param1.overSampling = USCI_A_UART_OVERSAMPLING_BAUDRATE_GENERATION;
    param1.parity = USCI_A_UART_NO_PARITY;   //校验位,无
    param1.msborLsbFirst = USCI_A_UART_LSB_FIRST;  //数据低位先发
    param1.numberofStopBits = USCI_A_UART_ONE_STOP_BIT;  //1停止位
    param1.uartMode = USCI_A_UART_MODE;


    if (STATUS_FAIL == USCI_A_UART_init(USCI_A1_BASE, &param1)){
       return;
    }
    //Enable UART module for operation
    USCI_A_UART_enable(USCI_A1_BASE);

    //Enable Receive Interrupt
    USCI_A_UART_clearInterrupt(USCI_A1_BASE,USCI_A_UART_RECEIVE_INTERRUPT);
    USCI_A_UART_enableInterrupt(USCI_A1_BASE,USCI_A_UART_RECEIVE_INTERRUPT);
}

同时,我们需要书写一个函数用于打印串口数据:

#include <string.h>
#include <stdarg.h>
#include <stdio.h>

void UART_printf(uint16_t baseAddress, const char *format,...)
{
    uint32_t length;
    va_list args;
    uint32_t i;
    char TxBuffer[128] = {0};

    va_start(args, format);
    length = vsnprintf((char*)TxBuffer, sizeof(TxBuffer), (char*)format, args);
    va_end(args);

    for(i = 0; i < length; i++)
        USCI_A_UART_transmitData(baseAddress, TxBuffer[i]);
}

4. 初始化屏幕

本项目所使用的屏幕初始化修改自中景园ST7735 1.44寸液晶屏提供的资料中,STM32的软件SPI驱动程序

我们将官方资料中的\02-1.44LCD显示屏STM32F103C8T6_SPI例程\HARDWARE\LCD目录复制到我们自己的项目中

由于CCS12没有对u8、u16、u32等进行定义,所以我们需要在头文件中自行添加:

#define u8  unsigned char
#define u16 unsigned int
#define u32 unsigned long

例程中使用了delay_ms()函数,我们需要将其重写:

#define MCLK_IN_HZ      25000000
#define delay_us(x)     __delay_cycles((MCLK_IN_HZ/1000000*(x)))
#define delay_ms(x)     __delay_cycles((MCLK_IN_HZ/1000*(x)))

之后,我们还要对lcd_init.h中的LCD端口定义进行修改,将对应的Clr和Set函数分别修改为相应的IO低电平和高电平。此外,由于硬禾学堂扩展板LCD的背光引脚已经被上拉开启,所以我们无需对背光引脚进行设置。修改部分的具体代码如下:

//-----------------LCD端口定义----------------

//// 以下为STM32的LCD端口定义
//#define LCD_SCLK_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_0)//SCL=SCLK
//#define LCD_SCLK_Set() GPIO_SetBits(GPIOA,GPIO_Pin_0)
//
//#define LCD_MOSI_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_1)//SDA=MOSI
//#define LCD_MOSI_Set() GPIO_SetBits(GPIOA,GPIO_Pin_1)
//
//#define LCD_RES_Clr()  GPIO_ResetBits(GPIOA,GPIO_Pin_2)//RES
//#define LCD_RES_Set()  GPIO_SetBits(GPIOA,GPIO_Pin_2)
//
//#define LCD_DC_Clr()   GPIO_ResetBits(GPIOA,GPIO_Pin_3)//DC
//#define LCD_DC_Set()   GPIO_SetBits(GPIOA,GPIO_Pin_3)
//
//#define LCD_CS_Clr()   GPIO_ResetBits(GPIOA,GPIO_Pin_4)//CS
//#define LCD_CS_Set()   GPIO_SetBits(GPIOA,GPIO_Pin_4)
//
//#define LCD_BLK_Clr()  GPIO_ResetBits(GPIOA,GPIO_Pin_5)//BLK
//#define LCD_BLK_Set()  GPIO_SetBits(GPIOA,GPIO_Pin_5)

#define LCD_SCLK_Clr() GPIO_setOutputLowOnPin(GPIO_PORT_P3,GPIO_PIN2)//SCL=SCLK
#define LCD_SCLK_Set() GPIO_setOutputHighOnPin(GPIO_PORT_P3,GPIO_PIN2)

#define LCD_MOSI_Clr() GPIO_setOutputLowOnPin(GPIO_PORT_P3,GPIO_PIN0)//SDA=MOSI
#define LCD_MOSI_Set() GPIO_setOutputHighOnPin(GPIO_PORT_P3,GPIO_PIN0)

#define LCD_RES_Clr()  GPIO_setOutputLowOnPin(GPIO_PORT_P3,GPIO_PIN7)//RES
#define LCD_RES_Set()  GPIO_setOutputHighOnPin(GPIO_PORT_P3,GPIO_PIN7)

#define LCD_DC_Clr()   GPIO_setOutputLowOnPin(GPIO_PORT_P2,GPIO_PIN7)//DC
#define LCD_DC_Set()   GPIO_setOutputHighOnPin(GPIO_PORT_P2,GPIO_PIN7)

#define LCD_CS_Clr()   GPIO_setOutputLowOnPin(GPIO_PORT_P2,GPIO_PIN6)//CS
#define LCD_CS_Set()   GPIO_setOutputHighOnPin(GPIO_PORT_P2,GPIO_PIN6)

由于背光不需要我们手动操作,因此我们需要去lcd_init.c中,将开启背光的宏函数调用关闭:

//	LCD_BLK_Set();//打开背光
//  delay_ms(100);

最后,我们需要对lcd_init.c中的LCD_GPIO_Init函数进行重写:

重写后的函数如下:

void LCD_GPIO_Init(void)
{
// 注释部分为STM32的GPIO初始化代码
//	GPIO_InitTypeDef  GPIO_InitStructure;
// 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	 //使能A端口时钟
//	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
// 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
//	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度50MHz
// 	GPIO_Init(GPIOA, &GPIO_InitStructure);	  //初始化GPIOA
// 	GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5);

    // 设置五个GPIO为输出模式
    GPIO_setAsOutputPin(GPIO_PORT_P2,GPIO_PIN6); // LCD_CSn
    GPIO_setAsOutputPin(GPIO_PORT_P2,GPIO_PIN7); // LCD_DCx
    GPIO_setAsOutputPin(GPIO_PORT_P3,GPIO_PIN0); // LCD_SDA
    GPIO_setAsOutputPin(GPIO_PORT_P3,GPIO_PIN2); // LCD_SCL
    GPIO_setAsOutputPin(GPIO_PORT_P3,GPIO_PIN7); // LCD_RESn

    // 将五个GPIO设置为高电平
    GPIO_setOutputHighOnPin(GPIO_PORT_P2,GPIO_PIN6); // LCD_CSn
    GPIO_setOutputHighOnPin(GPIO_PORT_P2,GPIO_PIN7); // LCD_DCx
    GPIO_setOutputHighOnPin(GPIO_PORT_P3,GPIO_PIN0); // LCD_SDA
    GPIO_setOutputHighOnPin(GPIO_PORT_P3,GPIO_PIN2); // LCD_SCL
    GPIO_setOutputHighOnPin(GPIO_PORT_P3,GPIO_PIN7); // LCD_RESn
}

此时,LCD已经初始化配置完毕,我们可以在main.c中尝试调用屏幕

5.配置输入捕获

首先,创建一些输入捕获所需要的变量

// 用于输入捕获的变量
uint32_t PwmTimePeriod = 0; //定时器周期
uint32_t Pwm_HighTime_Period = 0; //高电平时间
uint32_t TimePeriod = 0; //时间溢出次数
uint32_t Timecap1 = 0; //捕获开始时的时间
uint32_t Timecap2 = 0; //捕获完成高电平的时间
uint32_t Timecap3 = 0; //捕获完成的时间
uint8_t Duty_Circle = 0; //占空比
uint8_t TimeCapNum = 0; //计数
uint8_t FallFlash = 0; //判断上升沿还是下降沿计数

配置输入捕获定时器的初始化

void Timer_A2_Capture_Init()
{
    Timer_A_initContinuousModeParam htim = {0};
    htim.clockSource = TIMER_A_CLOCKSOURCE_SMCLK;
    htim.clockSourceDivider = TIMER_A_CLOCKSOURCE_DIVIDER_1;
    htim.timerInterruptEnable_TAIE = TIMER_A_TAIE_INTERRUPT_ENABLE;
    htim.timerClear = TIMER_A_DO_CLEAR;
    htim.startTimer = true;
    Timer_A_initContinuousMode(TIMER_A0_BASE, &htim);

    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P1, GPIO_PIN2);
    Timer_A_initCaptureModeParam capture_htim = {0};
    capture_htim.captureRegister = TIMER_A_CAPTURECOMPARE_REGISTER_1;
    capture_htim.captureMode = TIMER_A_CAPTUREMODE_RISING_EDGE;
    capture_htim.captureInputSelect = TIMER_A_CAPTURE_INPUTSELECT_CCIxA;
    capture_htim.synchronizeCaptureSource = TIMER_A_CAPTURE_SYNCHRONOUS;
    capture_htim.captureInterruptEnable = TIMER_A_CAPTURECOMPARE_INTERRUPT_ENABLE;
    capture_htim.captureOutputMode = TIMER_A_OUTPUTMODE_OUTBITVALUE;
    Timer_A_initCaptureMode(TIMER_A0_BASE,&capture_htim);
}

参考输入捕获的逻辑结构,编写输入捕获中断回调函数

#pragma vector=TIMER0_A1_VECTOR
__interrupt
void TIMER0_A1_ISR (void)
{
//    static uint16_t Overflow_Times = 0;
//    static uint16_t Sign_Begin = 0, Sign_End = 0;

    switch(TA0IV)
    {
        case TA0IV_TACCR1:
            if(TimeCapNum %2 ==0){
                // 当前是上升沿
                if(FallFlash){
                    Timecap3 = Timer_A_getCaptureCompareCount(TIMER_A0_BASE,TIMER_A_CAPTURECOMPARE_REGISTER_1);
                    PwmTimePeriod = ((TimePeriod * 65536) + Timecap3 - Timecap1);
                    Timecap1 = Timecap3;
                    FallFlash = 0;
                    Duty_Circle = Pwm_HighTime_Period * 100 / PwmTimePeriod;
                }
                else{
                    Timecap1 = Duty_Circle = Pwm_HighTime_Period * 100 / PwmTimePeriod;
                }
                TimePeriod = 0;

                // 由于下降沿比上升沿寄存器差值恰好为CM_1,所以直接加减CM_1即可切换上升沿与下降沿
                HWREG16(TIMER_A0_BASE + TIMER_A_CAPTURECOMPARE_REGISTER_1) += CM_1; // 设置为下降沿捕获
            }
            else{
                // 当前是下降沿
                Timecap2 = Timer_A_getCaptureCompareCount(TIMER_A0_BASE,TIMER_A_CAPTURECOMPARE_REGISTER_1);
                Pwm_HighTime_Period = ((TimePeriod * 65536) + Timecap2 - Timecap1);
                FallFlash = 1;
                HWREG16(TIMER_A0_BASE + TIMER_A_CAPTURECOMPARE_REGISTER_1) -= CM_1;
            }
            TimeCapNum ++;
            Timer_A_clearCaptureCompareInterrupt(TIMER_A0_BASE,TIMER_A_CAPTURECOMPARE_REGISTER_1);
            break;
        case TA0IV_TAIFG:
            TimePeriod ++;
            if(TimePeriod > 5){
                PwmTimePeriod = 0;
                Pwm_HighTime_Period = 0;
                Duty_Circle = 0;
                TimePeriod = 0;
            }
            Timer_A_clearTimerInterrupt(TIMER_A0_BASE);
            break;
        default:
            break;
    }
}

6.LCD取模

根据中景园电子提供的资料,对所需要使用的文字进行取模,并放入lcd_font.h 对需要使用的绿色圆圈图片和白色填充图片进行取模,并放入pic.h 具体请参考代码

7. main.c中的配置

使用以下函数来将频率和占空比信息转化为坐标信息

int rocker_x,rocker_y;
int last_rocker_x, last_rocker_y;
void pwm_to_rockerxy(float period, int duty){
    rocker_x = (1 - (period - 2.30)/2.18) * 128;
//    rocker_y = (1 - (duty - 19)/50) * 128;
    rocker_y = (duty - 30.0)/50 * 128;
    if(rocker_x > 100){
        rocker_x = 100;
    }
    if(rocker_x < 0){
        rocker_x = 0;
    }
    if(rocker_y > 100){
        rocker_y = 100;
    }
    if(rocker_y < 0){
        rocker_y = 0;
    }
}

参考程序结构,在主循环中完成相应信息显示

    while(1)
    {
//        UART_printf(USCI_A1_BASE, "数字测试:%.2f,字符串测试:%s\r\n", 3.14159, "能收到就算成功");
        period = 1000.*PwmTimePeriod/UCS_getSMCLK();
        pwm_to_rockerxy(period, Duty_Circle);
        UART_printf(USCI_A1_BASE, "Period: %f Ms, Duty: %d\%\r\n", period, Duty_Circle);

        if(rocker_x != last_rocker_x || rocker_y != last_rocker_y){
                    LCD_ShowPicture(last_rocker_x-14,last_rocker_y-14,28,28,gImage_white_cicle);
                    LCD_ShowPicture(rocker_x-14,rocker_y-14,28,28,gImage_cicle);
                    last_rocker_x = rocker_x;
                    last_rocker_y = rocker_y;
        }

        LCD_ShowChinese(0,0,"手柄控制器",RED,WHITE,12,0);
        LCD_ShowChinese(0,12,"周期",GREEN,WHITE,12,0);
        LCD_ShowChinese(60,12,"占空比",GREEN,WHITE,12,0);
        LCD_ShowChar(64, 0, 'X', BLUE,WHITE,12,0);
        LCD_ShowChar(96, 0, 'Y', BLUE,WHITE,12,0);

        LCD_ShowFloatNum1(25,12,period,4,GREEN,WHITE,12);
        LCD_ShowIntNum(100,12,Duty_Circle,2,GREEN,WHITE,12);

        LCD_ShowIntNum(76,0,rocker_x,3,BLUE,WHITE,12);
        LCD_ShowIntNum(108,0,rocker_y,3,BLUE,WHITE,12);

//        delay_ms(1000);
    }

六、遇到的主要难题及解决方法

  1. MSP430的默认时钟频率为1M,软件SPI刷新缓慢。解决方法为手动配置时钟,将频率设置为25M。
  2. 没有完善的输入捕获程序,没有找到输入捕获的边沿切换。解决方法为在中断回调函数中手动书写PWM输入捕获的实现,通过修改寄存器来修改上升沿和下降沿捕获。

七、不完善的地方

  1. 代码写的很丑,因为第一次使用CCS与MSP430,是边学边写的,导致代码结构不是很清晰。

  2. 由于硬件SPI配置过程中遇到了一些问题,导致无法实现硬件SPI驱动,因此使用了软件SPI驱动屏幕,屏幕的刷新效率和资源占用情况较大。

  3. 屏幕的局部刷新实现方法比较原始,仅通过先把上次的图形清空后写入新的图像,会导致屏幕刷新不够流畅

  4. 输入捕获代码不是很规范,在切换上升和下降沿时未找到相关库函数,于是直接使用寄存器修改。

八、一些建议

硬件方面,建议在摇杆上方添加一个小的手柄,方便摇杆操控。建议增加加热MOS管控制引脚拨码开关,防止未关闭加热MOS管导致烫伤。

软件方面,建议提供MSP430F5529开发板引脚与外设扩展板引脚对应关系表,方便程序设计。

附件下载
MSP430_game_handle.zip
团队介绍
南京邮电大学 2021级 自动化学院、人工智能学院 人工智能专业本科生 爱好深度学习、嵌入式开发
团队成员
IamJiangXu
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号