1. 从最小系统到第一个工程:STM32F103的“上电仪式”
拿到一块STM32F103的开发板,或者自己画了一块最小系统板,第一步要做什么?很多新手会迫不及待地打开例程,直接编译下载,然后对着不亮的LED或者没反应的串口发愁。我刚开始学的时候也这样,总觉得是代码有问题,反复折腾,最后才发现问题出在最基础的环节——工程环境没搭对,或者对芯片最基本的运行条件理解有误。所以,这篇笔记我们不急着写复杂的代码,而是先来一场彻底的“上电仪式”,把STM32F103从一块“砖头”变成一台可以听话的“计算机”。
这个过程的核心,就是理解最小系统和建立标准工程模板。最小系统是芯片能工作的最低硬件要求,就像人的心脏和大脑必须供血供氧;而工程模板则是我们与芯片对话的标准化“语言”和“工作台”。网上很多教程会直接给你一个现成的模板,但如果不清楚里面每一个文件、每一个配置项的来龙去脉,后续移植、调试时就会寸步难行。我会结合自己踩过的坑,带你从零搭建一个清晰、可移植的工程,并解释每一个关键步骤背后的“为什么”。
2. 最小系统深度解析:不只是原理图连接
提到STM32F103的最小系统,大家通常会想到:电源、复位、晶振、启动模式、下载接口。这没错,但仅仅连接正确还不够,我们必须理解每个部分的设计要点和常见陷阱。
2.1 电源与滤波:稳定的基石
STM32F103通常需要两组电源:VDD/VSS(主电源)和VDDA/VSSA(模拟电源)。很多初学者为了省事,直接把VDDA接到VDD,在要求不高的场合或许能工作,但这为ADC、DAC等模拟电路的精度埋下了隐患。
- 设计要点:VDDA必须从模拟电源域获取。即使使用同一个3.3V电源,也建议通过一个磁珠或0Ω电阻从VDD隔离过来,并在靠近芯片引脚处放置一个10uF钽电容+一个100nF陶瓷电容组成的滤波网络。VDD的每个引脚附近都需要一个100nF的去耦电容,这是必须的,它能提供芯片内部高速开关瞬间所需的大电流,没有它,芯片可能会运行不稳定甚至死机。
- 常见坑点:PCB布局时,这些去耦电容必须尽可能靠近芯片的电源引脚,走线要短而粗。我曾遇到一个板子ADC采样值跳动大,最后发现是VDDA的滤波电容放得太远了,几乎没起作用。
2.2 复位电路:不仅仅是上电复位
复位电路保证了芯片从一个已知的、确定的状态开始执行。除了经典的RC复位电路(10k电阻+10uF电容),现在更多使用专用的复位芯片(如MAX809),可以提供更精确的复位阈值和手动复位功能。
- 关键参数:STM32的NRST引脚是低电平有效。RC复位电路的时间常数(τ=R*C)决定了复位脉冲的宽度。这个宽度必须大于芯片要求的最小复位脉冲宽度(在数据手册中查找)。通常10k和10uF的组合(τ=100ms)是安全且常见的。
- 调试接口的复位:在使用ST-Link等调试器时,调试器通常会通过SWD接口的NRST信号线来控制芯片复位。如果你的板子上有复位按钮和RC电路,需要确保它们与调试器的复位驱动不会冲突(通常不会,因为都是开漏输出下拉)。
2.3 时钟系统:芯片的脉搏
STM32F103的时钟树是其强大灵活性的体现,但也相对复杂。最小系统上的两个晶振(8MHz主晶振和32.768kHz RTC晶振)为时钟树提供了源头。
- HSE(高速外部时钟):通常接8MHz晶振。它是系统时钟(SYSCLK)的主要来源,通过PLL倍频最高可达72MHz。晶振两端需要接两个20pF左右的负载电容,容值必须根据晶振规格书和PCB杂散电容调整,否则可能不起振或频率不准。
- LSE(低速外部时钟):通常接32.768kHz晶振,专供RTC(实时时钟)和独立看门狗(IWDG)使用。它的精度决定了时钟走时的准确性。同样需要注意负载电容的匹配。
- HSI(高速内部时钟):芯片自带的8MHz RC振荡器,精度较差(±1%),但无需外接元件。在HSE失效时,系统会自动切换至HSI,提高了可靠性。在初期调试时,如果怀疑晶振问题,可以先将代码配置为使用HSI,以排除硬件问题。
- 我的踩坑经历:有一次,板子烧录程序后毫无反应,测量电源、复位都正常。最后用示波器一看,8MHz晶振根本没起振。原因是PCB布局时,晶振走线从单片机下面穿过,受到了干扰,且负载电容容值不匹配。重新调整布局并更换电容后解决。教训是:时钟电路是模拟电路,对布局敏感,务必远离数字噪声源(如开关电源、高速数据线)。
2.4 启动模式与下载接口
BOOT0和BOOT1引脚决定了芯片上电后从哪里开始执行程序。最常用的模式是BOOT0=0(从用户闪存启动)。下载程序我们通常使用SWD接口(SWDIO和SWCLK),它只需要两根线,比传统的JTAG更节省引脚。
- SWD接口设计:除了连接SWDIO和SWCLK到调试器,强烈建议把NRST和GND也连上。连接NRST可以让调试器进行硬件复位,这在调试需要特定上电时序的程序时非常有用。VCC可以不连,但连上可以让一些调试器自动识别目标板电压。
- 上拉电阻:SWDIO线通常需要在目标板侧接一个10kΩ的上拉电阻到VCC,以确保接口在空闲时的状态稳定。虽然很多调试器内部也有上拉,但在目标板加上更可靠。
3. 构建你的第一个标准工程:告别复制粘贴
理解了硬件基础,我们开始在软件上“盖房子”。使用Keil MDK或IAR等IDE,创建一个清晰、模块化的工程结构至关重要。下面以Keil MDK为例。
3.1 工程目录结构设计
不要把所有文件都扔在同一个文件夹里。一个良好的结构应该是这样的:
Your_Project/ ├── Core/ │ ├── Inc/ // 用户头文件,如 main.h, gpio_config.h │ ├── Src/ // 用户源文件,如 main.c, gpio_config.c │ └── Startup/ // 启动文件 startup_stm32f103xe.s ├── Drivers/ │ ├── CMSIS/ // Cortex-M内核抽象层,包含核心寄存器定义 │ └── STM32F1xx_HAL_Driver/ │ ├── Inc/ // HAL库头文件 │ └── Src/ // HAL库源文件 ├── Middlewares/ // 中间件,如 FreeRTOS, FatFs (后续添加) ├── Projects/ │ └── MDK-ARM/ // Keil工程文件 (.uvprojx) ├── Utilities/ // 公用工具,如 debug.c, printf重定向 └── README.md为什么这么设计?解耦和复用。Core放你的应用代码;Drivers放芯片厂商提供的标准驱动,这部分几乎不用动;Projects放IDE相关的工程文件。这样,当你换一个IDE(比如移植到IAR或Makefile),或者升级HAL库版本时,只需要替换Drivers下的内容,你的应用代码Core完全不受影响。
3.2 关键文件详解与配置
启动文件 (
startup_stm32f103xe.s): 这是芯片上电后运行的第一段代码,用汇编写成。它做了三件大事:- 初始化堆栈指针(SP)。
- 设置中断向量表,并把复位中断的入口指向
main函数。 - 调用
SystemInit函数(在system_stm32f1xx.c中)初始化时钟,然后跳转到main函数。 - 注意:不同容量的STM32F103型号(小容量、中容量、大容量)对应的启动文件不同,后缀名不同(如
hd-大容量,md-中容量)。选错会导致程序跑飞。
系统初始化 (
system_stm32f1xx.c): 这个文件里的SystemInit()函数默认将时钟配置为使用HSI。我们几乎总是要修改它!我们需要在这个函数里(或者更好的做法,在main函数开头调用自己的时钟配置函数),将时钟源切换到HSE,并配置PLL,将系统时钟提升到72MHz。// 一个简单的时钟配置示例 (HAL库) void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; // 1. 配置HSE和PLL RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 开启HSE RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // HSE 8MHz * 9 = 72MHz HAL_RCC_OscConfig(&RCC_OscInitStruct); // 2. 配置系统时钟源、AHB、APB1、APB2分频 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 系统时钟来自PLL RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB 时钟 = 72MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; // APB1时钟 = 36MHz (最大) RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟 = 72MHz HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); // Flash等待周期需匹配 }关键点:
FLASH_LATENCY_2(两个等待周期)是当系统时钟大于48MHz时必须设置的,否则CPU从Flash读指令会出错,表现为程序运行异常。这个坑非常隐蔽。链接脚本 (
*.ld或 Keil中的*.sct): 它告诉链接器程序代码(.text)、初始化数据(.data)、未初始化数据(.bss)分别放在芯片存储器的什么位置(Flash的哪个地址,RAM的哪个地址)。对于初学者,Keil工程默认生成的分散加载文件(.sct)通常无需修改。但当你需要将某些变量放在特定RAM区域(比如用于DMA),或者使用内存管理时,就需要理解并修改它了。
3.3 外设引脚配置可视化工具:STM32CubeMX
手动编写上述初始化代码是很好的学习过程,但效率低且易出错。ST官方提供的STM32CubeMX图形化工具是必备神器。它允许你通过点击和拖拽来配置引脚功能、时钟树、中间件,并生成完整的初始化代码工程(支持Keil, IAR, Makefile)。
- 使用流程:
- 选择你的芯片型号(如STM32F103C8T6)。
- 在
Pinout & Configuration标签页,配置时钟树(直观地拉到72MHz),配置引脚功能(如设置PA0为GPIO_Output,用于控制LED)。 - 在
Project Manager标签页,设置工程名称、路径、IDE类型。 - 点击
Generate Code,它会生成一个包含所有初始化代码的完整工程。
- 注意事项:CubeMX生成的代码基于HAL库。它会在
main.c中调用HAL_Init(),并生成SystemClock_Config()函数。强烈建议在生成的/* USER CODE BEGIN * /和/ * USER CODE END */注释对之间添加你自己的代码,这样当你后续根据硬件变化重新配置CubeMX并重新生成代码时,你添加的代码不会被覆盖。
4. GPIO入门与调试利器:点亮LED与printf重定向
工程建好了,时钟配好了,第一个实战任务就是点亮LED。这不仅仅是控制一个引脚,更是理解STM32 GPIO工作模式、掌握调试方法的第一步。
4.1 GPIO的八种模式与配置要点
STM32的每个GPIO引脚都可以独立配置为多种模式,这是其灵活性的基础。以推挽输出模式点亮LED为例:
// 使用HAL库初始化PA0为推挽输出 GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // 第一步:必须开启GPIO端口的时钟! GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStruct.Pull = GPIO_NOPULL; // 推挽输出模式下,上下拉通常不需要 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; // 对于LED,低速即可 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 控制LED HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 高电平,LED灭(假设阴极接GPIO) HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); // 翻转引脚电平关键理解:
__HAL_RCC_GPIOA_CLK_ENABLE():这是新手最易忽略的一步!STM32的外设(包括GPIO)时钟默认是关闭的以省电,使用前必须开启。忘记开时钟会导致GPIO配置无效。GPIO_SPEED:这个配置影响的是IO口电平翻转的最大速度。对于驱动LED、按键扫描等低速应用,设为LOW可以减少高频噪声和功耗。对于驱动SPI、I2C等通信接口,则需要根据通信速率设置为HIGH或VERY_HIGH。- 推挽 vs 开漏:推挽输出能直接输出高/低电平,驱动能力强。开漏输出只能拉低或高阻态,需要外接上拉电阻才能输出高电平,常用于I2C等总线通信,可以实现“线与”功能。
4.2 调试必备:串口printf重定向
点亮LED后,我们如何知道程序运行到哪一步?变量值是多少?最直接的方法就是通过串口打印信息到电脑。这就需要重定向C库的printf函数到串口。
- 初始化一个串口(如USART1,PA9/PA10)。使用CubeMX配置非常方便,选择异步模式,设置波特率(如115200)。
- 重写
fputc或_write函数,让printf的输出流向串口发送函数。// 在 main.c 或专门的 debug.c 中 #include <stdio.h> // 如果使用微库 (MicroLib,Keil中可选),重写 fputc int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000); // huart1需是全局变量 return ch; } // 如果不使用微库,可能需要重写 _write 函数 - 在Keil中设置:
Options for Target->Target-> 勾选Use MicroLIB。MicroLIB是专为嵌入式优化的小型C库,对printf重定向支持更简单。 - 使用:现在你就可以在代码里任意使用
printf(“System Clock: %d Hz\n”, HAL_RCC_GetSysClockFreq());来调试了。
注意:
printf函数会占用大量时间和资源,尤其是在循环中打印浮点数 (%f)。在正式产品中应避免使用,或使用条件编译宏来控制。但在学习调试阶段,它是无可替代的利器。我曾用printf定位过一个因为栈空间设置过小导致的随机死机问题,通过在不同函数入口打印信息,很快发现是某个函数调用深度太大导致栈溢出。
5. 中断与时钟SysTick:让程序“学会”响应
单片机程序不能一直死循环 (while(1)),还需要及时响应外部事件(如按键按下)和处理定时任务(如定时闪烁)。这就需要中断和系统滴答定时器 (SysTick)。
5.1 外部中断 (EXTI) 处理按键
假设我们用PC13连接一个按键(STM32 Blue Pill板载按键),按下为低电平。我们配置其为下降沿触发中断。
- CubeMX配置:将PC13配置为
GPIO_EXTI13,模式为External Interrupt Mode with Falling edge trigger detection。 - 生成代码后,CubeMX会自动帮你在
stm32f1xx_it.c中生成中断服务函数EXTI15_10_IRQHandler()的框架。我们需要在其中添加清除中断标志和业务逻辑。
核心要点:必须清除中断挂起标志(void EXTI15_10_IRQHandler(void) { if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_13) != RESET) // 判断是否是PC13的中断 { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_13); // 必须清除中断标志! HAL_Delay(50); // 简单消抖,实际应用建议用定时器状态机 if(HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET) { // 确认按键按下,执行任务 printf("Key Pressed!\n"); HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); // 翻转LED } } }__HAL_GPIO_EXTI_CLEAR_IT),否则中断会不断触发,程序将卡死在中断函数里。这是中断编程的铁律。
5.2 系统滴答定时器 (SysTick) 实现精准延时
HAL_Delay()函数就是基于SysTick实现的。SysTick是一个24位的递减计数器,属于Cortex-M内核,因此所有基于该内核的芯片都有,可移植性好。
- 工作原理:
HAL_Init()会初始化SysTick,使其每1ms产生一次中断。HAL_Delay()函数就是通过查询一个由SysTick中断更新的计数器 (uwTick) 来实现阻塞延时。 - 阻塞 vs 非阻塞:
HAL_Delay()是阻塞式延时,在延时期间CPU空转,什么也做不了。这在主循环中要尽量避免,因为它会降低系统的响应性。 - 非阻塞延时技巧:我们可以利用
uwTick变量来实现非阻塞延时,这是编写高效主循环的基础。
这种基于时间戳比较的方法,是嵌入式系统中实现多任务时间调度的雏形。当你需要同时控制LED闪烁、按键检测、串口通信等多个有时间要求的任务时,非阻塞设计是唯一的选择。uint32_t lastToggleTime = 0; while (1) { // 非阻塞式LED闪烁,每500ms翻转一次 if (HAL_GetTick() - lastToggleTime >= 500) { lastToggleTime = HAL_GetTick(); HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0); } // 此处可以执行其他任务,如扫描状态、处理数据包 // 不会被 delay 阻塞 }
6. PWM输出实战:从呼吸灯到电机控制
PWM(脉冲宽度调制)是单片机控制领域的核心技术之一,用于控制LED亮度、电机速度、舵机角度等。STM32的定时器(TIM)功能强大,可以轻松产生PWM。
6.1 定时器与PWM基础概念
以通用定时器TIM2的通道1(对应PA0)为例,产生一个1kHz,占空比可调的PWM。
- 频率:PWM频率 = 定时器时钟源 / ((PSC + 1) * (ARR + 1))。
- 定时器时钟:如果APB1预分频器不为1,则TIM2的时钟是APB1时钟的2倍。我们之前配置APB1为36MHz,所以TIM2时钟为72MHz。
- 目标:1kHz频率。我们可以设置PSC=71,ARR=999。
- 计算:72,000,000 / ((71+1)*(999+1)) = 72,000,000 / 72,000 = 1000 Hz。
- 占空比:占空比 = (CCR / (ARR + 1)) * 100%。CCR是捕获/比较寄存器,值可以在0到ARR之间变化。通过修改CCR值,就能实时改变PWM的占空比。
6.2 CubeMX配置与代码实现
- CubeMX配置:
- 使能TIM2。
- 将通道1设置为
PWM Generation CH1。 - 在参数配置中,设置
Prescaler (PSC) = 71,Counter Period (ARR) = 999。此时Pulse (CCR)可以先设为500(即50%占空比)。 - 计算器会自动显示生成的频率(1kHz)。
- 生成代码后,在
main.c中启动PWM:HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 启动PWM输出 - 动态改变占空比,实现呼吸灯效果:
uint16_t pwmVal = 0; int8_t dir = 1; // 方向,1为增加,-1为减少 while (1) { // 非阻塞延时,每10ms调整一次占空比 if (HAL_GetTick() - lastChangeTime >= 10) { lastChangeTime = HAL_GetTick(); pwmVal += dir; __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwmVal); // 修改CCR值 if (pwmVal >= 1000) dir = -1; // 到达最大值,开始减小 if (pwmVal <= 0) dir = 1; // 到达最小值,开始增加 } // ... 其他任务 }__HAL_TIM_SET_COMPARE这个宏是HAL库提供的,用于安全地修改CCR寄存器的值。直接操作寄存器 (htim2.Instance->CCR1 = pwmVal) 也可以,但使用宏更规范。
6.3 进阶:互补PWM与死区插入
在控制直流电机或逆变器时,常需要一对互补的PWM信号(高电平和低电平交替),并且为了防止上下桥臂直通短路,需要在信号切换时插入一段两者都为低电平的“死区时间”。STM32的高级定时器(如TIM1, TIM8)支持硬件自动生成带死区的互补PWM,这是做电机驱动、数字电源必备的功能。配置时,在CubeMX的TIM参数配置中,找到Break and Dead Time选项,使能刹车和死区,并设置死区时间。硬件会自动处理,极大地简化了软件逻辑并提高了安全性。
7. ADC采样与DAC输出:连接模拟世界
单片机是数字世界的王者,但现实世界是模拟的。ADC(模数转换器)和DAC(数模转换器)是连接这两个世界的桥梁。
7.1 ADC单通道采样
STM32F103的12位ADC,采样率最高可达1MHz。以采样PA1引脚上的电压为例。
- CubeMX配置:
- 使能ADC1。
- 将通道IN1(对应PA1)设置为
Single-ended。 - 配置参数:
Resolution(分辨率)选12位,Scan Conversion Mode(扫描模式)选Disabled(单通道),Continuous Conversion Mode(连续转换)选Disabled(单次转换),DMA Continuous Requests先关闭。 - 在
NVIC Settings中使能ADC中断(如果需要中断方式读取)。
- 代码实现:
注意事项:// 启动一次转换并等待完成(阻塞式) uint16_t adcValue = 0; HAL_ADC_Start(&hadc1); // 启动转换 if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) // 等待转换完成,超时100ms { adcValue = HAL_ADC_GetValue(&hadc1); // 获取结果 } float voltage = (float)adcValue / 4095.0f * 3.3f; // 计算电压值,假设参考电压为3.3V printf("ADC Value: %d, Voltage: %.2f V\n", adcValue, voltage);- 采样时间:ADC对输入信号采样需要一定时间(
Sampling Time)。对于高阻抗信号源,需要增加采样时间以保证电容充电充分,否则采样值会偏低。可以在CubeMX中调整Sampling Time Common。 - 参考电压:ADC的转换结果是相对于参考电压 (
VREF+) 的。如果芯片有独立的VREF+引脚,务必接一个干净、稳定的电压源(如2.5V基准源),可以大幅提高精度。如果使用VDDA作为参考,则其稳定性决定了ADC精度。
- 采样时间:ADC对输入信号采样需要一定时间(
7.2 多通道与DMA传输
当需要快速、连续采样多个通道时,使用轮询或中断方式会消耗大量CPU资源。此时必须使用DMA(直接存储器访问)。
- CubeMX配置:在ADC配置中,开启扫描模式 (
Enabled),连续转换模式 (Enabled),并添加多个通道。然后在DMA Settings中添加一个DMA请求,模式设为Circular(循环模式),这样ADC会不停地采样,并通过DMA自动将数据搬运到你指定的数组中,完全无需CPU干预。 - 代码实现:
DMA是嵌入式高手必须掌握的技能,它能将CPU从繁重的数据搬运工作中解放出来,去处理更复杂的逻辑。在需要高速采样(如音频处理、振动分析)时,DMA是唯一的选择。#define ADC_BUFF_SIZE 100 uint16_t adcBuffer[ADC_BUFF_SIZE]; // DMA目标缓冲区 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcBuffer, ADC_BUFF_SIZE); // 启动ADC DMA传输 // 此后,adcBuffer数组会被DMA自动更新。你可以在主循环中直接读取这个数组进行数据处理。
7.3 DAC输出简单波形
STM32F103的DAC是12位的,可以将数字值转换为模拟电压输出。以PA4(DAC1_OUT1)为例,输出一个直流电压。
// 设置DAC输出值,范围0-4095,对应0-VDDA电压 uint16_t dacValue = 2048; // 中间值,约1.65V (假设VDDA=3.3V) HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dacValue); HAL_DAC_Start(&hdac1, DAC_CHANNEL_1); // 启动DAC输出结合定时器中断,不断更新DAC的输出值,就可以产生三角波、正弦波等简单波形。例如,在SysTick中断中,每1ms将dacValue增加一个固定步进,达到峰值后再递减,就能生成一个三角波。这对于信号发生器、音频测试等应用非常有用。
从最小系统的硬件基石,到工程模板的软件框架,再到GPIO、中断、定时器、ADC/DAC等核心外设的初步探索,我们完成了一次对STM32F103从硬件到软件的完整“上电”和“初体验”。每一个环节的深入理解,都能为后续更复杂的项目(如驱动显示屏、连接传感器、运行RTOS)打下坚实的基础。记住,嵌入式学习没有捷径,多动手、多思考、善用工具(CubeMX、调试器、示波器),遇到问题先查数据手册和参考手册,你就能从这些看似简单的“笔记”中,积累出解决实际工程问题的强大能力。