2023寒假一起练平台(4)——用基于MSP430备战电赛控制类题目的训练平台实现游戏手柄控制LCD上的信息
本项目实现了2023寒假一起练平台(4)- 基于MSP430备战电赛控制类题目的训练平台 项目2 的要求,即: 使用游戏手柄控制LCD上的信息
标签
PWM
LCD
MSP430
2023寒假在家练
摇杆
Geralt
更新2023-03-28
河南大学
907

前言

本项目实现了2023寒假一起练平台(4)- 基于MSP430备战电赛控制类题目的训练平台项目2的要求,“ 游戏手柄控制LCD上的信息”,即

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

Foob1Cgx2ZFPFDMtuDG1D4l9a4N9

硬件介绍

2023寒假一起练平台(4)是基于MSP430F5529的平台

MSP430F5529是一款带有USB接口,最高主频达25MHz的8位MCU。拥有128KB Flash和 8KB SRAM。属于MSP430家族的一员。

MSP430相信各位即使是没有用过也肯定听说过它的鼎鼎大名。它是Ti出品的一系列8位单片机的总称,拥有非常优秀的低功耗性能。

本次的板卡也是鼎鼎有名的launchpad,由Ti官方出品,具有板载仿真和一些简单的交互功能如按钮和LED。

FqDKZS7tSDk02vvfs6-ruEvOL7GQ

其引脚映射图如下

Fv17Pxa1ivnabZY_Jlnc1df1ZBLH

为了更加方便我们的学习,平台还额外搭配了一款输入输出扩展版,提供了以下功能扩展

  • 按键、旋转编码器输入 - 以模拟信号的方式
  • 双电位计控制输入 - 以数字信号的方式
  • RGB三色LED显示
  • 1.44寸128*128 LCD,SPI总线访问
  • MMA7660三轴姿态传感器
  • 电阻加热
  • 温度传感器
  • 与MSP430 Launch Pad开发板的接口

Fkjf6i7knKpu3aQH1s47aTHtuDCq

原理图: 

FpMonn5FuKnpG3ghtJb_JccCrkxW

此扩展版与另一项目的ESP32-S2模块也是兼容的,但是本项目中我们只使用MSP430LP5529的接口。

本次活动设定了6个不同的项目目标,我选择的是:项目2,游戏手柄控制LCD上的信息.即在LCD上以图形化的方式显示游戏摇杆的变化,通过游戏摇杆的拨动,触及LCD的全屏幕。

对与本项目而言,我们将使用底板上的摇杆和LCD两个外设。这两个外设与MSP430的引脚映射如下:

外设信号 引脚编号
摇杆信号输入 P1.2
LCD显示屏时钟信号 SCLK P3.2
LCD显示屏数据信号 MOSI P3.0
LCD显示屏复位信号 RST P3.7
LCD显示屏片选信号 CS P2.6
LCD显示屏数据/指令选择信号 DCX P2.7

为了方便使用,以上已经使用的引脚和扩展板上其他未使用的引脚均在文件main.h中以宏的方式进行定义

#define LED_1_GPIO_Port                GPIO_PORT_P1
#define LED_1_Pin                      GPIO_PIN0

#define LED_2_GPIO_Port                GPIO_PORT_P4
#define LED_2_Pin                      GPIO_PIN7

#define LED_R_GPIO_Port                GPIO_PORT_P2
#define LED_R_Pin                      GPIO_PIN5

#define LED_G_GPIO_Port                GPIO_PORT_P2
#define LED_G_Pin                      GPIO_PIN4

#define LED_B_GPIO_Port                GPIO_PORT_P1
#define LED_B_Pin                      GPIO_PIN5

#define HEATER_GPIO_Port               GPIO_PORT_P1
#define HEATER_Pin                     GPIO_PIN4

#define LCD_NCS_GPIO_Port              GPIO_PORT_P2
#define LCD_NCS_Pin                    GPIO_PIN6

#define LCD_DCX_GPIO_Port              GPIO_PORT_P2
#define LCD_DCX_Pin                    GPIO_PIN7

#define LCD_RST_GPIO_Port              GPIO_PORT_P3
#define LCD_RST_Pin                    GPIO_PIN7

#define SENSOR_SCL_GPIO_Port           GPIO_PORT_P4
#define SENSOR_SCL_Pin                 GPIO_PIN2

