1. 从点亮LED到精准控制为什么我们需要CPU Timer上次我们让TI F28P55/65X开发板上的LED亮了起来那感觉就像第一次让一个机器人睁开了眼睛挺有成就感的。但很快你就会发现只会让灯常亮或常灭这“智能”程度也太低了点。我们做嵌入式开发尤其是工业控制、电机驱动这些领域最核心的能力之一就是“精准地控制时间”。比如你想让一个LED每隔1秒精确地闪烁一次或者让电机每50微秒精确地执行一步操作靠我们手动在while(1)循环里写DELAY_US(1000000)这种忙等待函数行吗实测下来非常不靠谱。为什么因为这种软件延时会被中断打断而且CPU啥也不干就在那里空转效率极低根本谈不上“精准”。这时候就该芯片内部的硬件定时器——CPU Timer闪亮登场了。你可以把它想象成你手机里的多个闹钟。你设好“每隔1小时响一次”然后就可以放心去睡觉或者干别的了到点闹钟自己会响完全不需要你隔一会儿就看一眼时间。CPU Timer就是芯片内部的“硬件闹钟”你配置好定时周期它就在后台独立运行时间一到就通过“中断”的方式大声通知CPU“时间到该干活了” CPU这时才会暂停手头的工作如果有的话跳转去执行你预先写好的中断服务函数比如翻转一下LED的状态执行完立刻又回到原来的任务。这种方式既精准又不占用CPU核心的计算资源。在F28P55/65X这款性能强劲的芯片里有好几个这样的“硬件闹钟”。我们这次实战的主角是CPU Timer2。选择它是因为在常见的工程模板中CPU Timer0和Timer1可能被系统用于实时操作系统如TI-RTOS的时钟节拍而Timer2通常预留给用户自由使用正好适合我们来做实验。通过这个让LED精准闪烁的案例你将亲手打通从硬件配置、中断编写到调试的完整路径这才是嵌入式开发入门的“硬核”一步。2. 工程准备与SysConfig图形化配置万事开头准没错。我强烈建议你为每个新功能实验都新建一个工程保持项目的整洁。这次我们从上次那个成功的LED GPIO控制工程模板开始。2.1 创建新工程与易踩的坑打开CCS找到你之前的工程文件夹直接复制一份重命名为F28P55_CPUTimer_LED之类的名字。这里有个新手特别容易掉进去的坑我当年就栽过别忘了修改.project文件里的名字你用文件管理器找到工程文件夹里面有个隐藏的.project文件如果没看到需要设置显示隐藏文件。用记事本打开它找到name标签把里面的旧工程名改成你刚起的新名字。如果不改当你用CCS的“Import”功能导入时它会发现两个工程名重复很可能导致导入失败或者各种诡异的问题。改完之后再导入心里就踏实多了。导入成功后第一件事就是打开syscfg文件通常工程里都有一个*.syscfg启动SysConfig图形化配置工具。这个工具是TI近年来大力推广的对于新手来说简直是福音它把芯片复杂的寄存器配置变成了直观的图形界面和选项。2.2 找到并启用CPU Timer2在SysConfig的左侧设备导航树里展开你的芯片型号比如F28P55x你会看到Timers分类下面就有CPU Timer。点开它通常能看到CPU Timer 0/1/2。我们点击CPU Timer 2。右侧的配置面板会出现几个关键参数这里我们主要关注两个Enable This CPU Timer毫无疑问先勾选上启用它。Period (us)这是定时周期单位是微秒。这就是你给“闹钟”设定的响铃间隔。我们先填个1000000也就是1秒。让LED1秒闪一次效果比较明显。其他参数比如分频器Prescaler、重载模式等SysConfig通常已经根据最佳实践设置了默认值。对于这个实验我们暂时用默认值即可。这就好比闹钟你先设定好1小时响一次至于它是用石英振动还是原子钟来计时初始阶段我们不用深究。配置完成后记得点击右上角的Save或Apply按钮。这时SysConfig会自动在后台为我们生成代码。它做了哪些事呢在board.c文件中生成了Board_init()函数里关于初始化CPU Timer2的代码。在board.h文件中声明了myCPUTIMER2_BASE这个定时器基地址常量以及对应的中断服务函数ISR原型。2.3 验证配置是否成功怎么知道SysConfig真的配好了呢最直接的方法就是去检查board.h文件。用CCS打开它搜索CPUTIMER2或者myCPUTIMER2。你应该能看到类似这样的代码extern void INT_myCPUTIMER2_ISR(void); #define myCPUTIMER2_BASE (CPUTIMER2_BASE) #define myCPUTIMER2_INT (INT_CPUTIMER2)看到这几行就说明SysConfig已经成功为CPU Timer2生成了必要的“身份证”基地址和“电话号码”中断函数名。这一步的图形化操作替代了以前需要手动查阅上千页技术手册、查找寄存器地址的繁琐过程大大降低了入门门槛。3. 编写定时器驱动与中断服务函数图形化配置搭好了舞台接下来就该我们自己的代码登场了。SysConfig帮我们做了硬件层面的注册和关联但定时器怎么启动、周期怎么精确计算、中断发生后具体要执行什么任务这些逻辑还需要我们自己编写。3.1 创建系统功能模块sys.c/sys.h一个好的编程习惯是把不同功能的代码模块化。我们创建一个专门管理定时器等系统功能的模块。在工程App目录下的inc文件夹里新建一个sys.h头文件在src文件夹里新建一个sys.c源文件。在sys.h里我们引入必要的库文件并声明函数#ifndef APP_INC_SYS_H_ #define APP_INC_SYS_H_ #include device.h #include driverlib.h void configCPUTimer(uint32_t cpuTimerBase, float sysClkFreq, float periodUs); #endif /* APP_INC_SYS_H_ */重点在sys.c里实现configCPUTimer函数。这个函数是配置定时器的核心我把它掰开揉碎了讲#include sys.h void configCPUTimer(uint32_t cpuTimerBase, float sysClkFreq, float periodUs) { uint32_t periodCount; // 1. 计算定时器周期寄存器值这是最关键的公式 periodCount (uint32_t)((sysClkFreq / 1000000.0f) * periodUs); CPUTimer_setPeriod(cpuTimerBase, periodCount - 1); // 2. 设置预分频器这里设为1分频即不分频 CPUTimer_setPreScaler(cpuTimerBase, 0); // 3. 停止定时器进行初始设置 CPUTimer_stopTimer(cpuTimerBase); CPUTimer_reloadTimerCounter(cpuTimerBase); // 重载计数器 // 4. 设置仿真模式调试时遇到断点定时器如何行为 CPUTimer_setEmulationMode(cpuTimerBase, CPUTIMER_EMULATIONMODE_STOPAFTERNEXTDECREMENT); // 5. 使能定时器中断允许定时器到时产生中断信号 CPUTimer_enableInterrupt(cpuTimerBase); // 6. 启动定时器 CPUTimer_startTimer(cpuTimerBase); }核心公式解读periodCount (sysClkFreq / 1000000) * periodUs。sysClkFreq是你的系统时钟频率比如F28P55默认是150MHz即150,000,000 Hz。除以1000000是把单位从Hz换算成MHz。150,000,000 Hz / 1,000,000 150 MHz。这样公式前半部分得到了“每微秒有多少个时钟周期”。乘以periodUs周期微秒数比如1秒就是1,000,000 us。计算150 * 1,000,000 150,000,000。这个结果150,000,000就是定时器计数器需要计数的次数。但是定时器寄存器是从N减到0触发中断所以实际写入寄存器的值是N-1即150,000,000 - 1。这就是CPUTimer_setPeriod(cpuTimerBase, periodCount - 1);的由来。3.2 编写中断服务函数interruptISR.c定时器时间一到就会触发中断CPU会跳转到中断服务函数(ISR)执行。我们在App/src下新建一个interruptISR.c文件。#include device.h #include driverlib.h #include board.h // 定义一个全局变量用于LED状态翻转 uint16_t ledToggle 0; __interrupt void INT_myCPUTIMER2_ISR(void) { // 1. 清除定时器2的中断标志位非常重要告诉硬件中断已处理 CPUTimer_clearInterruptStatus(myCPUTIMER2_BASE); // 2. 执行你的任务翻转LED状态 // 假设LED4和LED5在板上且一个亮时另一个灭 GPIO_writePin(LED4, ledToggle); GPIO_writePin(LED5, 1 - ledToggle); // 3. 更新状态变量为下一次翻转准备 ledToggle 1 - ledToggle; // 4. 确认已处理PIE组内中断针对C2000系列的中断控制器 Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP1); }这里有几个生死攸关的细节函数名必须匹配INT_myCPUTIMER2_ISR这个名字必须和board.h里声明的一模一样。否则中断向量表找不到这个函数中断就无法正确响应。清除中断标志CPUTimer_clearInterruptStatus这行代码是必须的。它的作用是告诉定时器硬件“你的中断信号我收到了已经处理完毕。” 如果不清除硬件会认为中断一直未被处理导致中断只触发一次后就再也不触发了。这是新手最常见的“中断只进一次”问题的根源。操作要快ISR里的代码应该尽可能短小精悍执行时间要远小于定时器周期。不要在这里做复杂的计算或调用可能阻塞的函数。我们的任务只是翻转一下GPIO非常快。4. 主程序搭建与实验验证所有零件都准备好了现在在主函数里把它们组装起来并上电测试。4.1 main.c 的完整编排打开main.c代码结构非常清晰#include device.h #include driverlib.h #include board.h #include sys.h // 我们自己的系统模块头文件 void main(void) { // 第一阶段芯片底层初始化标准三板斧 Device_init(); // 初始化设备时钟、锁相环等 Device_initGPIO(); // 初始化GPIO模块禁用引脚锁 Interrupt_initModule(); // 初始化中断控制器(PIE) Interrupt_initVectorTable(); // 初始化中断向量表 // 第二阶段板级外设初始化SysConfig生成的代码在这里生效 Board_init(); // 初始化引脚复用、外设包括我们的CPU Timer2 // 第三阶段应用层配置 // 配置CPU Timer2系统时钟150MHz定时周期1秒 (1,000,000 us) configCPUTimer(myCPUTIMER2_BASE, DEVICE_SYSCLK_FREQ, 1000000.0f); // 第四阶段使能全局中断让定时器中断可以打断CPU EINT; // 使能全局中断(INTM) ERTM; // 使能实时调试中断(DBGM调试时有用) // 第五阶段主循环这里什么都不用做一切交给中断 while(1) { // 你可以在这里让CPU执行其他低优先级任务比如扫描按键 // 定时器中断会随时打断这里执行完ISR后又会自动回到这里 } }这段代码体现了典型的前后台系统架构。while(1)循环是“后台”处理一些非紧急的、周期不固定的任务。而定时器中断是“前台”专门处理对时间要求精准的紧急任务。两者通过中断机制协同工作。4.2 编译、烧录与现象观察点击CCS的编译按钮确保零错误零警告。然后连接开发板将程序下载进去。上电运行后你应该能看到开发板上的两个LED比如LED4和LED5开始非常稳定地交替闪烁亮灭周期正好是1秒。你可以用手表或手机秒表计时观察一分钟看看有没有肉眼可见的偏差。对于150MHz的时钟源和硬件定时器来说1秒定时的误差是微乎其微的。4.3 玩转定时周期深入理解参数现在我们来验证一下我们对那个核心公式的理解。回到main.c里找到调用configCPUTimer的那一行。把第三个参数从10000001秒改成**5000000.5秒**。重新编译烧录你会发现LED闪烁速度明显加快变成每秒闪烁两次。再改成**20000002秒**LED的闪烁就会变得慢悠悠的。你可以多试几个值比如1000000.1秒10Hz闪烁这时人眼已经能看到闪烁但LED会看起来像是在持续亮但有点“抖”。如果设置到500000.05秒20Hz由于视觉暂留LED看起来就像是常亮的了这就是PWM调光的原理基础。思考题如果你想实现一个精确的500毫秒0.5秒定时但系统时钟频率DEVICE_SYSCLK_FREQ是100MHz那么传入configCPUTimer的periodUs参数应该填多少计算一下周期寄存器值 (100MHz / 1MHz) * 500us 100 * 500 50000。所以periodUs填500000吗不对注意periodUs的单位是微秒500毫秒就是500,000微秒。所以应该填500000.0f。公式会计算出(100) * 500000 50,000,000减去1后写入寄存器。定时器计数5000万个时钟周期在100MHz下每个周期10纳秒总时间正好是0.5秒。5. 调试技巧与常见问题排查实验成功了固然开心但调试过程才是真正长本事的时候。这里分享几个我踩过坑后总结的调试经验。5.1 中断根本没触发检查清单如果LED一动不动首先检查几个关键点全局中断使能了吗主函数里EINT;语句执行了吗没有它所有中断都被屏蔽。中断函数名拼写对吗务必核对interruptISR.c里的函数名和board.h里声明的完全一致包括大小写。中断标志清除了吗确保ISR里第一行就是CPUTimer_clearInterruptStatus。PIE中断ACK清除了吗对于C2000ISR末尾的Interrupt_clearACKGroup也很重要。定时器真的启动了吗在configCPUTimer函数里CPUTimer_startTimer调用了吗周期值计算对吗如果周期值计算得非常大超过了32位定时器能容纳的最大值定时器可能永远不会溢出中断。如果计算值太小比如几个微秒中断触发得太频繁可能导致系统大部分时间都在处理中断看起来像卡死。先用一个较大的值如1秒测试。5.2 使用CCS调试器观察CCS的调试功能非常强大。你可以在INT_myCPUTIMER2_ISR函数入口处打一个断点。然后全速运行程序如果程序能停在断点处说明中断成功触发了。接着你可以单步执行观察LED引脚电平是否变化。查看寄存器在Expressions或Registers视图里添加CPUTIMER2.TIM计数器寄存器和CPUTIMER2.PRD周期寄存器观察它们的值是否在变化。查看变量观察ledToggle这个全局变量在中断里是否被正确翻转。5.3 中断只进一次这是最典型的问题99%的原因是忘记在ISR中清除中断标志位。硬件定时器中断触发后会有一个状态标志位置位。CPU响应中断并跳转到你的ISR后你必须用CPUTimer_clearInterruptStatus函数手动把这个标志位清零。如果不清零中断控制器会认为这个中断源一直处于“请求服务”状态在本次中断返回后就不会再响应它的下一次中断请求了。所以请务必把清除标志位作为ISR的第一条或第二条指令。5.4 闪烁频率不准如果感觉LED闪烁忽快忽慢或者和预期周期对不上检查系统时钟配置DEVICE_SYSCLK_FREQ这个常量定义的值对吗它是在device.h或相关时钟配置文件中定义的确保它和你实际的系统时钟频率一致。默认工程通常是150MHz。检查分频器设置我们的configCPUTimer函数里把预分频器Prescaler设为了0即1分频。如果你不小心改动了这里或者SysConfig有不同默认值定时器的计数时钟就会被分频导致实际周期变长。公式计算溢出确保(sysClkFreq / 1000000) * periodUs这个计算的结果在uint32_t类型能表示的范围内0到约42.9亿。对于150MHz时钟和1秒周期结果是1.5亿完全没问题。掌握了CPU Timer定时中断你就为F28P55/65X开发板赋予了“时间感知”的能力。这不仅仅是让LED闪烁更是你后续实现软件PWM、按键消抖、数据定时采样、通信超时控制等所有与时间相关功能的基础。试着改动周期参数感受一下硬件定时器的精准魅力再想想这个功能可以用在你的什么项目创意中动手实践才是最好的老师。