前言

  在上篇文章中,我们使用了DL_Common_delayCycles函数来进行粗略的延时,并且整个程序是顺序执行的,这并不利于更复杂的工程开发。因此这篇文章中,我们来研究MSPM0的SysTick定时器,利用它来为整个程序产生计时信息,这样一来,我们不仅可以写出更准确的延时函数,还能实现无阻塞的按键消抖逻辑,并且可以按时间来进行任务调度,使得各个程序任务看起来是“并行”执行的。

SysTick定时器

  SysTick定时器是与M0的CPU外设之一,它与CPU紧密耦合。在RTOS中,它可以作为操作系统的计时器;而对于我们目前的裸机应用来说,可以将SysTick作为一个简单的定时器,产生周期性的中断供我们使用。

SysTick定时器配置

  SysTick定时器的配置十分简单,建立工程,并且配置好时钟树后,添加SysTick配置,填上定时周期并启用即可。

图1 SysTick配置

图1 SysTick配置

  我这里用的CPU主频是80MHz(40MHz二倍频),所以80000个MCLK周期是1ms,这样配置后,SysTick定时器会每隔1ms给CPU一个中断。这个定时中断的间隔不能太短,不然中断过于频繁会占用太多CPU资源;当然间隔太长的话,我们的计时精度就会下降,一般1ms是个不错的选择。

建立自己的代码文件

  下面来编写Tick的代码,为了便于管理,我习惯把自己编写的代码文件单独放在一个User文件夹中,与IDE自动生成的文件区分开,另外由于单片机的工程文件一般不多,我喜欢把.h和.c文件都放在一个路径下,便于跳转查看。在工程路径下新建一个User文件夹,在CCS中右键当前工程,选择Properties,并点击弹出的窗口左下角的Show advanced settings,接着在C/C++ General选项卡中找到Paths and Symbols,这里我们需要将User文件夹添加到Includes和Source Location中,使得CCS能正确找到我们自己写的文件。对于Includes的添加,最好使用相对路径的写法,点击Add后,有Variables选项,输入“pro”来搜索,选择PROJECT_ROOT这一项,这时路径框中会自动出现${PROJECT_ROOT},这个内容会被CCS自动替换为工程的实际路径,这样在给别人分享工程时,对方就不需要人工修改这个路径了。我们的User文件夹就在工程路径下,因此填写为“${PROJECT_ROOT}/User”即可。

图2 Includes配置

图2 Includes配置

  对于Source Location,直接选择User文件夹即可。

图3 Source Location配置

图3 Source Location配置

  应用后会提示需要Rebuild Index,确认即可。这时我们就可以在User文件夹中编写我们自己的代码文件了。

  Interrupts.h文件:

#ifndef __INTERRUPTS_H__
#define __INTERRUPTS_H__

#include "ti_msp_dl_config.h"
#include "Tick.h"

void SysTick_Handler(void);

#endif /* #ifndef __INTERRUPTS_H__ */

  Interrupts.c文件:

#include "Interrupts.h"

// SysTick中断服务函数(1ms)
void SysTick_Handler(void) {
    Tick_SysTickCallback();
}

  其中SysTick_Handler是SysTick定时器的中断服务函数,每当SysTick定时器产生中断时,这个函数就被调用。MSPM0的SDK中对于中断服务函数的封装不多,通常一个外设只对应一个中断服务函数,但一个外设的中断源可能有很多种,因此在中断服务函数内,我们通常需要根据外设的中断索引寄存器(IIDX)来判断中断源,并执行相应的回调。这有可能导致不同文件需要同一个中断服务函数的问题,所以我目前的做法是把所有启用的中断服务函数都写在Interrupts.c文件中,对应的外设文件中只提供相应中断回调函数(Callback)。(当然,对于SysTick来说,中断源只有它自身一个,所以多写出一个回调函数仅仅是为了与其它外设一致)对于实际的计时逻辑,我们在另外的Tick文件中实现。

  Tick.h文件:

#ifndef __TICK_H__
#define __TICK_H__

#include <stdint.h>
#include "ti_msp_dl_config.h"
#include "UserTask.h"

extern volatile uint32_t Tick;

void Tick_delay(uint32_t t);
void Tick_SysTickCallback(void);

#endif /* #ifndef __TICK_H__ */

  Tick.c文件:

#include "Tick.h"

volatile uint32_t Tick = 0;

/**
 * @brief 延时(使用SysTick中断计时)
 * @param t 延时时间(ms)
*/
void Tick_delay(uint32_t t) {
    uint32_t tEnd = Tick + t;
    while (Tick < tEnd);
}