#define SENSOR_SDA_GPIO_Port           GPIO_PORT_P4
#define SENSOR_SDA_Pin                 GPIO_PIN1

#define SENSOR_ALERT_GPIO_Port         GPIO_PORT_P8
#define SENSOR_ALERT_Pin               GPIO_PIN2

#define JOYSTICK_GPIO_Port             GPIO_PORT_P1
#define JOYSTICK_Pin                   GPIO_PIN2
#define JOYSTICK_INT_Port              PORT1_VECTOR
#define JOYSTICK_IES_REG               P1IES
#define JOYSTICK_PORT_IN_REG           P1IN

#define ENCODER_GPIO_Port              GPIO_PORT_P6
#define ENCODER_Pin                    GPIO_PIN0

#define KEY1_GPIO_Port                 GPIO_PORT_P6
#define KEY1_Pin                       GPIO_PIN2

#define KEY2_GPIO_Port                 GPIO_PORT_P6
#define KEY2_Pin                       GPIO_PIN3

开发环境介绍

MSP430属于比较专业的MCU,且资源较为受限,不适合使用传统的如Arduino、MicroPython之类的方式进行开发。其推荐的开发环境为官方的Code Composer Studio(简称CCS)。

CCS是一款基于Eclipse的开发环境,在CCS中可以使用C/C++进行MSP430相关应用的开发,其默认编译器为Ti自行开发的C编译器,可选GCC编译器。

Fh2Nb2IoM7FeEIhxh95IvZAnWoPb

本项目中,为了更好的配合MSP430的特性,采用默认了Ti-C编译器,并使用C语言进行开发,

实现思路和程序主要流程

本项目的代码共分为四个子系统的设计,包括驱动子系统、时钟子系统、输入子系统和图形子系统。

驱动子系统负责与MSP430的底层进行对接,包括对系统时钟的管理,各个外设(如GPIO,SPI,TIMER)的初始化以及对接,还有对中断的管理(中断的启用、优先级以及中断处理子程序的管理)

时钟子系统负责对整个程序提供时间基准(时间戳)和延时服务

输入子系统负责对摇杆输出的PWM讯号进行测量,计算并管理光标当前的速度、方向和位置

图形子系统专注与LCD相关的操作,包括对LCD本身的控制(如初始化、指令/数据的发送、绘制区域的管理“窗函数”)和图形绘制(包含文本绘制和基本图形的绘制(光标绘制))

整个程序代码的架构如下:

FrTo1KjFY8cnxHfHTWw19zqNEMeT

 

程序的主要流程如下:

FgUL2fuylL64Q6CPwReM_6d0qc28

外设的初始化(驱动系统)

与基于Arduino的项目不同,因为我们选择了更加专业的开发平台,所以我们需要自行对MSP430的各个外设进行初始化,包括系统时钟、GPIO、SPI、TIMER、中断系统等等。以下是一些重要外设的初始化说明。

 

系统时钟的初始化

在默认情况下,MSP430工作于低速内部时钟,此时的主频只有不到1MHz,虽然这个状态比较省电,对于我们的程序来说太慢了。所以我们需要首先初始化时钟,使其工作在一个比较理想的速度上。在本项目中,我们设置系统时钟(MCLK)为16MHz。此处我们直接使用官方示例中的代码即可:

 static void SetupClocks(void){

    // Set core voltage level to handle 8MHz clock rate
    PMM_setVCore( PMM_CORE_LEVEL_1 );

    // Set the XT1/XT2 crystal frequencies used on the LaunchPad, and connected
    // to the clock pins, so that driverlib knows how fast they are (these are
    // needed for the DriverLib clock 'get' and crystal start functions)
    UCS_setExternalClockSource(
            LF_CRYSTAL_FREQUENCY_IN_HZ,                                         // XT1CLK input
            HF_CRYSTAL_FREQUENCY_IN_HZ                                          // XT2CLK input
    );

    // Set ACLK to use REFO as its oscillator source (32KHz)
    UCS_initClockSignal(
            UCS_ACLK,                                    // Clock you're configuring
            UCS_REFOCLK_SELECT,                          // Clock source
            UCS_CLOCK_DIVIDER_1                          // Divide down clock source by this much
    );

    // Set REFO as the oscillator reference clock for the FLL
    UCS_initClockSignal(
            UCS_FLLREF,                                  // Clock you're configuring
            UCS_REFOCLK_SELECT,                          // Clock source
            UCS_CLOCK_DIVIDER_1                          // Divide down clock source by this much
    );

    // Set MCLK and SMCLK to use the DCO/FLL as their oscillator source (8MHz)
    // The function does a number of things: Calculates required FLL settings; Configures FLL and DCO,
    // and then sets MCLK and SMCLK to use the DCO (with FLL runtime calibration)
    UCS_initFLLSettle(
            MCLK_DESIRED_FREQUENCY_IN_KHZ,               // MCLK frequency
            MCLK_FLLREF_RATIO                            // Ratio between MCLK and FLL's reference clock source
    );
}

 

