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

下面这张图展示了咱们课程的核心知识体系,我把它画成了结构图,方便大家有个整体印象:

课程知识体系结构图 定时器与 SPI 驱动开发实战 定时器模块 SPI 通信模块 基本定时器 通用定时器 SPI 协议基础 SPI 寄存器配置 时基单元配置 PWM / 输入捕获 CPOL / CPHA 配置 中断 / DMA 传输 寄存器配置实战 → 驱动封装 → 应用验证 说明:本课程从底层寄存器配置入手,逐步封装为可复用的驱动层,最终在应用层验证功能 每章均包含实战代码与调试技巧,确保学完即用

1.4 开发环境搭建:Keil MDK 与工程模板

好了,理论说完了,咱们来点实际的。开发环境我用的是 Keil MDK-ARM V5.38,配合 STM32F4 的固件库(Standard Peripheral Library)。为什么不用 CubeMX 和 HAL 库?嗯,这里我要说句实话——HAL 库封装得太厚了,很多寄存器细节被隐藏了。咱们这门课的目的是让你真正理解底层,所以直接从寄存器入手,用标准库做辅助。

搭建步骤其实不复杂,我列个清单:

  1. 安装 Keil MDK:从官网下载安装包,注意勾选 STM32F4 的器件支持包(Device Family Pack)。
  2. 安装固件库:下载 STM32F4xx_DSP_StdPeriph_Lib_V1.8.0,解压到本地。
  3. 创建工程模板:新建一个 Keil 工程,选择 STM32F407VG 芯片,添加必要的启动文件和库文件。
  4. 配置工程选项:设置晶振频率(我一般用 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 为例,配置步骤很简单:

  1. 使能 GPIOD 时钟(RCC_AHB1ENR 寄存器)
  2. 配置 PD12 为输出模式(GPIO_MODER 寄存器)
  3. 设置输出类型为推挽(GPIO_OTYPER 寄存器)
  4. 设置输出速度(GPIO_OSPEEDR 寄存器)
  5. 写 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 的驱动开发了。