STM32F103外设全景解析:从GPIO到DMA的嵌入式开发核心

📅 2026/7/31 7:17:58 👁️ 阅读次数 📝 编程学习
STM32F103外设全景解析:从GPIO到DMA的嵌入式开发核心

1. 从“最小系统”到“外设宇宙”:为什么F103依然是入门首选

如果你刚开始接触STM32,或者从51单片机转过来,面对琳琅满目的型号,大概率会有人推荐你从STM32F103系列开始。而在这个系列里,64K和128K Flash的型号(比如F103C8T6和F103CBT6)更是被戏称为“入门神车”。原因很简单:它们提供了一个近乎完美的平衡点——性能足够学习绝大多数外设,成本又足够低廉,社区资源(教程、代码、问题解答)丰富到几乎你遇到的任何坑,都有人踩过并填平了。

但很多新手拿到开发板,跑完LED闪烁和串口打印“Hello World”后,就容易陷入迷茫:除了GPIO点灯和UART打印,这芯片里到底还有什么?那些数据手册里密密麻麻的框图、英文缩写的外设名,到底能用来做什么?这篇内容,我就想抛开那些复杂的初始化代码,先带你从顶层俯瞰一下STM32F103这颗芯片的“外设宇宙”。理解了这个全景图,你再去看具体某个外设的教程,就会知道它在这个系统里扮演什么角色,为什么要这么配置,学习起来事半功倍。

简单来说,STM32F103的内核(Cortex-M3)是大脑,负责执行指令和运算。而外设,就是大脑指挥下的“五官”和“四肢”。GPIO是最简单的手指,可以输出高低电平(点灯)或感知外部电压(读按键)。但要想让单片机真正“活”起来,与世界交互,就必须依赖更多功能各异的外设。F103的外设库非常经典,几乎涵盖了嵌入式开发中80%的常用功能模块,理解了它们,就掌握了STM32开发的骨架。

2. 核心通信桥梁:三大片上总线与时钟树

在深入每个外设之前,必须得先搞明白它们是怎么和CPU核心“说上话”的。这不是枯燥的理论,它直接决定了你写代码时,配置外设的步骤和可能遇到的时序问题。

2.1 总线架构:数据的高速公路网

STM32F103内部有三条主要的总线,你可以把它们想象成不同等级和用途的公路:

  1. D-Bus (数据总线) 和 I-Bus (指令总线):这是连接内核和Flash存储器的“专用高速路”。内核通过I-Bus取指令,通过D-Bus读写数据。你写的程序代码就存放在Flash里,内核通过这两条路快速获取并执行。对于64K/128K Flash的型号,这条通路至关重要。
  2. System Bus (系统总线):这条总线连接内核、SRAM(运行内存)和主要的高速外设,比如DMA控制器。当你的程序需要操作大量数据(比如数组处理、串口数据搬运)时,就会用到这条总线。
  3. APB1 和 APB2 (高级外设总线):这是外设们接入系统的“主干道”。APB2是高速总线,挂载着一些对速度要求高的外设,比如GPIOA-G、ADC1、高级定时器TIM1、系统配置时钟等。APB1是低速总线,最高频率36MHz(在系统时钟为72MHz时),上面挂着USART2/3、I2C1/2、SPI2、基本定时器TIM2-TIM4等。

注意:这个总线划分直接影响了外设的时钟使能位置。在标准库中,你用RCC_APB2PeriphClockCmd()来开启GPIO、ADC1的时钟,而用RCC_APB1PeriphClockCmd()来开启USART2、I2C1的时钟。如果开错了,外设就无法工作,这是新手常踩的坑。

2.2 时钟树:外设的脉搏

如果说总线是公路,那时钟就是公路上车辆行驶的节拍器。每个外设都必须有时钟驱动才能工作。STM32的时钟树有点复杂,但入门阶段你只需要抓住几个关键点:

  • HSI/HSE:内部/外部高速时钟源。通常我们用外部8MHz晶振(HSE)来获得更稳定的时钟。
  • PLL:锁相环倍频器。它可以把HSE倍频到72MHz,作为系统主时钟(SYSCLK)。这也是F103的极限运行频率。
  • AHB、APB1、APB2预分频器:系统时钟SYSCLK经过分频,产生给不同总线的时钟。APB1最大36MHz,APB2最大72MHz的限制就是在这里设置的。
  • 外设时钟门控:每个外设在对应的APB总线上还有一个“开关”(时钟使能位)。即使总线有时钟,这个开关没打开,外设也没“上电”。这就是为什么初始化外设第一步永远是RCC_xxxPeriphClockCmd(ENABLE)