SPI的初始化

因为扩展板上的LCD屏使用的是SPI总线与MSP430进行通讯,所以我们还需要初始化SPI外设。SPI外设可以使用多个时钟源。通常而言,时钟的频率越高,通讯的速率越高,显示越流畅。为了显示的流畅性,我们选择系统时钟SMCLK作为SPI的时钟源。

具体初始化代码如下,注意,我们在这里还同时初始化了LCD屏需要的一些辅助GPIO:

    // LCD辅助引脚
    GPIO_setAsOutputPin(LCD_NCS_GPIO_Port, LCD_NCS_Pin);
    GPIO_setAsOutputPin(LCD_DCX_GPIO_Port, LCD_DCX_Pin);
    GPIO_setAsOutputPin(LCD_RST_GPIO_Port, LCD_RST_Pin);

   // ***********************************
    // 初始化SPI
    // ***********************************

    // 初始化相关IO
    GPIO_setAsPeripheralModuleFunctionOutputPin(GPIO_PORT_P3, GPIO_PIN0 + GPIO_PIN2);
    GPIO_setAsPeripheralModuleFunctionInputPin(GPIO_PORT_P3, GPIO_PIN1);

    // 配置SPI
    USCI_B_SPI_initMasterParam param = {0};
    param.selectClockSource = USCI_B_SPI_CLOCKSOURCE_SMCLK;
    param.clockSourceFrequency = UCS_getSMCLK();
    param.desiredSpiClock = SPI_A_CLK;
    param.msbFirst = USCI_B_SPI_MSB_FIRST;
    param.clockPhase = USCI_B_SPI_PHASE_DATA_CAPTURED_ONFIRST_CHANGED_ON_NEXT;
    param.clockPolarity = USCI_B_SPI_CLOCKPOLARITY_INACTIVITY_LOW;

    uint8_t ret = USCI_B_SPI_initMaster(USCI_B0_BASE, &param);
    if(ret != STATUS_SUCCESS){
        GPIO_setOutputLowOnPin(LED_R_GPIO_Port, LED_R_Pin);
        while(1);
    }

    // 启用SPI,并使用polling模式
    USCI_B_SPI_enable(USCI_B0_BASE);
    USCI_B_SPI_clearInterrupt(USCI_B0_BASE, USCI_B_SPI_RECEIVE_INTERRUPT);
    USCI_B_SPI_disableInterrupt(USCI_B0_BASE, USCI_B_SPI_RECEIVE_INTERRUPT);
    USCI_B_SPI_disableInterrupt(USCI_B0_BASE, USCI_B_SPI_TRANSMIT_INTERRUPT);

 

时钟子系统(时间戳系统)

因为牵扯到频率的测量、加速度的计算等功能,我们需要一个时间基准也就是时间戳,简单来说就是程序/TIMER启动后的时间长度。

与Arduino平台不同,MSP430的软件库中并没有默认的可以提供时间戳的API。所以我们还需要一个定时器来提供时间基准。

MSP430有两组Timer,分别为TimerA和TimerB,这里我们使用TimerA,并且在0通道上开启溢出中断。

为了提供一个稳定和易于计算的时基,我们设置Timer为MCLK的8分频,并且设置周期为19,这样,在16MHz的MCLK频率下,Timer的中断的频率就是100kHz,即10us触发一次溢出中断。

    Timer_A_clearTimerInterrupt(TIMER_A0_BASE);

    TA0CCTL0 = CCIE;                             // 定时器CCR0中断使能
    TA0CCR0 = 19;                                // 设置定时器的时间
    TA0CTL = TASSEL_2 + ID_3 + MC_1 + TACLR;     // 定时器时钟选择SMCLK,计数方式为增计数模式,清除TAR,分频系数8

 