// SysTick中断回调(1ms)
void Tick_SysTickCallback(void) {
    Tick++;
    UserTask_tick();
}

  对于计时逻辑,我们需要实现的是Tick_SysTickCallback这一回调函数,此时这个函数的内容只是让一个Tick全局变量自增,后续当有更多计时需求时,可以继续向这个回调函数中添加内容。对于中断中需要修改的变量,都应该定义为volatile类型,以防编译器将其优化掉。Tick_delay函数则是一个毫秒延时函数,当我们已经有一个全局的毫秒计时变量后,延时函数就非常简单了,只需要根据时长计算出结束时刻,然后等待到这一时刻即可。UserTask_tick函数是用户任务的计时函数,在后面进行介绍。

  上面的代码也能够体现出我目前的一些编程习惯,简单整理我的命名风格:

  1. 文件名采用大写开头的驼峰命名法;
  2. 宏定义采用下划线分隔的全大写,对于保护宏,在前后各加两个下划线;
  3. 对于声明至整个工程的函数,以模块名作为前缀,隔一个下划线后加小写开头驼峰命名法的功能名称;中断回调函数参考SDK的命名,将Handler改为Callback;
  4. 对于全局变量,以模块名作为前缀,隔一个下划线后加大写开头驼峰命名法的功能名称;
  5. 对于局部变量,采用小写开头的驼峰命名法。

SysTick延时测试

  下面来简单测试下我们新写的延时函数的效果,配置一个GPIO输出,让它翻转一次然后延时500ms,不断循环。这里为了让我们编写的代码清晰,建立UserTask.h和UserTask.c文件,分别存放用户任务的声明和实现。

  UserTask.h文件:

#ifndef __USER_TASK_H__
#define __USER_TASK_H__

#include <stdint.h>
#include "ti_msp_dl_config.h"
#include "Tick.h"

void UserTask_init(void);
void UserTask_loop(void);
void UserTask_tick(void);

#endif /* #ifndef __USER_TASK_H__ */

  UserTask.c文件:

#include "UserTask.h"

void UserTask_init(void) {
    
}

void UserTask_loop(void) {
    DL_GPIO_togglePins(GPIO_LED_PORT, GPIO_LED_LED_B_PIN);
    Tick_delay(500);
}

void UserTask_tick(void) {
    
}

  UserTask_init函数用于实现用户任务的初始化,只在程序开头系统初始化后被调用一次,这里目前没有需要初始化的内容;UserTask_loop函数用于实现各项用户任务,在初始化后被循环调用,这里通过SysTick延时实现了每500ms让LED灯的电平翻转一次。下面只要在主程序文件中正确调用这两个函数即可。UserTask_tick函数则是用于用户任务的计时,目前没有需要计时的任务,暂时空置。

  主程序文件:

#include "ti_msp_dl_config.h"
#include "UserTask.h"

int main(void) {
    SYSCFG_DL_init();

    UserTask_init();

    while (1) {
        UserTask_loop();
    }
}

  实际上这样的习惯是从用STM32的HAL库来的,由于CubeMX自动生成的代码已经主程序文件中占用了很多行,所以习惯了把用户代码放在单独的文件中。当然CCS的SysConfig工具生成的代码其实本来就在单独的文件中,所以直接在主程序中写用户的逻辑也问题不大。

  烧录程序,可以看到LED以1s的周期进行闪烁。用示波器测量IO翻转的间隔,可以看到是精准的500ms,说明SysTick成功产生了精准的延时。

图4 SysTick延时效果

图4 SysTick延时效果

按键程序框架

  使用单片机获取按键输入,实现用户的交互逻辑,其实是个有些复杂的事情。首先,通常的机械按键产生的信号不能被简单地认为是一个脉冲信号,由于按键内触点等部件的弹性,在按键按下和弹起的瞬间,会产生多次的触点闭合与断开,当接入电路时,体现在信号波形上就是多次的高频脉冲,称为“抖动”。因此,按键产生的信号首先要进行“消抖”处理,才能进行下一步操作。按键消抖可以在硬件上实现(如加入RC低通滤波器),或者软件上实现。在我设计的“番茄派”开发板上,普通按钮是直接串一个小电阻接地,没有硬件消抖设计,因此需要在软件上完成消抖处理;而旋转编码器则是因为需要使用中断处理,为了便于程序设计,在硬件上加入了消抖滤波器。其次,除了单击,若要想实现长按、双击、组合键等高级操作,也需要在程序逻辑上下一些功夫。因此网上有不少种按键的实现程序,下面我按照自己的想法,提出我的设计。