我个人的体会是,初期不必深究时钟树的每一个分频路径,但一定要理解“外设工作需要时钟 -> 时钟来源于某个总线 -> 总线时钟来源于系统时钟并可能被分频 -> 使用前必须使能该外设的时钟”这条链路。很多诡异的问题,比如定时器计时不准、串口乱码,追根溯源都是时钟配置不对。

3. 数字世界的基础:GPIO与中断控制器(NVIC)

这是所有交互的起点。

3.1 GPIO:不仅仅是“开”和“关”

通用输入输出口,这是你接触的第一个外设。但它的功能比想象中强大:

  • 输入模式:除了检测高低电平,还有上拉/下拉输入。这非常实用,比如接一个按钮,另一端接地,就可以配置为上拉输入。这样按钮没按下时,IO口被内部电阻拉到高电平;按下时,变为低电平,省去了外部上拉电阻。
  • 输出模式:推挽输出能强有力地驱动LED;开漏输出常用于I2C总线,实现“线与”功能。
  • 复用功能:这是GPIO的“高级形态”。当你想使用串口、SPI等外设时,需要把某些GPIO配置为“复用推挽输出”或“复用开漏输出”。此时,GPIO的输出电平不再受你写的GPIO_WriteBit控制,而是由内部的外设(如USART)自动控制。新手常犯的错误就是,用了外设却忘了把GPIO模式设置为复用模式,导致数据无法发出或接收。
  • 重映射功能:有些外设的引脚是固定的,但有些可以“重映射”到其他引脚上。这在PCB布线引脚紧张时非常有用。需要查阅芯片的数据手册“Alternate function remapping”章节。

3.2 NVIC:中断的调度中心

“中断”是单片机实现实时响应的核心机制。NVIC就是管理所有中断的“交通警察”。

  • 可嵌套、可抢占:高优先级中断可以打断低优先级中断,这保证了关键任务(比如电机失控保护)能得到即时响应。
  • 优先级分组:STM32的中断优先级分为“抢占优先级”和“子优先级”。通过NVIC_PriorityGroupConfig()设置分组,决定了多少位用于抢占。例如,分组2(2位抢占,2位子优先级),意味着有4个可抢占的优先级层次。建议一个项目里固定一种分组方式,不要混用,否则优先级判断会混乱。
  • 与EXTI联动:外部中断/事件控制器(EXTI)是GPIO和NVIC之间的桥梁。它负责监测GPIO引脚上的边沿(上升沿、下降沿),并产生中断请求给NVIC。配置外部中断的步骤通常是:1. 初始化GPIO为输入;2. 开启AFIO时钟(因为EXTI映射功能属于AFIO);3. 用GPIO_EXTILineConfig()将GPIO引脚连接到EXTI线;4. 配置EXTI线的触发模式;5. 配置NVIC。

一个实操心得:对于按键等机械开关,因为存在抖动,直接用在中断里做状态判断可能会误触发。常见的做法是在EXTI中断里只设置一个标志位,然后在主循环或低优先级任务里进行延时消抖和状态处理。或者,更高级一点,利用定时器配合GPIO读操作来实现硬件消抖。

4. 让程序“感知”时间:定时器(TIM)全景

定时器是STM32外设中最复杂、最强大的一类。F103的定时器种类繁多,各有侧重。

定时器类型主要特点典型应用场景
基本定时器 (TIM6, TIM7)最简单,只有向上计数,纯计时用途。产生精确的时基,用于软件延时、触发DAC转换等。
通用定时器 (TIM2, TIM3, TIM4, TIM5)功能全面,支持向上/向下/中央对齐计数,有4个独立通道。PWM输出(驱动电机、舵机、调光),输入捕获(测量脉冲宽度、频率),输出比较(定时触发事件)。
高级定时器 (TIM1, TIM8)在通用定时器基础上,增加了互补输出、死区插入、紧急刹车等功能。主要用于电机控制、开关电源等复杂的电力电子领域。

4.1 核心概念:时钟源、预分频器、自动重载寄存器