timer的中断处理函数里面,我们不做多的操作,只是简单地增加一个计数器uwTick的值。这个值就是我们的时间基准值,也就是时间戳。由于Timer的中断频率被我们设置为了10us一次,所以这个值的就是以10us为单位的时间戳(即Timer启动后的时间)。用于频率测量时,最高的分辨率可以达到100kHz。对于本项目最高不过500Hz的PWM频率而言,是绰绰有余了。

#pragma vector = TIMER0_A0_VECTOR
__interrupt void TIMER0_A0_ISR(void)
{
    TA0CTL &= ~TAIFG;
    uwTick++;
}

 

输入系统(摇杆状态的测量)

由项目的硬件资料可知,与通常的输出模拟信号的摇杆不同。本次硬件平台的摇杆输出的是一个PWM方波讯号。摇杆在X方向的位置变化会影响PWM的频率,而摇杆Y方向的位置的变化则会影响PWM的占空比。这样,我们就不能通过ADC读取摇杆在两个方向上的分量来测量摇杆的当前位置,取而代之的是我们需要测量这个PWM波的频率和占空比,这分别对应摇杆在X,Y方向上的位置。之后我们就可以根据摇杆的位置来决定光标移动的方向和移动的速度。

与传统的模拟输出相比,这样的好处是可以节省一个IO(模拟方式需要独立对两个方向上的输出电压进行测量)而且可以得到更加精确的位置(因为频率测量可以达到很高的精度),但是缺点是比起ADC方式来说不够直观。

如图所示,对于PWM的信号,两次上升沿(或者两次下降沿)之间的时间差就是周期,每个下降沿与上次上升沿之间的时间差就是脉宽时间。

FnoVtXLg-jN1fDPUHAPspZRap5ez

得到了周期和脉宽时间,那么周期的倒数就是频率,脉宽除以周期就可以得到占空比。

如何对波形进行测量?此处我选择使用MSP430的IO中断功能配合时间戳来实现。

首先我们需要初始化摇杆的引脚为输入功能,并且开启引脚中断。注意,MSP430并未提供引脚变化中断,因为我们以两次上升沿之间的时间作为脉宽,所以我们此处首先将中断类型设置为上升沿触发。

    // 摇杆输入引脚开启中断
    GPIO_selectInterruptEdge(JOYSTICK_GPIO_Port, JOYSTICK_Pin, GPIO_LOW_TO_HIGH_TRANSITION);
    GPIO_clearInterrupt(JOYSTICK_GPIO_Port, JOYSTICK_Pin);
    GPIO_enableInterrupt(JOYSTICK_GPIO_Port, JOYSTICK_Pin);

接下来是摇杆输出信号的中断处理函数,我们在这里通过首先通过读取寄存器,判断触发边沿是上边沿还是下边沿。

之前我们已经通过时钟子系统实现了程序运行的时间戳。这样,通过记录波形每两次上升沿之间的时间差,我们就可以得到波形的周期。周期有了,取它的倒数(或者说是1s时间除以周期)就是波形的频率了。

占空比的测量也同样非常简单,我们记录任何下降沿发生时的时间,然后这个下降沿与上一个上升沿的时间差就是波形的正脉宽。

我们用正脉宽时间除以波形的周期,就可以得到波形的占空比。这里通过乘以1000来获得占空比的千分比值。

#pragma vector=JOYSTICK_INT_Port
__interrupt void Joystick_ISR(void)
{

    static uint32_t lastRiseTimestamp = 0;
    static uint32_t lastFallingTimestamp = 0;
    static uint32_t trigTimestamp = 0;

    GPIO_clearInterrupt(JOYSTICK_GPIO_Port, JOYSTICK_Pin);

    trigTimestamp = uwTick;

    // 判断是上升沿触发还是下降沿触发
    if ((JOYSTICK_IES_REG & JOYSTICK_Pin) != JOYSTICK_Pin)
    {
        // 两次上升沿之间就是信号的周期
        pwmPeriod = trigTimestamp - lastRiseTimestamp;
        if (pwmPeriod != 0)
        {
            pwmFreq = 100000UL / pwmPeriod;
        }
        lastRiseTimestamp = trigTimestamp;
    }
    else
    {
        if (lastFallingTimestamp < lastRiseTimestamp)
        {
            // 获取信号的正脉宽
            highTime = trigTimestamp - lastRiseTimestamp;
            if (pwmPeriod != 0)
            {
                pwmDuty = highTime * 1000 / pwmPeriod;
            }
        }
        lastFallingTimestamp = trigTimestamp;
    }

    // 更改触发边沿设置,为下一次边沿的触发做准备
    JOYSTICK_IES_REG ^= JOYSTICK_Pin;
}