普通按键读取程序

  下面对于低电平有效,无外接上拉电阻的轻触按键,研究按键读取程序。这样的按键一端连接单片机引脚,另一端接地,在弹起时处于高阻态,按下时处于低电平,因此需要单片机配置GPIO为带上拉电阻的输入,此时在按键弹起时,可读取到高电平,在按键按下时,可读取到低电平。这里配置5个按键的GPIO输入,分别对应上下左右中按键,另外还有1个旋转编码器的按键,注意Digital IOMUX Features中的Internal Resistor配置。

图5 按钮GPIO配置

图5 按钮GPIO配置

  下图展示了我设计的按键程序基本逻辑思路。

图6 按键程序逻辑

图6 按键程序逻辑

  图中所示按键逻辑主要包含原始值读取、消抖倒计时、按下计时和状态判断更新4部分,其中原始值读取和两个计时都放在SysTick中断内实现;状态判断更新则单独实现一个接口函数,在主循环中调用实现。读取按键原始值时,将低有效的按键信号转换为正逻辑输出,便于后续操作,因此图中黑色的按键原始值为低时表示按键未按下,为高时表示按键按下。

  机械按键的抖动一般集中在按下和弹起时刻附近,持续时间较短。为了实现消抖处理,对每个按键引入一个消抖倒计时(屏蔽倒计时)blockTick变量,仅当blockTick为0时可接受按键按下动作,检测到按键按下后,将blockTick置为消抖倒计时值,并由SysTick中断内进行递减,在blockTick减为0之前,将按键动作屏蔽,不再接收按键按下动作。此时已能对按键实现消抖和按下/弹起判断,为了进一步方便实现长按等功能,再引入对每个按键引入按下计时pressTime变量,其在blockTick非0时逐渐递增计时,在blockTick为0时清零。如此一来,通过对当前和上一次pressTime值的判断逻辑,即可实现按键空闲(弹起)、按下瞬间、按下、弹起瞬间和长按达成5种状态的分辨。下面给出对应程序。

  BTN.h文件:

#ifndef __BTN_H__
#define __BTN_H__

#include <stdint.h>
#include "ti_msp_dl_config.h"

// 按键原始值读取
#define BTN_LEFT_RAW  (DL_GPIO_readPins(GPIO_BTN_PORT, GPIO_BTN_BTN_LEFT_PIN) == 0)
#define BTN_DOWN_RAW  (DL_GPIO_readPins(GPIO_BTN_PORT, GPIO_BTN_BTN_DOWN_PIN) == 0)
#define BTN_RIGHT_RAW (DL_GPIO_readPins(GPIO_BTN_PORT, GPIO_BTN_BTN_RIGHT_PIN) == 0)
#define BTN_UP_RAW    (DL_GPIO_readPins(GPIO_BTN_PORT, GPIO_BTN_BTN_UP_PIN) == 0)
#define BTN_MID_RAW   (DL_GPIO_readPins(GPIO_BTN_PORT, GPIO_BTN_BTN_MID_PIN) == 0)
#define ENC_SW_RAW    (DL_GPIO_readPins(GPIO_ENC_ENC_SW_PORT, GPIO_ENC_ENC_SW_PIN) == 0)

#define BTN_LEFT  0 // 左键
#define BTN_DOWN  1 // 下键
#define BTN_RIGHT 2 // 右键
#define BTN_UP    3 // 上键
#define BTN_MID   4 // 中键
#define ENC_SW    5 // 旋转编码器按键

#define BTN_CNT 6 // 按键数量

#define BTN_DEBOUNCE_TIME   100  // 按键消抖时间(ms)
#define BTN_LONG_PRESS_TIME 1000 // 按键长按时间(ms)

// 按键状态
typedef enum BTN_State_t {
    BTN_STATE_IDLE,      // 空闲(弹起)
    BTN_STATE_PRESS,     // 按下瞬间
    BTN_STATE_DOWN,      // 按下
    BTN_STATE_RELEASE,   // 弹起瞬间
    BTN_STATE_LONG_PRESS // 长按达成
} BTN_State_t;

// 按键数据
typedef struct BTN_Data_t {
    uint8_t raw;        // 按键原始值
    uint16_t pressTime; // 按键按下计时(消抖后, ms)
    uint16_t blockTick; // 按键消抖倒计时(ms)
} BTN_Data_t;