理解定时器,关键是弄懂这三个寄存器如何协同工作:

  1. 时钟源:定时器挂载在APB1或APB2上。这里有个关键点:如果APB预分频系数是1,则定时器时钟等于APB时钟;否则,定时器时钟是APB时钟的2倍。例如,系统时钟72MHz,APB1预分频系数为2(得36MHz),那么挂载在APB1上的TIM2的时钟不是36MHz,而是72MHz!这个细节很多资料会忽略,导致计算定时时间出错。
  2. 预分频器 (PSC):它是一个16位寄存器,可以对定时器时钟进行分频。实际驱动计数器累加的时钟频率 = 定时器时钟 / (PSC + 1)。
  3. 自动重载寄存器 (ARR):计数器从0开始累加,累加到ARR值(或从ARR值向下减到0)时,产生一次“更新事件”,计数器重置,并可以触发更新中断。

定时时间的计算定时时间 = (ARR + 1) * (PSC + 1) / 定时器时钟频率。例如,定时器时钟72MHz,要产生1ms中断,设PSC=7199,则分频后时钟为72MHz / 7200 = 10kHz。ARR设为9,则定时周期为 (9+1) / 10kHz = 1ms。

4.2 PWM生成:输出比较模式的精髓

PWM是调光、调速的灵魂。其原理是利用定时器的输出比较模式

  • 定时器在ARR范围内循环计数。
  • 你设置一个捕获/比较寄存器 (CCR)
  • 当计数器值小于CCR时,输出一种电平(通常为高);大于CCR时,输出另一种电平(低)。
  • 这样,CCR/ARR 的比值就决定了高电平占整个周期的比例,即占空比

在标准库中,配置PWM的步骤是:初始化定时器时基(ARR, PSC),初始化输出比较通道(模式设为PWM,极性等),使能通道输出,最后使能定时器。一个易错点:修改PWM频率(通过改ARR)或占空比(通过改CCR)时,如果是在运行中动态修改,有时需要先失能定时器或通道,改完再使能,否则可能导致当前周期输出异常。更稳妥的做法是使用定时器提供的“预装载”功能,设置新值后,等本次更新事件生效。

4.3 输入捕获:测量时间间隔的利器

输入捕获常用于测频率、脉宽。其原理是:当检测到引脚上特定边沿(上升/下降沿)时,硬件瞬间将当前计数器的值“捕获”到CCR寄存器中,并可以产生中断。你只需要在中断里读取CCR的值,并与上一次捕获的值相减,再根据计数频率,就能算出两次边沿之间的时间差。

测量高电平脉宽的经典方法:设置通道为上升沿捕获。第一次上升沿中断时,记录时间T1,并切换为下降沿捕获。下降沿中断时,记录时间T2,则脉宽 = (T2 - T1)。最后再切换回上升沿捕获,等待下一个周期。这里要注意计数器溢出的处理,如果脉宽可能超过ARR一次计数的时间,就需要在中断里对溢出次数进行计数。

5. 与外界对话:串行通信三巨头(USART, SPI, I2C)

这是单片机与其他芯片、模块、电脑通信的主要方式。

5.1 USART:最常用的异步串口

USART(通用同步异步收发器)我们通常当UART(异步模式)用。核心是波特率,即每秒传输的比特数。双方波特率必须一致。

  • 波特率计算波特率 = 串口时钟 / (16 * USARTDIV)。USARTDIV是一个浮点数,由整数部分和小数部分(分频寄存器)组成。标准库的USART_Init()函数内部会帮你计算并设置好。
  • 数据帧格式:起始位(1位低电平)+ 数据位(8或9位)+ 校验位(可选)+ 停止位(1、1.5、2位高电平)。最常见的是8N1(8数据位,无校验,1停止位)
  • 使用要点
    • 中断接收:对于不定长数据,推荐开启接收中断。在中断服务函数里,将收到的字节存入环形缓冲区。主循环再从缓冲区取出处理。绝对避免在中断里进行复杂处理或调用printf等耗时函数
    • DMA收发:对于高速或大数据量传输,一定要用DMA。可以解放CPU,实现“后台”通信。
    • printf重定向:通过重写fputc函数,可以方便地用printf通过串口打印调试信息,这是最有效的调试手段之一。

5.2 SPI:高速全双工同步通信

SPI(串行外设接口)采用主从模式,四根线:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。时钟由主机产生,数据在时钟边沿同步移出和移入,速度远快于UART。

  • 模式配置:关键参数是时钟极性(CPOL)和时钟相位(CPHA)。这决定了数据在时钟的哪个边沿采样。从设备的数据手册会明确规定其SPI模式(0,1,2,3)。主机必须与从机模式匹配,否则数据错乱。模式0(CPOL=0, CPHA=0)最常用。
  • 软件片选与硬件NSS:通常我们用普通GPIO手动控制CS引脚,更灵活。硬件NSS管理复杂,初学者建议用软件控制。
  • 实操坑点:SPI时序很严格。在初始化外设(如FLASH、屏幕)时,要严格按照其数据手册的序列来,先发命令字,再发地址,再读/写数据。有时需要在命令间插入微小延时。对于MISO引脚,在主机发送数据但不需要接收时,也要注意它的状态,有些从机在此期间会输出高阻态。