至此,我们就成功得到了摇杆当前的状态。

得到了摇杆的状态后,为了实现“光标移动的速度与拨动摇杆的力度相对应”的功能,我们就需要对光标的加速度进行计算。因为我们获取摇杆状态是在系统中断中完成的。基于“中断处理最简化”的策略(而且MSP430的性能并不是十分出众),我们最好不要在中断处理函数中进行额外的操作。所以我们将此部分的功能放在接下来的UI绘制中完成。

LCD的初始化

扩展板上的LCD通过SPI接口与MSP430开发板连接,之前我们已经初始化了SPI总线和相关的辅助IO,此处我们只需要实现一个对应的传输函数即可。为了实现最高的效率,我选择直接操作寄存器来完成数据的发送:

// SPI底层传输函数
__INLINE static void ST7735_Transmit(const uint8_t* data, uint16_t len){
    while(len--){
        HWREG8(USCI_B0_BASE + OFS_UCBxTXBUF) = (*data++);
        __delay_cycles(4);
    }
}

 

然后是LCD需要的数据和指令传输函数:

 // 向LCD传输指令
void ST7735_WriteCommand(uint8_t cmd) {
    GPIO_ResetPin(ST7735_DCX_PORT, ST7735_DCX_PIN);

#if !SCREEN_ALWAYS_SELECT
    GPIO_ResetPin(ST7735_NCS_PORT, ST7735_NCS_PIN);
#endif

    ST7735_Transmit(&cmd, 1);

#if !SCREEN_ALWAYS_SELECT
    GPIO_SetPin(ST7735_NCS_PORT, ST7735_NCS_PIN);
#endif
}

// 向LCD传输数据
void ST7735_WriteDataByte(uint8_t data){
    GPIO_SetPin(ST7735_DCX_PORT, ST7735_DCX_PIN);

#if !SCREEN_ALWAYS_SELECT
    GPIO_ResetPin(ST7735_NCS_PORT, ST7735_NCS_PIN);
#endif

    ST7735_Transmit(&data, 1);

#if !SCREEN_ALWAYS_SELECT
    GPIO_SetPin(ST7735_NCS_PORT, ST7735_NCS_PIN);
#endif
}

 

最后根据LCD面板的参数,在初始化阶段写入初始化代码序列(严格来收,此部分的代码应向LCD面板生产厂家索取,但是由于ST7735属于十分通用的LCD屏幕控制器,市场上的使用ST7735的LCD也基本上完全兼容——本次的扩展版上的LCD模组亦是如此。所以初始化代码可直接参考Adafruit或者TFT_eSPI等开源库的LCD驱动代码,无需使用专用代码)