/**
 * @brief 获取按键状态
 * @details 判断按键状态并更新state
 * @param state 按键状态枚举数组指针
 */
void BTN_getState(BTN_State_t* state);

void BTN_tick(void);

#endif /* #ifndef __BTN_H__ */

  BTN.c文件:

#include "BTN.h"

BTN_Data_t BTN_Data[BTN_CNT] = {0};

static void read(BTN_Data_t* data);

// 读取按键原始值
static void read(BTN_Data_t* data) {
    data[BTN_LEFT].raw = BTN_LEFT_RAW;
    data[BTN_DOWN].raw = BTN_DOWN_RAW;
    data[BTN_RIGHT].raw = BTN_RIGHT_RAW;
    data[BTN_UP].raw = BTN_UP_RAW;
    data[BTN_MID].raw = BTN_MID_RAW;
    data[ENC_SW].raw = ENC_SW_RAW;
}

/**
 * @brief 获取按键状态
 * @details 判断按键状态并更新state
 * @param state 按键状态枚举数组指针
 */
void BTN_getState(BTN_State_t* state) {
    static uint16_t pressTimeLast[BTN_CNT] = {0};
    uint16_t i;

    for (i = 0; i < BTN_CNT; i++) {
        if (pressTimeLast[i] != BTN_Data[i].pressTime) {
            if (pressTimeLast[i] == 0) {
                state[i] = BTN_STATE_PRESS;
            }
            else if (BTN_Data[i].pressTime == 0) {
                state[i] = BTN_STATE_RELEASE;
            }
            else if (BTN_Data[i].pressTime == BTN_LONG_PRESS_TIME) {
                state[i] = BTN_STATE_LONG_PRESS;
            }
            else if (BTN_Data[i].pressTime) {
                state[i] = BTN_STATE_DOWN;
            }
            else {
                state[i] = BTN_STATE_IDLE;
            }
            pressTimeLast[i] = BTN_Data[i].pressTime;
        }
        else if (BTN_Data[i].pressTime) {
            state[i] = BTN_STATE_DOWN;
        }
        else {
            state[i] = BTN_STATE_IDLE;
        }
    }
}

void BTN_tick(void) {
    uint16_t i;

    read(BTN_Data);
    for (i = 0; i < BTN_CNT; i++) {
        // 消抖与按下计时
        if (!BTN_Data[i].blockTick) {
            if (BTN_Data[i].raw) {
                BTN_Data[i].pressTime++;
                BTN_Data[i].blockTick = BTN_DEBOUNCE_TIME;
            }
            else {
                BTN_Data[i].pressTime = 0;
            }
        }
        else {
            BTN_Data[i].pressTime++;
        }

        // 消抖倒计时
        if (!BTN_Data[i].raw && BTN_Data[i].blockTick) {
            BTN_Data[i].blockTick--;
        }
    }
}

  Tick.h文件:

#ifndef __TICK_H__
#define __TICK_H__

#include <stdint.h>
#include "ti_msp_dl_config.h"
#include "UserTask.h"
#include "BTN.h"

extern volatile uint32_t Tick;

void Tick_delay(uint32_t t);
void Tick_SysTickCallback(void);

#endif /* #ifndef __TICK_H__ */

  Tick.c文件:

#include "Tick.h"

volatile uint32_t Tick = 0;

/**
 * @brief 延时(使用SysTick中断计时)
 * @param t 延时时间(ms)
*/
void Tick_delay(uint32_t t) {
    uint32_t tEnd = Tick + t;
    while (Tick < tEnd);
}

// SysTick中断回调(1ms)
void Tick_SysTickCallback(void) {
    Tick++;
    UserTask_tick();
    BTN_tick();
}

  在按键程序中,使用结构体数组的形式记录所有按键数据,并利用循环处理,这样在需要添加新按键时,只需要修改宏定义和原始值读取部分即可实现。

旋转编码器读取程序

  旋转编码器是个不错的人机交互元件,一般的旋转编码器有3个输出,分别是A相、B相和按钮;其中按钮与普通按钮一样,在上述按键程序中已经加入,A相和B相信号是有相位差的脉冲信号,其脉冲数量与旋转角度(格数)相关,相位关系与旋转方向相关。由于通常需要使用中断等方式判断编码器信号的相位关系,不便于在程序内实现软件消抖,因此在编码器的电路中加入硬件RC低通滤波器,使得其信号变为漂亮干净的脉冲交给单片机。
  对于我在开发板上使用的旋转编码器,每旋转一格,其A和B相信号均产生一个跳变沿(上升或下降沿);并且顺时针旋转时,A相信号超前于B相信号;逆时针旋转时,A相信号滞后于B相信号,如下两图所示。

