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内部有三条主要的总线你可以把它们想象成不同等级和用途的公路D-Bus (数据总线) 和 I-Bus (指令总线)这是连接内核和Flash存储器的“专用高速路”。内核通过I-Bus取指令通过D-Bus读写数据。你写的程序代码就存放在Flash里内核通过这两条路快速获取并执行。对于64K/128K Flash的型号这条通路至关重要。System Bus (系统总线)这条总线连接内核、SRAM运行内存和主要的高速外设比如DMA控制器。当你的程序需要操作大量数据比如数组处理、串口数据搬运时就会用到这条总线。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最大36MHzAPB2最大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()设置分组决定了多少位用于抢占。例如分组22位抢占2位子优先级意味着有4个可抢占的优先级层次。建议一个项目里固定一种分组方式不要混用否则优先级判断会混乱。与EXTI联动外部中断/事件控制器EXTI是GPIO和NVIC之间的桥梁。它负责监测GPIO引脚上的边沿上升沿、下降沿并产生中断请求给NVIC。配置外部中断的步骤通常是1. 初始化GPIO为输入2. 开启AFIO时钟因为EXTI映射功能属于AFIO3. 用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 核心概念时钟源、预分频器、自动重载寄存器理解定时器关键是弄懂这三个寄存器如何协同工作时钟源定时器挂载在APB1或APB2上。这里有个关键点如果APB预分频系数是1则定时器时钟等于APB时钟否则定时器时钟是APB时钟的2倍。例如系统时钟72MHzAPB1预分频系数为2得36MHz那么挂载在APB1上的TIM2的时钟不是36MHz而是72MHz这个细节很多资料会忽略导致计算定时时间出错。预分频器 (PSC)它是一个16位寄存器可以对定时器时钟进行分频。实际驱动计数器累加的时钟频率 定时器时钟 / (PSC 1)。自动重载寄存器 (ARR)计数器从0开始累加累加到ARR值或从ARR值向下减到0时产生一次“更新事件”计数器重置并可以触发更新中断。定时时间的计算定时时间 (ARR 1) * (PSC 1) / 定时器时钟频率。例如定时器时钟72MHz要产生1ms中断设PSC7199则分频后时钟为72MHz / 7200 10kHz。ARR设为9则定时周期为 (91) / 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位高电平。最常见的是8N18数据位无校验1停止位。使用要点中断接收对于不定长数据推荐开启接收中断。在中断服务函数里将收到的字节存入环形缓冲区。主循环再从缓冲区取出处理。绝对避免在中断里进行复杂处理或调用printf等耗时函数。DMA收发对于高速或大数据量传输一定要用DMA。可以解放CPU实现“后台”通信。printf重定向通过重写fputc函数可以方便地用printf通过串口打印调试信息这是最有效的调试手段之一。5.2 SPI高速全双工同步通信SPI串行外设接口采用主从模式四根线SCK时钟、MOSI主出从入、MISO主入从出、CS片选。时钟由主机产生数据在时钟边沿同步移出和移入速度远快于UART。模式配置关键参数是时钟极性CPOL和时钟相位CPHA。这决定了数据在时钟的哪个边沿采样。从设备的数据手册会明确规定其SPI模式0,1,2,3。主机必须与从机模式匹配否则数据错乱。模式0CPOL0 CPHA0最常用。软件片选与硬件NSS通常我们用普通GPIO手动控制CS引脚更灵活。硬件NSS管理复杂初学者建议用软件控制。实操坑点SPI时序很严格。在初始化外设如FLASH、屏幕时要严格按照其数据手册的序列来先发命令字再发地址再读/写数据。有时需要在命令间插入微小延时。对于MISO引脚在主机发送数据但不需要接收时也要注意它的状态有些从机在此期间会输出高阻态。5.3 I2C两根线的多设备总线I2C内部集成电路只需要两根线SDA数据和SCL时钟。支持多主多从通过设备地址寻址。开漏输出与上拉电阻I2C总线必须使用开漏输出模式并且必须在SDA和SCL线上各接一个上拉电阻通常4.7kΩ以实现“线与”功能。这是硬件上最容易忽略的一点没接上拉电阻总线根本无法正常工作。协议流程起始信号 - 发送7位从机地址1位读写位 - 等待从机应答(ACK) - 发送/接收数据每字节后跟应答- 停止信号。软件模拟 vs 硬件I2CSTM32的硬件I2C外设在早期固件库中口碑不佳容易卡死。因此很多人选择用两个GPIO口软件模拟I2C时序“软件I2C”。这样虽然速度慢但稳定可控。现在HAL库对硬件I2C的稳定性有了很大改善但软件模拟在初学者和简单应用中依然很流行。如果使用硬件I2C务必处理好总线错误中断和超时复位。6. 模拟世界的接口ADC与DAC单片机是数字芯片要处理真实的模拟信号电压、温度、声音就需要ADC模数转换器和DAC数模转换器。6.1 ADC将电压变为数字F103的ADC是12位精度即把0-3.3V参考电压的输入电压分成40962^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的关键配置通道与请求映射不同的外设ADC、USART、SPI等的DMA请求固定映射到DMA控制器的特定通道上。配置时不能选错。例如USART1_TX的DMA请求在DMA1的通道4上。传输方向外设到内存Peripheral-to-Memory还是内存到外设Memory-to-Peripheral或者内存到内存。指针递增搬运完一个数据后源地址和目标地址的指针是否自动增加。对于把ADC数据搬运到数组通常设置外设地址不递增总是ADC数据寄存器地址内存地址递增。数据宽度外设和内存两端的数据宽度字节、半字、字要匹配。循环模式非常有用。当设置为循环模式时DMA传输完一轮后会自动重置传输计数器重新开始下一轮。这对于ADC连续采样、USART持续收发等场景是必须的实现了“双缓冲区”的类似效果数据流不会间断。中断可以开启“传输完成中断”在一轮DMA搬运结束后去处理已经准备好的完整数据块。一个综合案例ADC多通道扫描DMA定时器触发。这是嵌入式数据采集的经典方案定时器以固定频率如10kHz产生一个触发事件TRGO。这个触发事件连接到ADC的转换启动。ADC配置为扫描模式扫描多个通道如4个。ADC的DMA请求被使能。DMA配置为循环模式目标是一个足够大的内存数组。系统运行后定时器每隔100us触发一次ADCADC依次转换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 FlashF103C8T6绝对够用。你的程序代码、常量数据、字库等通常远小于这个值。只有当你需要存储大量资源如图片、音频数据、复杂字体、或者运行一个轻量级RTOS并包含较多任务时才需要考虑128K版本。我的建议是初期学习和做大部分项目C8T6性价比最高。它的引脚数48个也足够你练习所有基础外设。最后学习外设最好的方法不是死记硬背寄存器而是带着项目去驱动。比如用PWM做一个呼吸灯用ADC测电位器电压并在OLED上显示用定时器输入捕获测遥控器PWM信号用USART和DMA实现与电脑的快速数据交互。在调试这些功能的过程中你自然会去查阅参考手册理解每一个配置位的含义这才是最有效的学习路径。芯片的数据手册和参考手册是你最好的老师而库函数标准库或HAL库则是帮你简化操作的得力助手。先会用库函数快速实现功能再尝试去读手册理解库函数背后配置了哪些寄存器这样层层深入就能真正驾驭这颗经典的芯片。