void ST7735_Init() {

    GPIO_SetPin(ST7735_NCS_PORT,  ST7735_NCS_PIN);
    GPIO_SetPin(ST7735_DCX_PORT, ST7735_DCX_PIN);

#ifdef ST7735_RST_PORT
    GPIO_SetPin(ST7735_RST_PORT, ST7735_RST_PIN);
    HAL_Delay(100);
    GPIO_ResetPin(ST7735_RST_PORT, ST7735_RST_PIN);
    HAL_Delay(5);
    GPIO_SetPin(ST7735_RST_PORT, ST7735_RST_PIN);
    HAL_Delay(200);
#endif

#if SCREEN_ALWAYS_SELECT
    GPIO_ResetPin(ST7735_NCS_PORT, ST7735_NCS_PIN);
#endif


    /** Sleep out */
    ST7735_WriteCommand(0x11);
    HAL_Delay(120);

    /** ST7735S Frame Rate */
    ST7735_WriteCommand(0xB1);
    ST7735_WriteDataByte(0x05);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteCommand(0xB2);
    ST7735_WriteDataByte(0x05);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteCommand(0xB3);
    ST7735_WriteDataByte(0x05);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteDataByte(0x05);
    ST7735_WriteDataByte(0x3C);
    ST7735_WriteDataByte(0x3C);

    /** Display Inversion Control */
    ST7735_WriteCommand(0xB4);
    ST7735_WriteDataByte(0x03);

    /** Power Control */
    ST7735_WriteCommand(0xC0);
    ST7735_WriteDataByte(0x28);
    ST7735_WriteDataByte(0x08);
    ST7735_WriteDataByte(0x04);
    ST7735_WriteCommand(0xC1);
    ST7735_WriteDataByte(0XC0);
    ST7735_WriteCommand(0xC2);
    ST7735_WriteDataByte(0x0D);
    ST7735_WriteDataByte(0x00);
    ST7735_WriteCommand(0xC3);
    ST7735_WriteDataByte(0x8D);
    ST7735_WriteDataByte(0x2A);
    ST7735_WriteCommand(0xC4);
    ST7735_WriteDataByte(0x8D);
    ST7735_WriteDataByte(0xEE);
    ST7735_WriteCommand(0xC5);
    ST7735_WriteDataByte(0x1A);

    /** Inversion Command */
    ST7735_WriteCommand(ST7735_DEFAULT_INV_CMD);

    /** Memory Data Access Control */
    ST7735_WriteCommand(0x36);
    ST7735_WriteDataByte(ST7735_ROTATION_OPTION);

    /** Interface Pixel Format */
    ST7735_WriteCommand(0x3A);   // RGB565(65k)
    ST7735_WriteDataByte(0x05);

    /** Gamma Setting */
    ST7735_WriteCommand(0xE0);
    ST7735_WriteDataByte(0x04);
    ST7735_WriteDataByte(0x22);
    ST7735_WriteDataByte(0x07);
    ST7735_WriteDataByte(0x0A);
    ST7735_WriteDataByte(0x2E);
    ST7735_WriteDataByte(0x30);
    ST7735_WriteDataByte(0x25);
    ST7735_WriteDataByte(0x2A);
    ST7735_WriteDataByte(0x28);
    ST7735_WriteDataByte(0x26);
    ST7735_WriteDataByte(0x2E);
    ST7735_WriteDataByte(0x3A);
    ST7735_WriteDataByte(0x00);
    ST7735_WriteDataByte(0x01);
    ST7735_WriteDataByte(0x03);
    ST7735_WriteDataByte(0x13);
    ST7735_WriteCommand(0xE1);
    ST7735_WriteDataByte(0x04);
    ST7735_WriteDataByte(0x16);
    ST7735_WriteDataByte(0x06);
    ST7735_WriteDataByte(0x0D);
    ST7735_WriteDataByte(0x2D);
    ST7735_WriteDataByte(0x26);
    ST7735_WriteDataByte(0x23);
    ST7735_WriteDataByte(0x27);
    ST7735_WriteDataByte(0x27);
    ST7735_WriteDataByte(0x25);
    ST7735_WriteDataByte(0x2D);
    ST7735_WriteDataByte(0x3B);
    ST7735_WriteDataByte(0x00);
    ST7735_WriteDataByte(0x01);
    ST7735_WriteDataByte(0x04);
    ST7735_WriteDataByte(0x13);

    ST7735_WriteCommand(0x29); //Display on
}

通过传输初始化代码之后,LCD模块就进入了就绪状态。我们就可以通过一系列绘图函数来进行绘图了,绘图相关函数的代码编写十分繁琐。我参考了部分开源代码,也手动编写了一部分,将其全部封装在“ST7735.h”和“ST7735.C”两个文件中。这两个文件可以在附件的代码文件中找到,此处不再赘述。

UI的绘制

LCD准备就绪后,我们就要进入程序的主逻辑循环了,此处使用一个无限循环来实现。每循环一次,LCD绘制一帧。

    while(1)
    {
        // 主逻辑循环
        // ....
    }

在每一帧的开始,我们更新测量得到的频率和占空比的数据,并且显示在LCD的左上角。

    // 打印摇杆输出信号的频率和占空比
    ST7735_DrawString("Hz,", 19, 1);
    ST7735_DrawString("%", 49, 1);
    sprintf(strbuf, "%d", (int) pwmFreq);
    ST7735_DrawString(strbuf, 1, 1);
    sprintf(strbuf, "%d", (int) (pwmDuty / 10));
    ST7735_DrawString(strbuf, 37, 1);