图7 旋转编码器顺时针旋转信号

图7 旋转编码器顺时针旋转信号

图8 旋转编码器逆时针旋转信号

图8 旋转编码器逆时针旋转信号

  图中分别展示了编码器顺时针和逆时针旋转2格的A和B相信号,可以清晰地看到其相位关系。为了使用单片机判断A和B相信号的相位关系,我们需要准确地在信号边沿判断两信号关系,恰好GPIO中断可以很好地实现这种边沿触发逻辑。具体来说,由于这个编码器每旋转1格,在每相信号都会产生上升沿或下降沿,因此可以将A相信号对应引脚配置为双边沿中断,在中断内判断A相和B相信号关系:若两信号值不同,则为顺时针旋转,计数值递增;若两信号值相同,则为逆时针旋转,计数值递减。对应配置和代码如下所示。

图9 旋转编码器GPIO配置

图9 旋转编码器GPIO配置

  注意Interrupts部分的配置。

  Interrupts.h文件:

#ifndef __INTERRUPTS_H__
#define __INTERRUPTS_H__

#include "ti_msp_dl_config.h"
#include "Tick.h"
#include "Encoder.h"

void SysTick_Handler(void);
void GROUP1_IRQHandler(void);

#endif /* #ifndef __INTERRUPTS_H__ */

  Interrupts.c文件:

#include "Interrupts.h"

// SysTick中断服务函数(1ms)
void SysTick_Handler(void) {
    Tick_SysTickCallback();
}

// GPIO中断服务函数
void GROUP1_IRQHandler(void) {
    if (DL_GPIO_getPendingInterrupt(GPIO_ENC_ENC_A_PORT) == GPIO_ENC_ENC_A_IIDX) {
        ENC_IRQCallback();
    }
}

  在MSPM0单片机中,GPIO给CPU的中断事件来源是GROUP1中断,在它的服务函数内,判断中断来源是编码器引脚中断,再调用相应的编码器中断回调函数。

  Encoder.h文件:

#ifndef __ENCODER_H__
#define __ENCODER_H__

#include "ti_msp_dl_config.h"

// 编码器引脚读取
#define ENC_A_VAL (DL_GPIO_readPins(GPIO_ENC_ENC_A_PORT, GPIO_ENC_ENC_A_PIN) != 0)
#define ENC_B_VAL (DL_GPIO_readPins(GPIO_ENC_ENC_B_PORT, GPIO_ENC_ENC_B_PIN) != 0)

void ENC_init(void);

/**
 * @brief 获取编码器增量
 * @details 获取上次调用至本次调用之间的编码器增量
 * @return 编码器增量(正值:增加; 负值:减少)
 */
int ENC_getInc(void);

void ENC_IRQCallback(void);

#endif /* #ifndef __ENCODER_H__ */

  Encoder.c文件:

#include "Encoder.h"

volatile int ENCCnt = 0;

void ENC_init(void) {
    NVIC_EnableIRQ(GPIO_ENC_INT_IRQN);
}

/**
 * @brief 获取编码器增量
 * @details 获取上次调用至本次调用之间的编码器增量
 * @return 编码器增量(正值:增加; 负值:减少)
 */
int ENC_getInc(void) {
    int cnt = ENCCnt;
    ENCCnt = 0;
    return cnt;
}

// 编码器中断回调
void ENC_IRQCallback(void) {
    if (ENC_A_VAL != ENC_B_VAL) { // 顺时针
        ENCCnt++;
    }
    else { // 逆时针
        ENCCnt--;
    }
}

  在编码器初始化环节,使能其对应的GPIO中断。编码器中断回调函数内,按A和B相信号值的关系判断旋转方向,进行计数值递增或递减。ENC_getInc函数则可在用户任务中调用,获取上次调用至本次调用之间的编码器增量,并将计数值清零进行下次增量统计。