5.3 I2C:两根线的多设备总线

I2C(内部集成电路)只需要两根线:SDA(数据)和SCL(时钟)。支持多主多从,通过设备地址寻址。

  • 开漏输出与上拉电阻:I2C总线必须使用开漏输出模式,并且必须在SDA和SCL线上各接一个上拉电阻(通常4.7kΩ),以实现“线与”功能。这是硬件上最容易忽略的一点,没接上拉电阻,总线根本无法正常工作。
  • 协议流程:起始信号 -> 发送7位从机地址+1位读写位 -> 等待从机应答(ACK) -> 发送/接收数据(每字节后跟应答)-> 停止信号。
  • 软件模拟 vs 硬件I2C:STM32的硬件I2C外设在早期固件库中口碑不佳,容易卡死。因此很多人选择用两个GPIO口软件模拟I2C时序(“软件I2C”)。这样虽然速度慢,但稳定可控。现在HAL库对硬件I2C的稳定性有了很大改善,但软件模拟在初学者和简单应用中依然很流行。如果使用硬件I2C,务必处理好总线错误中断和超时复位

6. 模拟世界的接口:ADC与DAC

单片机是数字芯片,要处理真实的模拟信号(电压、温度、声音),就需要ADC(模数转换器)和DAC(数模转换器)。

6.1 ADC:将电压变为数字

F103的ADC是12位精度,即把0-3.3V(参考电压)的输入电压,分成4096(2^12)个等级。

  • 通道与引脚:ADC有多个通道,对应不同的GPIO引脚。有些通道在芯片内部连接到了温度传感器、内部参考电压等。
  • 转换模式
    • 单次转换:触发一次,转换一个通道一次。
    • 连续转换:自动连续对单个通道进行转换。
    • 扫描模式:配合多通道,自动按顺序转换一组通道。这是最常用的模式,可以周期性地轮询多个模拟输入。
  • 触发源:转换可以由软件触发,也可以由定时器事件触发。后者可以实现精确的定时采样,比如用于音频采集。
  • DMA传输:在扫描多通道时,每个通道转换完成都会产生数据。如果每个数据都进中断读取,会消耗大量CPU资源。最佳实践是启用ADC的DMA请求,让DMA自动将转换结果搬运到内存的数组中。你只需要设置好DMA和数组,等一轮转换完成(通过DMA中断或查询标志位)再去处理整个数组的数据即可。
  • 校准与注意:ADC首次启用前最好进行一次校准(调用ADC_Calibration())。另外,ADC的输入阻抗有限,如果信号源内阻很大(比如用很大的分压电阻),会导致采样不准,需要在输入前加电压跟随器(运放)进行缓冲。

6.2 DAC:将数字变为电压

F103的DAC也是12位精度,可以将0-4095的数字值,转换成0-3.3V的模拟电压输出。

  • 输出缓冲:DAC内部有一个输出运放缓冲器,默认是使能的。它提高了带负载能力,但会引入一点偏移误差。对于需要极高精度的场合,可以禁用缓冲器,但输出阻抗会变大,驱动能力变弱。
  • 触发与波形生成:DAC转换也可以由定时器触发。这是一个极其强大的功能:你预先把一个正弦波的数据表存放在数组里,然后用一个定时器以固定频率触发DAC转换,DAC就会自动依次输出数组中的值,从而在引脚上产生一个连续、平滑的模拟波形!这就是直接数字合成(DDS)的简单实现,可以用来做信号发生器。
  • 与ADC的配合:可以用ADC采集一个电压,经过算法处理(比如PID计算),再用DAC输出,形成一个简单的模拟闭环控制系统。

7. 直接内存访问(DMA):解放CPU的搬运工

DMA常常被初学者忽略,但它却是提升系统效率和实现高速数据处理的关键。它的作用很简单:在外设和内存之间,或者内存和内存之间,搬运数据,而完全不需要CPU参与。

7.1 为什么需要DMA?