然后根据频率和占空比计算摇杆在XY方向的加速度

    // 计算X方向的加速度
    if (pwmFreq >= PWM_FREQ_TH_MAX && pwmFreq <= PWM_FREQ_MAX)
    {
        accX = map(pwmFreq, PWM_FREQ_TH_MAX, PWM_FREQ_MAX, 0, JOYSTICK_ACC_MAX + 1);
    }
    else if (pwmFreq <= PWM_FREQ_TH_MIN && pwmFreq >= PWM_FREQ_MIN)
    {
        accX = map(pwmFreq, PWM_FREQ_MIN, PWM_FREQ_TH_MIN, -JOYSTICK_ACC_MAX, 0);
    }
    else
    {
        accX = 0;
    }

    // 计算Y方向的加速度
    if (pwmDuty >= PWM_DUTY_TH_MAX && pwmDuty <= PWM_DUTY_MAX)
    {
        accY = map(pwmDuty, PWM_DUTY_TH_MAX, PWM_DUTY_MAX, 0, JOYSTICK_ACC_MAX + 1);
    }
    else if (pwmDuty <= PWM_DUTY_TH_MIN && pwmDuty >= PWM_DUTY_MIN)
    {
        accY = map(pwmDuty, PWM_DUTY_MIN, PWM_DUTY_TH_MIN, -JOYSTICK_ACC_MAX, 0);
    }
    else
    {

这里用到了一个自行编写的map函数(参考Arduino的实现),用来把频率和占空比的变化范围映射到指定的加速度的范围内

int32_t map(int32_t x, int32_t in_min, int32_t in_max, int32_t out_min, int32_t out_max) {
  return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

然后根据加速度更新光标的位置

    // 计算当前光标位置
    curX += accX;
    curY += accY;
    if (curX >= ST7735_HW_WIDTH - 1)
    {
        curX = ST7735_HW_WIDTH - 1;
    }
    if (curX < 0)
    {
        curX = 0;
    }
    if (curY >= ST7735_HW_HEIGHT - 1)
    {
        curY = ST7735_HW_HEIGHT - 1;
    }
    if (curY < 0)
    {
        curY = 0;
    }

因为MSP430的图形性能并不算强劲,为了加快速度,这里如果光标没有发生移动,我们就不用绘制它。当前循环直接结束,开始下一循环。

    // 如果光标从未移动,则不再绘制光标
    if (lastCurX == curX && lastCurY == curY)
    {
        continue;
    }

如果光标发生了移动,我们先清除旧的光标,然后在新的位置绘制光标

    // 清除旧的光标
    ST7735_DrawFastHorLine(lastCurX - JOYSTICK_CURSOR_SIZE, lastCurY, JOYSTICK_CURSOR_SIZE * 2, 0);
    ST7735_DrawFastVerLine(lastCurX, lastCurY - JOYSTICK_CURSOR_SIZE, JOYSTICK_CURSOR_SIZE * 2, 0);

    // 绘制边框
    ST7735_DrawRect(0, 0, ST7735_HW_WIDTH, ST7735_HW_HEIGHT, BORDER_COLOR);

    // 记录光标位置
    lastCurX = curX;
    lastCurY = curY;

    // 绘制新的光标
    ST7735_DrawFastHorLine(lastCurX - JOYSTICK_CURSOR_SIZE, lastCurY, JOYSTICK_CURSOR_SIZE * 2, JOYSTICK_CURSOR_COLOR);
    ST7735_DrawFastVerLine(lastCurX, lastCurY - JOYSTICK_CURSOR_SIZE, JOYSTICK_CURSOR_SIZE * 2 , JOYSTICK_CURSOR_COLOR);

随后本轮循环结束,开始下一轮的循环。

至此,我们就实现了全部的项目需求。

结语

通过本次活动,我们学习到了MSP430开发的一些基础知识包括对MSP430中断、GPIO、SPI等外设的使用,还有对还有波形的测量和LCD屏的驱动。希望我在本项目中的分享可以帮到你。也希望电子森林的活动能越办越好。谢谢大家。

附件下载
JoystickApp.zip
项目源代码
团队介绍
个人开发者
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号