2026-07-19 电赛TI板例程10+8学习:从GPIO输入到定时器中断
一、今日目标
- 跑通例程10(keyscan),理解GPIO输入、按键消抖、轮询检测
- 跑通例程8(systick),理解定时器中断、非阻塞延时、软件分频
- 建立"GPIO输出→输入→中断→PWM"的闭环认知
- 记录从"会看代码"到"会改代码"的完整过程
二、环境确认
| 项目 | 状态 | 备注 |
|---|---|---|
| Keil MDK | ✅ 正常 | 已安装TI MSPM0支持包 |
| J-Link驱动 | ✅ 正常 | SWD模式 |
| 核心板连接 | ✅ 正常 | 四线连接 |
| 例程10编译 | ✅ 0 Error | 10_keyscan.uvprojx |
| 例程8编译 | ✅ 0 Error | 8_systick.uvprojx |
硬件:无名创新24年电赛H题小车套件,MSPM0G3507核心板
屏幕:SPI接口OLED(7针),但例程8代码是I2C驱动,暂时跳过显示
三、例程10(keyscan)核心代码分析
3.1 main.c 完整结构
#include"ti_msp_dl_config.h"#defineDELAY(16000000)// 0.5s(当前代码未使用)voiddelay_ms(uint16_tms){while(ms--)delay_cycles(CPUCLK_FREQ/1000);// 32MHz/1000 = 32000周期/ms}intmain(void){SYSCFG_DL_init();// 一键初始化while(1){// S1按键检测(高电平有效)if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)){delay_ms(10);// 消抖if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN))// 再次确认{while(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN));// 等待释放DL_GPIO_togglePins(PORTB_PORT,PORTB_RGB_R_PIN);// 翻转红灯}}// S2按键检测(低电平有效,注意取反!)if(!DL_GPIO_readPins(PORTB_PORT,PORTB_S2_PIN)){delay_ms(10);if(!DL_GPIO_readPins(PORTB_PORT,PORTB_S2_PIN)){while(!DL_GPIO_readPins(PORTB_PORT,PORTB_S2_PIN));DL_GPIO_togglePins(PORTB_PORT,PORTB_RGB_G_PIN);// 翻转绿灯}}delay_ms(100);DL_GPIO_togglePins(GPIO_LEDS_PORT,GPIO_LEDS_USER_TEST_PIN);// 测试灯闪烁}}3.2 代码元素逐行拆解
| 代码元素 | 作用 | 对应STM32标准库 | 关键理解 |
|---|---|---|---|
DL_GPIO_readPins(port, pin) | 读取引脚电平 | GPIO_ReadInputDataBit() | 返回值是掩码,不是0/1! |
DL_GPIO_initDigitalInputFeatures() | 初始化输入+上拉/下拉 | GPIO_Init()配置输入模式 | 需要配置上拉电阻 |
delay_ms(10) | 软件消抖 | 自定义或HAL_Delay | 机械按键抖动5-20ms |
while(readPins(...)) | 等待按键释放 | 无(手动实现) | 防止按键按住时重复触发 |
!DL_GPIO_readPins(...) | 低电平检测 | !GPIO_ReadInputDataBit() | S2硬件是上拉输入 |
3.3 返回值陷阱(核心认知)
// ❌ 错误!返回值不一定是1if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)==1)// ✅ 正确:判断大于0(高电平)if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)>0)// ✅ 正确:判断等于0(低电平)if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)==0)// ✅ 最精确:与掩码比较if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)==KEY_S1_PIN)原因:DL_GPIO_readPins返回的是对应位的掩码值,如0x00200000(第21位为1),而不是简单的1。
3.4 三层检测模型
第一层:if(检测到电平) ← 初步判断 ↓ 第二层:delay_ms(10) ← 消抖等待 if(再次确认电平) ← 确认是真的按下 ↓ 第三层:while(等待释放) ← 等待按键松开 执行动作(翻转LED)四、例程10拓展练习
拓展1:统一按键检测逻辑(失败→成功)
目标:把S1也改成低电平触发,和S2统一风格。
修改:
// 修改前:S1高电平触发,S2低电平触发if(DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN))// S1if(!DL_GPIO_readPins(PORTB_PORT,PORTB_S2_PIN))// S2// 修改后:统一低电平触发if(!DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)){delay_ms(10);if(!DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN)){while(!DL_GPIO_readPins(KEY_PORT,KEY_S1_PIN));DL_GPIO_togglePins(PORTB_PORT,PORTB_RGB_R_PIN);}}结果:S2失效,程序卡在S1的while循环里。
原因分析:
- S1硬件是下拉输入(未按下时低电平),按下后变高电平
- 改成低电平检测后,
!0永远为真,while死循环 - 主循环被卡死,执行不到下面的S2检测
教训:两个按键硬件连接方式不同,代码逻辑本来就该不同。强行统一会导致逻辑错误。
恢复:S1保持高电平检测,S2保持低电平检测。
拓展2:状态机切换LED模式(成功)
目标:短按S1切换LED模式(灭→常亮→闪烁→灭)。
代码:
#defineMODE_OFF0#defineMODE_ON1#defineMODE_BLINK2uint8_tled_mode=MODE_OFF;// 按键处理led_mode=(led_mode+1)%3;// 主循环根据模式执行switch(led_mode){caseMODE_OFF:DL_GPIO_clearPins(PORTB_PORT,PORTB_RGB_R_PIN);break;caseMODE_ON:DL_GPIO_setPins(PORTB_PORT,PORTB_RGB_R_PIN);break;caseMODE_BLINK:DL_GPIO_togglePins(PORTB_PORT,PORTB_RGB_R_PIN);delay_ms(300);// 300ms周期,肉眼可见闪烁break;}遇到的问题:直接复制代码,红灯常亮而不是闪烁。
原因:delay_ms(300)在switch内部,但while循环顶部还有delay_ms(100),循环周期约400ms。blink_cnt需要50次循环才翻转,周期太长。
解决:直接用delay_ms(300)控制闪烁周期,不用计数器。
验证:每按一次S1,红灯切换一种模式(灭→亮→闪烁→灭)。
五、例程8(systick)核心代码分析
5.1 main.c 完整结构
#include"ti_msp_dl_config.h"#include"systick.h"#include"drv_oled.h"#include"stdio.h"#defineDELAY(3200000)// 未使用systimemain_t,timer_t;intmain(void){SYSCFG_DL_init();oled_init();// OLED初始化(SPI屏,代码是I2C,跳过)NVIC_EnableIRQ(TIMER_0_INST_INT_IRQN);// 使能定时器中断DL_TimerG_startCounter(TIMER_0_INST);// 启动定时器while(1){// OLED显示(跳过)delay_ms(200);DL_GPIO_togglePins(USER_GPIO_PORT,USER_GPIO_LED_PA0_PIN);// PA0闪烁get_systime(&main_t);}}voidTIMER_0_INST_IRQHandler(void){switch(DL_TimerG_getPendingInterrupt(TIMER_0_INST)){caseDL_TIMERG_IIDX_ZERO:get_systime(&timer_t);DL_GPIO_togglePins(USER_GPIO_PORT,USER_GPIO_RED_PIN);// 红灯闪烁break;default:break;}}5.2 关键配置(SysConfig截图)
| 参数 | 值 | 说明 |
|---|---|---|
| Timer Clock Prescaler | 100 | 32MHz / 100 = 320kHz |
| Timer Clock Frequency | 320.00 kHz | 定时器实际时钟 |
| Desired Timer Period | 100ms | 中断周期 |
| Actual Timer Period | 100.00 ms | 实际中断周期 |
| Enable Interrupts | Zero event | 计数到零触发中断 |
| Start Timer | 未勾选 | 代码中手动启动 |
5.3 中断 vs 轮询对比
| 维度 | 轮询(例程10按键) | 中断(例程8定时器) |
|---|---|---|
| CPU行为 | 主动查询,忙等 | 被动响应,做其他事 |
| 效率 | 低,CPU空转 | 高,CPU解放 |
| 响应延迟 | 取决于查询间隔 | 硬件级,微秒级 |
| 适用场景 | 简单、低频事件 | 定时、高频事件 |
| 代码复杂度 | 简单 | 需要中断服务函数 |
5.4 遇到的问题:红灯不亮
排查过程:
- PA0闪烁正常 → 程序在跑
- 检查中断函数名 → 正确(TIMER_0_INST_IRQHandler)
- 检查SysConfig → 中断已使能(Zero event)
- 检查引脚定义 → 发现
USER_GPIO_RED_PIN和GPIO_GRP_0_PIN_R_PIN不是同一个引脚!
原因:
USER_GPIO_RED_PIN= GPIOA.26(例程8的独立红灯)GPIO_GRP_0_PIN_R_PIN= GPIOB.26(例程10的RGB红灯)- 硬件上不是同一个LED
解决:把中断函数里的红灯改为GPIO_GRP_0_PIN_R_PIN。
验证:PA0每200ms闪烁(主循环),RGB红灯每100ms闪烁(中断),频率不同,证明中断独立运行。
六、例程8拓展练习
拓展1:改变定时器周期(成功)
操作:修改SysConfig中Desired Timer Period:
| 原值 | 新值 | 效果 |
|---|---|---|
| 100ms | 10ms | 红灯快闪(50Hz) |
| 100ms | 500ms | 红灯慢闪(1Hz) |
| 100ms | 1000ms | 每秒闪一次 |
理解:Period越大,中断越慢,LED闪得越慢。
拓展2:非阻塞延时(失败→成功)
目标:用定时器中断做时间基准,替代delay_ms()的阻塞等待。
第一次尝试(失败):
volatileuint32_ttick_ms=0;// 中断中每100ms+1// 主循环if(tick_ms-last_tick>=200)// 以为是200ms{last_tick=tick_ms;DL_GPIO_togglePins(...);}结果:LED不闪烁。
原因:中断周期是100ms,tick_ms每100ms才+1。tick_ms - last_tick >= 200实际是200 × 100ms =20秒!
解决:统一时间单位,代码中按中断次数计算:
// 中断周期100ms,tick_cnt每100ms+1volatileuint32_ttick_cnt=0;// 每2个中断周期(200ms)翻转if(tick_cnt-last_tick>=2){last_tick=tick_cnt;DL_GPIO_togglePins(...);}验证:PA0每200ms闪烁,主循环中没有delay_ms,CPU一直在运行。
拓展3:多个定时任务(成功)
volatileuint32_ttick_cnt=0;// 任务1:每100ms(1个中断)翻转PA0if(tick_cnt-last_tick_1>=1){last_tick_1=tick_cnt;DL_GPIO_togglePins(USER_GPIO_PORT,USER_GPIO_LED_PA0_PIN);}// 任务2:每500ms(5个中断)翻转红灯if(tick_cnt-last_tick_5>=5){last_tick_5=tick_cnt;DL_GPIO_togglePins(GPIO_GRP_0_PORT,GPIO_GRP_0_PIN_R_PIN);}验证:PA0每100ms闪(快),红灯每500ms闪(慢),互不干扰。
拓展4:按键控制定时器启停(成功)
if(timer_running)DL_TimerG_startCounter(TIMER_0_INST);// 启动elseDL_TimerG_stopCounter(TIMER_0_INST);// 停止验证:上电红灯不闪,按S1开始闪,再按S1停止。
拓展5:按键动态改变周期(失败→成功)
目标:按S1切换红灯闪烁频率(快/中/慢)。
第一次尝试(失败):
DL_TimerG_setPeriod(TIMER_0_INST,3200);// 报错:没有这个函数原因:无名科技提供的库版本没有DL_TimerG_setPeriod。
解决:不用硬件修改,用软件计数实现调速:
uint8_tspeed_mode=1;// 0=快(100ms) 1=中(500ms) 2=慢(1000ms)// 根据速度模式,决定计数目标uint8_ttarget_cnt;switch(speed_mode){case0:target_cnt=1;break;// 100ms = 1个中断case1:target_cnt=5;break;// 500ms = 5个中断case2:target_cnt=10;break;// 1000ms = 10个中断}if(tick_cnt-last_tick>=target_cnt){last_tick=tick_cnt;DL_GPIO_togglePins(GPIO_GRP_0_PORT,GPIO_GRP_0_PIN_R_PIN);}验证:按S1切换红灯闪烁速度(100ms→500ms→1000ms→100ms)。
拓展6:状态机+中断(成功)
#defineSTATE_IDLE0#defineSTATE_RUN1#defineSTATE_FAST2#defineSTATE_SLOW3uint8_tsystem_state=STATE_IDLE;// 按键切换状态system_state=(system_state+1)%4;// 根据状态执行不同动作switch(system_state){caseSTATE_IDLE:/* 所有灯灭 */break;caseSTATE_RUN:/* 红灯500ms闪烁 */break;caseSTATE_FAST:/* 红灯100ms+绿灯500ms */break;caseSTATE_SLOW:/* 红灯1000ms闪烁 */break;}验证:按S1循环切换四种模式。
七、关键陷阱汇总
| 陷阱 | 现象 | 解决 |
|---|---|---|
readPins返回值陷阱 | 判断条件永远为假 | 用>0或==0判断,不要用==1 |
| 按键硬件差异 | 统一逻辑后某个按键失效 | 确认上拉/下拉配置,不要强行统一 |
| 中断函数名错误 | 程序卡死,LED常亮 | 必须和SysConfig生成的向量名一致 |
| 引脚定义混淆 | LED不亮 | USER_GPIO_RED_PIN和GPIO_GRP_0_PIN_R_PIN不是同一个 |
| 时间单位错误 | LED"不闪烁"(实际很慢) | 确认中断周期,tick单位是中断次数不是ms |
setPeriod不存在 | 编译报错 | 用软件计数实现调速,不修改硬件周期 |
| 中断里阻塞 | 程序卡死 | 中断里不能用delay_ms |
共享变量不加volatile | 变量不更新 | 中断和主循环共享的变量必须加volatile |
八、概念连接
8.1 从例程10到例程8的演进
例程1(GPIO输出) 例程10(GPIO输入) 例程8(定时器中断) ↓ ↓ ↓ LED亮灭控制 按键检测+消抖 定时触发+非阻塞 ↓ ↓ ↓ 单一动作 条件判断 时间基准+多任务 ↓ ↓ ↓ 阻塞延时 轮询检测 中断响应8.2 软件分频的理解
硬件定时器:100ms中断(固定不变) ↓ 软件计数: tick_cnt % 1 == 0 → 100ms任务(快) tick_cnt % 5 == 0 → 500ms任务(中) tick_cnt % 10 == 0 → 1000ms任务(慢) ↓ 一个硬件定时器 → 多个不同频率任务这是嵌入式中最常用的"软件分频"方法,比动态修改硬件周期更稳定。
8.3 向PWM衔接
例程8 定时器中断(通知型) ↓ 例程6/7 Timer PWM(自动型) ↓ 区别: - 中断:到点了告诉CPU,CPU自己处理(翻转LED) - PWM:到点了硬件自动改变引脚电平,CPU完全解放(输出方波)九、今日问题清单
| 序号 | 问题描述 | 当前状态 | 优先级 | 下一步行动 |
|---|---|---|---|---|
| 1 | SPI OLED与I2C代码不匹配 | ✅ 已解决 | 高 | 购买I2C OLED或改写SPI驱动 |
| 2 | readPins返回值陷阱 | ✅ 已理解 | 高 | 记住用>0/==0判断 |
| 3 | 按键硬件差异导致统一逻辑失败 | ✅ 已理解 | 中 | 确认上拉/下拉后再写代码 |
| 4 | 引脚定义混淆(USER_GPIO vs GPIO_GRP_0) | ✅ 已解决 | 高 | 查ti_msp_dl_config.h确认 |
| 5 | 时间单位错误(tick单位是中断次数) | ✅ 已理解 | 高 | 确认中断周期后再写代码 |
| 6 | DL_TimerG_setPeriod不存在 | ✅ 已解决 | 中 | 用软件计数替代 |
| 7 | 中断函数里不能阻塞 | ✅ 已理解 | 高 | 中断里只做简单操作 |
| 8 | 共享变量必须加volatile | ✅ 已理解 | 高 | 养成习惯 |
| 9 | 状态机从隐式到显式 | 📝 已实践 | 中 | 继续练习 |
| 10 | Keil调试(Watch窗口) | 🔍 待学习 | 中 | 下次学习 |
十、今日感悟与模式记录
学习节奏
“跑通例程→尝试修改→遇到问题→排查→理解原理→再修改→成功”
这个循环比"一次性看完所有代码"效率高得多。每个陷阱都是一次深度理解的机会。
关键认知更新
- 返回值陷阱是TI的常态:
readPins返回掩码不是0/1,必须养成检查返回值类型的习惯 - 硬件决定软件逻辑:按键的上拉/下拉配置决定了检测逻辑,不能强行统一
- 引脚定义必须确认:
USER_GPIO_RED_PIN和GPIO_GRP_0_PIN_R_PIN不是同一个,查ti_msp_dl_config.h - 时间单位必须统一:中断周期是100ms,
tick单位是中断次数,不是ms - 软件分频比硬件修改更稳定:不用
setPeriod,用计数器实现多频率 - 中断是效率的关键:从轮询到中断,CPU从忙等变成被动响应,效率质变
工具链使用
| 工具 | 今日用途 | 效果 |
|---|---|---|
| Keil | 编译、烧录 | 熟悉,遇到delay_ms重复定义问题 |
| SysConfig | 配置定时器周期、中断 | 理解Prescaler和Period的关系 |
| J-Link | 程序下载 | 顺畅 |
| Kimi | 实时答疑、代码分析、概念连接 | 长期陪伴,理解上下文 |
记录时间:2026-07-19 16:15
下次学习:例程6/7(Timer PWM),让电机转起来