1. 课程导论与开发环境搭建
各位同学好,我是你们这趟嵌入式驱动开发之旅的向导。说实话,每次开新课前我都在想,怎么把那些枯燥的寄存器配置讲得有意思点。今天咱们要聊的定时器和 SPI,可以说是嵌入式世界里最基础也最核心的两个模块。你想想看,没有定时器,你的系统怎么知道时间过去了多久?没有 SPI,你的主控芯片怎么跟外设高速交换数据?
我个人习惯,在动手写代码之前,一定先把“为什么”搞清楚。所以这一章,我们先搭好台子,把硬件平台和开发环境整明白。后面再撸起袖子干寄存器,心里才有底。
本章核心目标:理解定时器与 SPI 在嵌入式系统中的作用,熟悉 STM32F4 硬件平台,完成 Keil MDK 开发环境搭建与工程模板创建。
1.1 定时器概述:不只是“数数”那么简单
定时器,说白了就是一个会数数的计数器。它内部有一个寄存器,每来一个时钟脉冲就加 1。当这个值达到我们设定的上限时,就会产生一个事件——可能是触发中断,也可能是翻转一个 GPIO 引脚。
但你别小看这个“数数”的功能。我在项目中遇到过这样一个场景:一个电机控制板需要输出精确的 PWM 波形来控制伺服电机。如果只用软件延时来做,CPU 会被完全占死,而且精度根本达不到要求。这时候,定时器的 PWM 模式就派上用场了——硬件自动翻转电平,CPU 可以腾出手来处理其他任务。
STM32F4 的定时器资源非常丰富,我整理了一张表,方便大家对照:
| 定时器类型 | 位宽 | 主要特性 | 典型应用 |
|---|---|---|---|
| 基本定时器 (TIM6/TIM7) | 16 位 | 仅时基功能,无外部引脚 | 产生周期性中断、DAC 触发 |
| 通用定时器 (TIM2~TIM5) | 16/32 位 | PWM、输入捕获、输出比较 | 电机控制、信号测量、LED 调光 |
| 高级定时器 (TIM1/TIM8) | 16 位 | 互补输出、死区插入、刹车功能 | 三相电机驱动、开关电源 |
我的小建议:初学阶段,先把通用定时器玩透。TIM2 和 TIM3 是 32 位的,计数范围更大,适合做长时间延时。TIM4 和 TIM5 是 16 位的,但配置起来更简单。我个人习惯先用 TIM3 做实验,因为它的中断优先级设置比较灵活。
1.2 SPI 协议概述:高速串行通信的“老司机”
SPI,全称 Serial Peripheral Interface,是一种同步、全双工的串行通信协议。它由 Motorola 公司提出,现在已经成为嵌入式领域的事实标准。
为什么说它是“老司机”?因为它速度快——通常可以跑到几十 MHz,而且硬件实现简单。我做过一个项目,需要用 STM32F4 读取一个高速 ADC 芯片的数据,采样率要求 1Msps。如果用 I2C,总线速度只有 400kHz,根本跑不动。换成 SPI,时钟直接配到 21MHz,数据哗哗地就进来了。
SPI 协议有四根线:
- SCLK:串行时钟,由主机产生
- MOSI:主机输出,从机输入
- MISO:主机输入,从机输出
- CS:片选信号,低电平有效
这里有个容易踩的坑——SPI 有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定。我曾经在调试一个 LCD 屏幕时,怎么刷都不显示,折腾了半天才发现是 SPI 模式配错了。从机要求的是模式 0(CPOL=0, CPHA=0),我配成了模式 3(CPOL=1, CPHA=1),时序完全对不上。
| SPI 模式 | CPOL | CPHA | 时钟空闲电平 | 数据采样边沿 |
|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低电平 | 第一个跳变沿(上升沿) |
| 模式 1 | 0 | 1 | 低电平 | 第二个跳变沿(下降沿) |
| 模式 2 | 1 | 0 | 高电平 | 第一个跳变沿(下降沿) |
| 模式 3 | 1 | 1 | 高电平 | 第二个跳变沿(上升沿) |
避坑指南:拿到一个 SPI 从器件的数据手册,第一件事就是查它的 SPI 模式。我曾经因为没仔细看手册,把 CPHA 配反了,结果读回来的数据全是乱码。后来我养成了一个习惯——在代码注释里写明从机要求的模式,方便以后排查。
1.3 硬件平台介绍:STM32F4 系列
咱们这门课选用的硬件平台是 STM32F4 系列。为什么选它?因为它是 ARM Cortex-M4 内核,带 FPU(浮点运算单元),主频最高 168MHz,性能足够强悍。而且它的定时器和 SPI 外设设计得非常典型,学会了 F4 系列,其他 STM32 系列基本可以触类旁通。
我手头这块板子是 STM32F407VGT6,资源如下:
- 内核:ARM Cortex-M4F,带单精度 FPU
- 主频:168MHz
- Flash:1MB
- SRAM:192KB
- 定时器:14 个(包括 2 个高级、8 个通用、2 个基本、2 个看门狗)
- SPI:3 个(SPI1~SPI3),最高速率 42MHz
下面这张图展示了咱们课程的核心知识体系,我把它画成了结构图,方便大家有个整体印象:
1.4 开发环境搭建:Keil MDK 与工程模板
好了,理论说完了,咱们来点实际的。开发环境我用的是 Keil MDK-ARM V5.38,配合 STM32F4 的固件库(Standard Peripheral Library)。为什么不用 CubeMX 和 HAL 库?嗯,这里我要说句实话——HAL 库封装得太厚了,很多寄存器细节被隐藏了。咱们这门课的目的是让你真正理解底层,所以直接从寄存器入手,用标准库做辅助。
搭建步骤其实不复杂,我列个清单:
- 安装 Keil MDK:从官网下载安装包,注意勾选 STM32F4 的器件支持包(Device Family Pack)。
- 安装固件库:下载 STM32F4xx_DSP_StdPeriph_Lib_V1.8.0,解压到本地。
- 创建工程模板:新建一个 Keil 工程,选择 STM32F407VG 芯片,添加必要的启动文件和库文件。
- 配置工程选项:设置晶振频率(我一般用 8MHz 外部晶振)、优化等级(调试阶段选 -O0)、生成 HEX 文件。
下面是我常用的工程模板目录结构,你可以直接照着建:
Project_Template/
├── User/ # 用户代码
│ ├── main.c
│ ├── stm32f4xx_it.c # 中断服务函数
│ └── stm32f4xx_conf.h # 外设配置文件
├── FWLib/ # 标准外设库
│ ├── src/ # .c 源文件
│ └── inc/ # .h 头文件
├── CMSIS/ # 内核相关文件
│ ├── core_cm4.h
│ ├── startup_stm32f40xx.s
│ └── system_stm32f4xx.c
├── Output/ # 编译输出目录
└── Project.uvprojx # Keil 工程文件
我的小技巧:在 stm32f4xx_conf.h 中,我习惯把暂时用不到的外设宏注释掉,这样编译速度会快很多。比如刚开始只学定时器,就把 SPI、I2C、USART 的宏都关掉。等用到的时候再打开,省得每次编译都要等半天。
1.5 第一个工程:点亮 LED 验证环境
环境搭好了,咱们得验证一下能不能正常工作。我习惯用点亮一个 LED 来“拜码头”——这是嵌入式开发的传统,你想想看,如果连灯都点不亮,后面写再多代码也是白搭。
STM32F4 开发板上通常有一个板载 LED,接在 PD12、PD13、PD14 或 PD15 上(不同板子可能不一样,建议查一下原理图)。咱们以 PD12 为例,配置步骤很简单:
- 使能 GPIOD 时钟(RCC_AHB1ENR 寄存器)
- 配置 PD12 为输出模式(GPIO_MODER 寄存器)
- 设置输出类型为推挽(GPIO_OTYPER 寄存器)
- 设置输出速度(GPIO_OSPEEDR 寄存器)
- 写 GPIO_BSRR 寄存器,让 PD12 输出高电平
代码长这样:
#include "stm32f4xx.h"
void LED_Init(void)
{
// 1. 使能 GPIOD 时钟
RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
// 2. 配置 PD12 为输出模式 (MODER[25:24] = 01)
GPIOE->MODER &= ~GPIO_MODER_MODER12_1;
GPIOE->MODER |= GPIO_MODER_MODER12_0;
// 3. 推挽输出 (默认就是推挽,这一步可以省略)
GPIOE->OTYPER &= ~GPIO_OTYPER_OT_12;
// 4. 输出速度设为 50MHz
GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR12;
}
void LED_On(void)
{
// 写 BSRR 寄存器的低 16 位,置位 PD12
GPIOE->BSRR = GPIO_BSRR_BS_12;
}
void LED_Off(void)
{
// 写 BSRR 寄存器的高 16 位,复位 PD12
GPIOE->BSRR = GPIO_BSRR_BR_12;
}
int main(void)
{
LED_Init();
while(1)
{
LED_On();
// 简单延时,后面会用定时器替代
for(volatile uint32_t i = 0; i < 500000; i++);
LED_Off();
for(volatile uint32_t i = 0; i < 500000; i++);
}
}
避坑指南:写寄存器的时候,一定要注意“读-改-写”的顺序。比如配置 MODER 寄存器,如果直接赋值 GPIOE->MODER = 0x01000000,会把其他引脚的模式也改掉。正确的做法是先清零目标位,再置位。我刚开始学的时候吃过这个亏,一个不小心把 JTAG 引脚的模式改了,调试器直接连不上了。
编译下载后,如果看到 LED 在闪烁,恭喜你——开发环境搭建成功了!后面咱们就可以正式开始定时器和 SPI 的驱动开发了。