ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103入门实战:从开发环境搭建到GPIO、串口、定时器核心外设精讲

STM32F103入门实战:从开发环境搭建到GPIO、串口、定时器核心外设精讲 1. 从零开始为什么STM32F10x是嵌入式入门的绝佳选择如果你刚刚接触单片机或者从51、Arduino这类平台过来准备踏入“32位机”的世界那么STM32F10x系列几乎是你绕不开的第一站。我当年也是从这个系列入手的它就像嵌入式开发里的“新手村”装备齐全、教程遍地但又足够强大能让你一路玩到“满级”。STM32F10x特别是我们常说的“F103”属于ARM Cortex-M3内核性能远超8位机价格却非常亲民。市面上大量的开发板、教程、开源项目都基于它形成了一个极其繁荣的生态。这意味着你遇到的绝大多数问题几乎都能在网上找到答案这对于初学者来说是降低学习门槛、建立信心的关键。很多人一上来就想搞最前沿的芯片结果光搭环境、找资料就劝退了。从F10x开始你能把精力真正集中在理解嵌入式系统的核心概念上比如GPIO、中断、定时器、串口通信这些而不是在复杂的工具链和稀缺的资料里挣扎。2. 搭建你的第一个战场开发环境与工程创建工欲善其事必先利其器。对于STM32开发一套顺手的工具链是成功的一半。这里主要有两条主流路线Keil MDK-ARM和STM32CubeIDE。我强烈建议初学者从Keil开始并不是说CubeIDE不好而是Keil的生态更成熟你遇到的所有初级问题几乎都能找到基于Keil的解决方案包括寄存器操作和标准库。2.1 工具链安装与芯片包管理首先你需要安装Keil MDK现在叫Keil MDK-ARM。安装过程比较简单但有一个关键步骤安装对应的Device Family PackDFP也就是STM32F1系列的芯片支持包。很多新手安装完Keil后发现新建工程时找不到STM32F103系列问题就出在这里。注意Keil的许可证管理比较严格对于学习用途可以使用其代码大小限制的评估版。如果遇到编译后代码超限通常是未正确激活或安装芯片包。安装完Keil和F1的DFP后你还需要一个调试下载器最经济实惠的就是ST-Link。市面上二三十块的ST-Link V2兼容版非常多基本都能用。你需要安装ST-Link的驱动通常卖家会提供或者去ST官网下载“STSW-LINK009”这个软件包里面包含驱动。安装好后把ST-Link通过SWD接口SWDIO、SWCLK、GND、3.3V连接到你的开发板在Keil的调试设置里选择“ST-Link Debugger”就能识别到芯片了。2.2 新建工程的两种哲学标准库与HAL库新建第一个工程时你会面临第一个重要选择用标准外设库Standard Peripheral Library, SPL还是硬件抽象层库Hardware Abstraction Layer Library, HAL标准库SPL这是STM32早期的官方库现在已经停止更新但对于F1系列来说完全够用并且是存量资料最多的。它更接近底层寄存器你需要手动配置每个外设的时钟、初始化结构体。学习标准库能让你更深刻地理解外设的工作原理知道每一行代码在操作哪个寄存器。对于打基础来说这是最佳选择。新建工程时你需要手动将标准库的源文件stm32f10x_xxx.c和头文件路径添加到项目中。HAL库这是ST现在主推的库配合STM32CubeMX图形化配置工具使用可以自动生成初始化代码。它的优点是移植性好换一个STM32系列芯片代码改动可能很小缺点是代码比较臃肿执行效率相对低一些并且封装层次高有时出了问题不好排查。我的建议是入门阶段从标准库开始。虽然前期麻烦一点需要自己搭建工程框架但这个过程能让你彻底搞明白时钟树、外设映射、头文件包含这些基础概念。很多经典的教程比如正点原子、野火其早期资料都是基于标准库的跟着学更容易。当你用标准库把GPIO、串口、定时器都玩转之后再去看HAL库和CubeMX就会有一种“降维打击”的感觉理解它为什么那么设计。这里给出一个基于标准库的新建工程核心步骤在Keil中创建新项目选择你的具体芯片型号如STM32F103C8T6。在项目目录下手动创建User、Library、Startup等文件夹。将标准库文件复制到Library文件夹。关键文件包括CMSIS文件夹下的核心支持文件。STM32F10x_StdPeriph_Driver下的src源文件和inc头文件。启动文件startup_stm32f10x_md.s对于中等容量F103md表示Medium Density。在Keil的工程管理窗口中将这些文件分组添加到项目中。在Keil的Options for Target-C/C中添加头文件包含路径并定义全局宏USE_STDPERIPH_DRIVER和STM32F10X_MD根据你的芯片容量定义。编写一个简单的main.c包含#include “stm32f10x.h”并写一个空的while(1)循环。编译通过0错误0警告你的工程框架就搭好了。3. 点亮第一盏灯深入理解GPIO与时钟系统第一个程序永远是“点灯”但这不仅仅是让一个LED闪烁。这是你理解STM32运行机制的第一个窗口。3.1 时钟树芯片的脉搏在写任何外设代码之前必须理解时钟。STM32的时钟树比较复杂但入门时你只需要抓住几个关键点HSI/HSE内部高速时钟8MHz精度一般和外部高速时钟通常接8MHz晶振更稳定。系统上电默认使用HSI。PLL锁相环倍频器。可以把HSI或HSE的频率倍频得到更高的系统时钟SYSCLK。对于F103最高可以到72MHz。我们通常用HSE8M经过PLL 9倍频得到72MHz的SYSCLK。AHB、APB1、APB2总线SYSCLK经过分频供给不同的总线。APB2是高速总线GPIO、USART1、高级定时器挂在这上面APB1是低速总线最大36MHzUSART2/3、I2C、基本定时器挂在这上面。为什么点灯前要配置时钟因为GPIO外设挂在APB2总线上默认情况下它的时钟可能是关闭的为了省电。你必须先打开APB2总线上GPIO端口的时钟才能去配置它的引脚。在标准库中使用RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);来打开GPIOC的时钟。3.2 GPIO的八种模式与配置要点STM32的GPIO功能强大有8种模式输入类模拟输入、浮空输入、上拉输入、下拉输入。输出类开漏输出、推挽输出、复用开漏输出、复用推挽输出。对于驱动普通的LED阴极接GPIO阳极接VCC我们使用推挽输出模式。它能提供较强的拉电流和灌电流能力。配置一个GPIO引脚在标准库中遵循以下步骤// 1. 定义GPIO初始化结构体 GPIO_InitTypeDef GPIO_InitStructure; // 2. 开启对应GPIO端口的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 3. 配置结构体成员 GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; // 引脚13 GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 输出速度驱动LED选50MHz没问题 // 4. 初始化 GPIO_Init(GPIOC, GPIO_InitStructure); // 5. 操作引脚 GPIO_SetBits(GPIOC, GPIO_Pin_13); // 置高LED灭假设低电平点亮 GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 置低LED亮这里有一个关键细节GPIO_Speed。它配置的是IO口的翻转速度速度越高边沿越陡峭功耗和噪声也越大。对于简单的LED闪烁2MHz都绰绰有余。但在后续做串口通信、特别是PWM输出时这个速度需要根据实际信号频率来设置否则可能导致波形失真。3.3 软件延时与系统滴答定时器SysTick让LED闪烁我们需要延时。最简陋的方法是写一个for循环空转这就是软件延时。它极度不精确且会独占CPU。void Delay_ms(uint32_t nms) { uint32_t i, j; for(i0; inms; i) for(j0; j8000; j); // 这个8000需要根据你的系统时钟频率实际调整 }更优雅的方式是使用SysTick。这是一个24位的递减计数器属于Cortex-M3内核的内设定时器通常用来提供操作系统的时钟节拍。我们可以用它来实现精确的毫秒级延时。初始化SysTick将其时钟源设置为系统时钟AHB并配置重装载值。例如系统时钟72MHz我们设置重装载值为72000那么每次中断就是1ms。编写中断服务函数在里面对一个全局变量如msTicks进行递增。实现一个Delay_ms()函数其原理是记录当前的msTicks值然后循环等待直到msTicks的增量达到所需的毫秒数。使用SysTick延时CPU在等待期间可以执行其他任务如果是放在主循环里判断或者进入低功耗模式效率高得多。这是你从“单片机思维”迈向“嵌入式系统思维”的第一步。4. 与外界对话串口通信与调试信息输出点亮LED后下一个里程碑就是让芯片“说话”也就是串口通信。这是调试和与上位机交互的最重要手段。4.1 USART外设配置与引脚复用STM32F103的USART1挂在APB2总线其他USART挂在APB1。配置流程和GPIO类似但多了一个概念引脚复用。USART的TX、RX引脚通常是GPIO的复用功能。以USART1为例PA9为TXPA10为RX开启GPIOA和USART1的时钟。配置PA9为复用推挽输出GPIO_Mode_AF_PPPA10为浮空输入或上拉输入GPIO_Mode_IN_FLOATING/IPU。配置USART初始化结构体波特率、字长、停止位、奇偶校验、硬件流控制、收发模式。使能USART。USART_InitTypeDef USART_InitStructure; // ... 开启时钟配置GPIO ... USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE);4.2 发送数据与重定向printf配置好后就可以用USART_SendData()函数发送一个字节了。但更常用的方法是重定向C库的printf函数到串口这样就能像在PC上一样方便地打印调试信息。重定向需要重写fputc函数#include stdio.h // 记得包含这个头文件 int fputc(int ch, FILE *f) { while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 等待发送完成 USART_SendData(USART1, (uint8_t) ch); return ch; }然后在Keil的Options for Target-Target中勾选Use MicroLIB一个针对嵌入式优化的小型C库。这样你就可以在代码中直接使用printf(“Hello STM32!\r\n”);了。注意printf函数会显著增加代码体积并且不是线程安全的。在正式项目中如果资源紧张或需要实时性建议自己封装一个轻量级的日志输出函数而不是直接使用printf。4.3 接收数据与中断处理发送解决了接收同样重要。接收数据通常采用中断方式避免主程序轮询占用CPU。在USART初始化后使能接收中断USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);在NVIC嵌套向量中断控制器中配置USART1中断的优先级并使能。编写USART1的中断服务函数USART1_IRQHandler()。在函数内判断中断来源是否是USART_IT_RXNE接收寄存器非空如果是则读取数据USART_ReceiveData(USART1)并进行处理例如存入环形缓冲区。一个常见的坑串口接收一段时间后卡死可能原因是溢出错误ORE。当接收数据过快CPU来不及读取新的数据就会覆盖旧数据并产生溢出错误。一旦发生ORE需要先读取USART_ReceiveData()再清除错误标志USART_ClearFlag(USART1, USART_FLAG_ORE)否则串口会一直卡在错误状态。在HAL库中这个错误可能表现为HAL_UART_ERROR_FE之类的标志。5. 精准的节奏大师定时器基础与PWM应用定时器是STM32最强大、最复杂的外设之一。从基本的定时中断到输入捕获测频率再到PWM输出控制电机、舵机都离不开它。5.1 基本定时器TIM6/TIM7与通用定时器TIM2-TIM5F103的定时器分好几类入门主要接触基本定时器和通用定时器。基本定时器最简单只有时基单元主要用于产生定时中断或者DAC触发。通用定时器功能全面具备时基单元、输入捕获、输出比较、PWM生成、单脉冲输出等。TIM2-TIM5是16位向上/向下/中央对齐计数器的通用定时器。时基单元是核心包括预分频器PSC对输入时钟CK_PSC进行分频。例如系统时钟72MHzPSC设为71则计数器时钟 72MHz / (711) 1MHz。计数器CNT在计数器时钟驱动下递增或递减。自动重装载寄存器ARR决定计数周期。当CNT计数到ARR值向上计数时产生更新事件UEVCNT清零重新开始同时可以触发中断或DMA。定时时间的计算定时时间 (ARR 1) * (PSC 1) / 定时器时钟频率。例如定时器时钟72MHzPSC7199ARR9999则定时时间 (99991)(71991)/72MHz 100007200/72,000,000 1秒。5.2 输出比较与PWM生成PWM脉冲宽度调制是控制亮度、速度、位置的利器。STM32的通用定时器可以很方便地生成PWM。配置定时器时基PSC, ARR这决定了PWM的频率。PWM频率 定时器时钟 / ((PSC1)*(ARR1))。配置对应通道如TIM3的通道1对应PA6为PWM输出模式。在标准库中使用TIM_OCInitTypeDef结构体设置TIM_OCMode TIM_OCMode_PWM1TIM_OutputState TIM_OutputState_EnableTIM_Pulse这个值决定了占空比。TIM_Pulse是捕获比较寄存器CCR的值。占空比 TIM_Pulse / (ARR1)。例如ARR999TIM_Pulse300则占空比为30%。使能定时器的预装载寄存器TIM_OCPreloadConfig并使能定时器。之后你只需要动态修改CCR的值就能改变PWM的占空比。一个关键点对于电机控制等需要多路同步PWM的场景如三相逆变会用到互补输出和死区插入这是高级定时器TIM1/TIM8的功能初学者了解即可。5.3 输入捕获测量频率与脉宽输入捕获是定时器的另一个重要功能可以用来测量外部信号的频率或脉宽。其原理是当捕获引脚上出现指定边沿上升沿或下降沿时定时器当前的CNT值会被锁存到捕获比较寄存器CCR中并可以产生中断。配置定时器时基设置一个足够大的ARR确保在测量信号周期内计数器不会溢出。配置输入捕获通道设置触发边沿如上升沿。在中断中读取CCR的值。这个值就是从上一次清零到本次边沿触发所经过的时钟计数。连续捕获两个上升沿将两次CCR的差值考虑计数器溢出乘以计数时钟的周期就得到了信号的周期其倒数即为频率。测量高频率信号时如果信号频率接近甚至超过定时器计数频率直接测量误差会很大。此时可以采用测周法测量多个周期求平均或者使用定时器的外部时钟模式将待测信号直接作为定时器的计数时钟源。6. 应对突发事件外部中断与NVIC优先级管理GPIO中断或者说外部中断EXTI是单片机响应外部异步事件的关键机制。比如按键按下、传感器信号跳变。6.1 EXTI线路与GPIO的映射STM32的EXTI控制器有19条中断/事件线线0~15对应GPIO的Pin0~Pin15线16~18连接特定外设如PVD、RTC。同一个Pin编号的GPIO只能有一个连接到EXTI线。例如PA0、PB0、PC0…都连接到EXTI线0但同一时刻只能选择其中一个作为中断源。配置一个按键接在PA0下降沿触发中断的步骤开启GPIOA和AFIO复用功能IO用于EXTI引脚选择的时钟。配置PA0为上拉输入这样按键按下时产生下降沿。配置EXTI线0与PA0连接GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);配置EXTI初始化结构体选择线0触发方式为下降沿使能中断。配置NVIC设置EXTI0中断通道的优先级并使能。编写中断服务函数EXTI0_IRQHandler()在函数内判断是否是EXTI线0的中断处理按键逻辑最后必须清除中断标志位EXTI_ClearITPendingBit(EXTI_Line0);否则会不断进入中断。6.2 NVIC中断的调度中心NVIC嵌套向量中断控制器是Cortex-M3内核的一部分负责管理所有中断的优先级和响应。抢占优先级 子优先级STM32的中断优先级分为抢占优先级和子优先级。抢占优先级高的可以打断抢占优先级低的中断嵌套。抢占优先级相同的中断同时发生时子优先级高的先执行如果抢占和子优先级都相同则按硬件中断编号顺序执行。优先级分组通过NVIC_PriorityGroupConfig()设置优先级分组来决定多少位用于抢占优先级多少位用于子优先级。例如NVIC_PriorityGroup_2表示2位抢占优先级0~32位子优先级0~3。这个设置整个项目只需要调用一次通常在main函数开头。中断服务函数每个中断都有固定的函数名必须与启动文件中定义的向量表一致。例如EXTI0中断函数必须命名为EXTI0_IRQHandler。在函数内部要先判断具体是哪个中断源因为多个EXTI线可能共用一个中断向量处理完后必须清除对应的中断标志位。一个重要的经验中断服务函数要尽可能短小精悍只做最紧急的事情如设置标志位、拷贝数据。复杂的处理逻辑应该放到主循环中根据中断设置的标志位来执行。这就是“前台后台”系统的基本思想避免在中断中耗时过长影响其他中断的响应。7. 直接内存访问DMA的基本概念与应用场景当你需要大量、高速地在内存和外设之间搬运数据时比如ADC采集一串数据存到数组或者从内存发送一大段数据到串口如果都用CPU来搬运会严重消耗CPU资源。DMA直接内存访问就是为了解放CPU而生的。7.1 DMA工作原理与通道资源DMA控制器就像一个“智能的数据搬运工”。你告诉它源地址在哪里比如ADC数据寄存器目标地址在哪里比如内存中的一个数组要搬多少数据数据宽度是多少。然后你启动它它就会在后台默默工作搬完指定数量的数据后给你发一个中断通知整个过程几乎不占用CPU。STM32F103有两个DMA控制器DMA1和DMA2每个控制器有若干通道。不同的外设会固定分配到特定的DMA通道上。例如DMA1的通道1可以用于ADC1、TIM2_CH3等。配置时不能冲突。7.2 以串口发送为例的DMA配置使用DMA进行串口发送可以极大地提高效率尤其是在发送大量数据如图像、文件时。开启USART和对应DMA通道的时钟。配置USART使能其DMA发送请求USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);。配置DMA初始化结构体DMA_PeripheralBaseAddr外设地址即USART1-DR。DMA_MemoryBaseAddr内存地址即你要发送的数据数组首地址。DMA_DIR传输方向从内存到外设。DMA_BufferSize要传输的数据量。DMA_PeripheralInc外设地址是否递增否固定为DR寄存器。DMA_MemoryInc内存地址是否递增是依次发送数组元素。DMA_PeripheralDataSize/DMA_MemoryDataSize数据宽度字节/半字/字。DMA_Mode模式可选单次传输或循环传输。DMA_Priority通道优先级。使能DMA通道。当需要发送时设置好内存地址和缓冲区大小然后使能USART的DMA请求传输就会自动开始。使用DMA的注意事项缓存一致性如果内存缓冲区位于CPU缓存中在STM32F103这种没有Cache的芯片上不存在此问题但在高级芯片上需要注意DMA操作的是物理内存CPU操作的是缓存可能导致数据不一致。通常需要手动刷新或无效化缓存。传输完成中断务必在DMA传输完成中断中关闭DMA通道或进行下一轮操作准备并清除中断标志。避免重复触发。内存对齐确保源和目标地址的数据宽度对齐否则可能导致硬件错误。8. 模拟世界的窗口ADC采集与数据处理基础单片机处理的是数字信号但现实世界很多信号是模拟的温度、光照、电压。ADC模数转换器就是连接两者的桥梁。8.1 STM32F103的ADC特性与配置F103的ADC是12位逐次逼近型。12位分辨率意味着可以将参考电压通常是3.3V分成40962^12个等级每个等级约0.8mV。配置ADC涉及以下关键参数采样时间ADC内部有一个采样保持电容采样时间决定了给这个电容充电的时间。对于高内阻的信号源需要更长的采样时间才能保证采样准确。STM32的ADC采样时间可以配置为1.5到239.5个ADC时钟周期。转换模式单次转换触发一次转换一个通道后停止。连续转换开启后自动连续转换指定通道。扫描模式配合多通道自动按顺序转换一组通道。触发源可以是软件触发ADC_SoftwareStartConvCmd也可以是定时器触发实现定时等间隔采样。数据对齐12位结果可以左对齐或右对齐存放在16位数据寄存器中。右对齐更直观。一个简单的单通道ADC读取流程以规则通道为例开启ADC和GPIO模拟输入引脚时钟。配置GPIO为模拟输入模式。配置ADC初始化结构体分辨率、扫描/连续模式、触发方式、数据对齐。配置ADC规则通道选择通道号、采样时间。使能ADC进行校准先执行复位校准再执行校准。软件触发转换等待转换完成标志位ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)然后读取数据ADC_GetConversionValue(ADC1)。8.2 提高ADC精度硬件与软件滤波ADC的读数总会存在波动和噪声。为了提高稳定性需要滤波。硬件滤波在ADC输入引脚加一个小的滤波电容如0.1uF到地可以滤除高频噪声。对于低频信号可以使用RC低通滤波电路。软件滤波最简单有效的是多次采样取平均。例如连续采样64次然后求和取平均值。更高级的可以用中值滤波、滑动平均滤波等。一个关于参考电压的坑STM32F103的ADC参考电压默认是VDDA通常与VCC相连3.3V。如果VCC不稳定ADC读数就会漂移。对于精度要求高的场合建议使用独立、稳定的基准电压源芯片给VREF引脚供电并将ADC配置为使用外部VREF。8.3 定时器触发与DMA搬运在需要高速、连续采样的场合如音频信号单次软件触发效率太低。这时需要结合定时器和DMA配置一个定时器如TIM2产生固定频率的更新事件PWM输出模式也可以但通常用更新事件作为触发源。配置ADC为外部触发转换触发源选择该定时器的触发输出如ADC_ExternalTrigConv_T2_TRGO。配置DMA将ADC数据寄存器ADC1-DR的数据自动搬运到内存中的一个大型数组。启动定时器、使能ADC的DMA、使能ADC。定时器会按照设定频率自动触发ADC转换转换结果通过DMA实时存到内存。你只需要在DMA传输完成中断中处理已经采集好的一整块数据即可。这种方式实现了“无人值守”的高速数据采集CPU开销极低。
返回列表