想象一下,ADC以1MHz的速度采样,每采一个点就产生一个中断让CPU来读取。CPU光是进中断、保存现场、读数据、恢复现场就要消耗几十个时钟周期,根本忙不过来,还会丢失数据。DMA就是解决这个问题的:你告诉DMA源地址(如ADC数据寄存器)、目标地址(如内存中的数组)、数据长度,然后启动它。ADC每转换完一个数据,硬件信号就会通知DMA控制器,DMA就自动把这个数据搬到数组里。搬完指定长度后,DMA再产生一个中断通知CPU“活干完了,数据齐了,你来处理吧”。这样CPU就被解放出来执行其他任务。

7.2 DMA的关键配置

  1. 通道与请求映射:不同的外设(ADC、USART、SPI等)的DMA请求,固定映射到DMA控制器的特定通道上。配置时不能选错。例如,USART1_TX的DMA请求在DMA1的通道4上。
  2. 传输方向:外设到内存(Peripheral-to-Memory),还是内存到外设(Memory-to-Peripheral),或者内存到内存。
  3. 指针递增:搬运完一个数据后,源地址和目标地址的指针是否自动增加。对于把ADC数据搬运到数组,通常设置外设地址不递增(总是ADC数据寄存器地址),内存地址递增。
  4. 数据宽度:外设和内存两端的数据宽度(字节、半字、字)要匹配。
  5. 循环模式:非常有用。当设置为循环模式时,DMA传输完一轮后,会自动重置传输计数器,重新开始下一轮。这对于ADC连续采样、USART持续收发等场景是必须的,实现了“双缓冲区”的类似效果,数据流不会间断。
  6. 中断:可以开启“传输完成中断”,在一轮DMA搬运结束后,去处理已经准备好的完整数据块。

一个综合案例:ADC多通道扫描+DMA+定时器触发。这是嵌入式数据采集的经典方案:

  • 定时器以固定频率(如10kHz)产生一个触发事件(TRGO)。
  • 这个触发事件连接到ADC的转换启动。
  • ADC配置为扫描模式,扫描多个通道(如4个)。
  • ADC的DMA请求被使能。
  • DMA配置为循环模式,目标是一个足够大的内存数组。
  • 系统运行后,定时器每隔100us触发一次ADC,ADC依次转换4个通道,每转换完一个数据,DMA就将其搬走。DMA在后台持续工作,CPU只需要定期(比如每采满1000个点)去处理一下数组中的数据即可。整个系统运行高效,实时性强。

8. 其他重要外设与选型思考

除了上述核心外设,F103还有一些值得了解的功能模块:

  • 备份寄存器 (BKP) 和实时时钟 (RTC):由备用电池供电,系统主电源掉电后,它们依然可以保持时间和少量关键数据。适合做数据记录、事件时间戳等。
  • 看门狗 (IWDG/WWDG):独立看门狗和窗口看门狗。用于在程序跑飞或陷入死循环时,强制复位系统,是产品稳定性的重要保障。工业产品一定要用
  • CAN总线:汽车和工业领域广泛使用的可靠通信协议。F103的CAN外设功能完整,可以用来做车载设备或工业节点。
  • USB 2.0全速设备:可以让你把STM32做成一个USB设备,比如自定义的HID设备(键盘、鼠标)、虚拟串口(CDC)、或者大容量存储设备(MSC)。需要理解USB协议栈,通常使用官方或第三方库(如STM32_USB-FS-Device_Driver)。

关于64K vs 128K Flash的选型:对于初学者和大多数中等复杂度项目,64K Flash(F103C8T6)绝对够用。你的程序代码、常量数据、字库等通常远小于这个值。只有当你需要存储大量资源(如图片、音频数据、复杂字体、或者运行一个轻量级RTOS并包含较多任务)时,才需要考虑128K版本。我的建议是,初期学习和做大部分项目,C8T6性价比最高。它的引脚数(48个)也足够你练习所有基础外设。

最后,学习外设最好的方法不是死记硬背寄存器,而是带着项目去驱动。比如,用PWM做一个呼吸灯,用ADC测电位器电压并在OLED上显示,用定时器输入捕获测遥控器PWM信号,用USART和DMA实现与电脑的快速数据交互。在调试这些功能的过程中,你自然会去查阅参考手册,理解每一个配置位的含义,这才是最有效的学习路径。芯片的数据手册和参考手册是你最好的老师,而库函数(标准库或HAL库)则是帮你简化操作的得力助手。先会用库函数快速实现功能,再尝试去读手册,理解库函数背后配置了哪些寄存器,这样层层深入,就能真正驾驭这颗经典的芯片。