基于时间的任务调度

  有了上述系统计时手段、按键和编码器的获取程序,就可以实现相对复杂一些的交互逻辑了。对于单片机的多任务调度,目前有很多优秀且轻量级的RTOS系统可用,但对于一般较为简单的场景来说,即使是RTOS也显得有些臃肿,因此下面给出我目前裸机编程常用的简易任务调度策略,我称之为基于时间的任务调度:

  将用户任务分为持续任务和定时任务两类,其中持续任务是需要频繁执行或快速响应,且执行时间短的任务;定时任务是需要按特定间隔时间执行,或是执行时间较长且不需要频繁执行的任务。持续任务在主循环中依次调用,并保证其短小迅速即可;定时任务则通过SysTick中断计时,主循环中判断其到达执行时刻后再调用执行。每个用户任务都需要在较短的有限时间内完成执行,从而保证其它任务能够及时响应,因此这是个非抢占式调度策略。对于任务间的通信,使用全局变量实现。这个任务调度策略难以实现像正经操作系统那样的资源高效利用,但它胜在逻辑简单清晰,并且已经能很好地胜任较为简单的单片机应用场景了。

  接下来使用上述任务调度策略,实现这样一个按键与编码器交互功能:中间按键弹起时,亮红灯;按键按下时,亮绿灯;按键单击切换蓝灯亮灭;按键长按切换蓝灯闪烁和暂停;蓝灯的闪烁间隔可由旋转编码器在100~1000ms以10ms步进调节。SysTick、中断、按键和编码器的代码已在前文给出,下面为用户任务代码。

  UserTask.h文件:

#ifndef __USER_TASK_H__
#define __USER_TASK_H__

#include <stdint.h>
#include "ti_msp_dl_config.h"
#include "Tick.h"
#include "BTN.h"
#include "Encoder.h"

// 限幅
#define CONSTRAIN(x, min ,max) ((x) < (min) ? (min) : ((x) > (max) ? (max) : (x)))

void UserTask_init(void);
void UserTask_loop(void);
void UserTask_tick(void);

void Task_BTN(void);
void Task_ENC(void);
void Task_LED(void);

#endif /* #ifndef __USER_TASK_H__ */

  UserTask.c文件:

#include "UserTask.h"

BTN_State_t BTN_State[BTN_CNT] = {0};
uint8_t Blink = 0;

uint16_t LEDTick = 0;  // LED任务计时(ms)
uint16_t LEDTime = 500; // LED任务间隔(ms)

void UserTask_init(void) {
    ENC_init();
}

void UserTask_loop(void) {
    Task_BTN();
    Task_ENC();
    Task_LED();
}

void UserTask_tick(void) {
    LEDTick++;
}

void Task_BTN(void) {
    BTN_getState(BTN_State);
    if (BTN_State[BTN_MID] == BTN_STATE_LONG_PRESS) { // 长按切换蓝灯闪烁/暂停
        Blink = !Blink;
    }
    else if (BTN_State[BTN_MID] == BTN_STATE_PRESS) { // 单击切换蓝灯亮灭
        DL_GPIO_togglePins(GPIO_LED_PORT, GPIO_LED_LED_B_PIN);
    }
    else if (BTN_State[BTN_MID] == BTN_STATE_DOWN) { // 按下亮绿灯
        DL_GPIO_clearPins(GPIO_LED_PORT, GPIO_LED_LED_R_PIN);
        DL_GPIO_setPins(GPIO_LED_PORT, GPIO_LED_LED_G_PIN);
    }
    else if (BTN_State[BTN_MID] == BTN_STATE_IDLE) { // 弹起亮红灯
        DL_GPIO_clearPins(GPIO_LED_PORT, GPIO_LED_LED_G_PIN);
        DL_GPIO_setPins(GPIO_LED_PORT, GPIO_LED_LED_R_PIN);
    }
}

void Task_ENC(void) {
    int inc = ENC_getInc();
    int newLEDTime = (int)LEDTime + inc * 10; // 计算新间隔

    // 限制间隔在100~1000ms之间
    LEDTime = (uint16_t)CONSTRAIN(newLEDTime, 100, 1000);
}

void Task_LED(void) {
    if (LEDTick >= LEDTime) {
        LEDTick = 0;
        if (Blink) {
            DL_GPIO_togglePins(GPIO_LED_PORT, GPIO_LED_LED_B_PIN);
        }
    }
}

  在上述代码中,按照按键、编码器和LED各自的功能,实现了3个用户任务函数,并使用Blink和LEDTime等全局变量在任务间传递是否闪烁和闪烁间隔数据。

视频1 按键与编码器交互功能效果

结语

  这篇文章展示了MSPM0单片机的SysTick配置与用法,以及我对于按键程序框架和任务调度策略的一些思考与实践,最后通过按键与编码器控制LED的交互程序,展示了它们联合实现的效果。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