stm20260709-STM32CubeMX STM32F103RCT6 完整工程搭建图文学习笔记
stm20260709-STM32CubeMX STM32F103RCT6 完整工程搭建图文学习笔记
芯片: STM32F103RCT6 (LQFP64封装, Flash 256KB, RAM 48KB, Cortex-M3 72MHz)
工具: STM32CubeMX V6.x + MDK-ARM V5.32
日期: 2026年7月9日
前期准备
1. 安装 STM32CubeMX
- 从 ST 官网下载 STM32CubeMX 安装程序(当前版本 V6.x),安装路径建议默认即可。
- 安装过程中需要选择 Java 运行环境(CubeMX 内置 JRE,一般无需额外安装)。
- 首次启动后,点击
Help → Manage Embedded Software Packages,下载并安装 STM32F1 系列 Firmware Package(包含 HAL 库源码、启动文件等),版本建议 ≥ 1.8.x。
2. 安装 MDK-ARM (Keil uVision5)
- 从 Arm Keil 官网下载 MDK-ARM V5.32+ 安装程序。
- 安装完成后,打开 Pack Installer,下载 STM32F1xx_DFP(Device Family Pack),确保芯片支持包已安装。
- 如需使用 ST-Link 调试器,还需安装 ST-Link 驱动(STM32 ST-LINK Utility 或 STSW-LINK009)。
3. 硬件连接确认
| 项目 | 说明 |
|---|---|
| 调试接口 | ST-Link V2/V3,SWD 2线(SWDIO + SWCLK)+ GND + 3V3 |
| 串口调试 | USART3: PC10(TX) → USB-TTL RX, PC11(RX) → USB-TTL TX |
| 供电 | 3V3 稳压供电,或通过 ST-Link 3V3 输出 |
| 晶振 | 外部 HSE 8MHz 晶振(板载,无需额外连接) |
步骤1:新建项目 & 芯片选型
1.1 打开 CubeMX 新建项目
启动 STM32CubeMX,点击主界面 File → New Project 或直接点击首页的 ACCESS TO MCU SELECTOR 按钮,进入 MCU 芯片选择界面。
1.2 芯片选择
在 MCU Selector 页面,左侧过滤器中进行筛选:
| 过滤项 | 选择值 | 说明 |
|---|---|---|
| Series | STM32F1 | F1系列 |
| Line | STM32F103 | 中等性能线 |
| Package | LQFP64 | 64引脚封装 |
| Core | Cortex-M3 | 内核类型 |
筛选后在右侧芯片列表中找到 STM32F103RCT6,双击选中或右键选择 Start Project。
选中芯片后,CubeMX 会自动打开 Pinout & Configuration 视图,显示芯片引脚分配图。
步骤2:SYS 系统配置 & Debug 接口
2.1 进入 SYS 配置
在左侧 System Core 下拉菜单中,点击 SYS,进入系统配置面板。
2.2 Debug 模式设置
在 SYS 配置面板的 Debug 下拉框中,选择 Serial Wire(SWD 2线调试模式)。
为什么要选 Serial Wire?
- 默认 Debug =
No Debug时,芯片上电后 PA13(SWDIO) 和 PA14(SWCLK) 可被释放为普通 GPIO,但 一旦程序运行后将无法再通过 ST-Link 连接调试(俗称"锁芯片")。- 选择
Serial Wire后,PA13 和 PA14 被固定为调试功能引脚,始终可通过 ST-Link 进行下载和在线调试。JTAG (4 pins)模式占用 PA13/PA14/PA15/PB3/PB4 五个引脚,资源浪费较大,本项目不用。- 务必每次新建工程都设置 Debug = Serial Wire,否则烧录一次后可能无法再连接调试器!
2.3 Timebase Source 设置
SYS 面面中的 Timebase Source 默认为 SystemTick(SysTick 定时器)。如果操作系统(如 FreeRTOS)需要使用 SysTick,则应将 Timebase Source 改为其他硬件定时器(如 TIM1),避免与 OS 内核定时器冲突。本项目暂不使用 RTOS,保持默认 SystemTick 即可。
步骤3:RCC 时钟源配置
3.1 进入 RCC 配置
在左侧 System Core 下拉菜单中,点击 RCC,进入时钟源配置面板。
3.2 配置 HSE (外部高速时钟)
| 配置项 | 选择值 | 说明 |
|---|---|---|
| High Speed Clock (HSE) | Crystal/Ceramic Resonator |
使用板载 8MHz 外部晶振 |
| Low Speed Clock (LSE) | Disable |
本项目不使用外部低速晶振(32.768kHz RTC晶振) |
HSE 模式说明:
Crystal/Ceramic Resonator:使用外部晶振,需配置 OSC_IN/OSC_OUT 引脚(PD0/PD1),适用于板载8MHz晶振的场景。Bypass:外部时钟信号直接输入 OSC_IN(OSC_OUT 不用),适用于有源晶振或外部时钟源。Disable:不使用外部时钟,完全依赖 HSI 内部 8MHz RC 振荡器(精度较低 ±1%,不推荐用于需要精确时序的场景)。
选择 Crystal/Ceramic Resonator 后,PD0(OSC_IN) 和 PD1(OSC_OUT) 引脚会被自动标记为 RCC 功能。
步骤4:72MHz 时钟树配置
4.1 进入 Clock Configuration 视图
点击 CubeMX 顶部菜单栏的 Clock Configuration 标签页,进入时钟树图形化配置界面。

4.2 72MHz 时钟树详细配置
在时钟树界面上,按以下步骤逐级配置:
第1级:HSE 输入
| 节点 | 配置 | 值 |
|---|---|---|
| Input frequency | HSE | 8 MHz |
| PLL Source Mux | 选择 | HSE (不选 HSI) |
注意:PLL Source Mux 有两个选项:HSI 和 HSE。务必选择 HSE(8MHz外部晶振),因为 HSI 的 ±1% 精度不够,且通过 HSI 做PLL倍频时配置较为复杂。
第2级:PLL 倍频
| 节点 | 配置项 | 值 | 说明 |
|---|---|---|---|
| PLLM | Division factor | /1 | HSE 8MHz → PLL输入 8MHz |
| PLLN | Multiplication factor | ×9 | 8MHz × 9 = 72MHz |
| PLLP | Division factor | /2 (默认) | 但由于 PLLM=1, 实际 VCO 输出 = 8×9 = 72MHz, 经 /2 后为 36MHz,需要调整 |
⚠ 重要修正:STM32F103 PLL 倍频计算公式为:
PLLCLK = (HSE / PLLM) × PLLN
(注意:F1系列 PLLM 称为 PLLXTPRE,只有 /1 和 /2 两个选项;PLLN 就是倍频系数)正确配置方式:
- HSE = 8MHz
- PLLXTPRE (HSE divider for PLL entry) = Not divided (即 /1)
- PLL Mul (PLLN) = ×9
- PLLCLK = 8 × 9 = 72 MHz ✓
如果 PLLXTPRE 选了 /2,则 PLLCLK = (8/2) × 9 = 36MHz,不是72MHz!
CubeMX中的操作:在时钟树上,点击 HSE 分支到 PLL 之间的分频器图标,确保选择
Not divided(即不分频,÷1)。
第3级:系统时钟选择与分频
| 节点 | 配置项 | 值 |
|---|---|---|
| System Clock Mux | 选择 | PLLCLK |
| AHB Prescaler | /1 | HCLK = SYSCLK = 72 MHz |
| APB1 Prescaler | /2 | PCLK1 = 36 MHz (必须!APB1最高36MHz) |
| APB2 Prescaler | /1 | PCLK2 = 72 MHz |
为什么 APB1 必须 /2?
STM32F103 的 APB1 总线最高时钟为 36MHz,超过此频率将导致外设工作异常。当 APB1 分频系数 ≠ 1 时,挂载在 APB1 上的定时器(TIM2-TIM7)的时钟会自动×2,即 36×2 = 72MHz,这是定时器输入时钟的来源。
APB2 最高 72MHz,可不分频直接运行。
时钟树完整数据流:
HSE 8MHz → PLLXTPRE(/1) → PLL×9 → PLLCLK 72MHz → SYSCLK 72MHz
→ AHB(/1) → HCLK 72MHz (Cortex-M3内核时钟)
→ APB1(/2) → PCLK1 36MHz (USART2/3, SPI2/I2C1/2, TIM2-7)
→ APB2(/1) → PCLK2 72MHz (USART1, SPI1, ADC1, TIM1)
配置完成后,时钟树上的数值应全部显示为绿色(无红色报错),表示配置合法。
4.3 时钟安全系统 (CSS)
可选配置:在时钟树界面可勾选 Clock Security System (CSS),当 HSE 晶振失效时自动切换到 HSI 并产生 NMI 中断。本项目暂不启用。
步骤5:GPIO 引脚配置
5.1 项目 IO 分配表
根据报警项目硬件设计,STM32F103RCT6 的 GPIO 引脚分配如下:
| 功能分类 | 引脚 | GPIO | 模式 | 说明 |
|---|---|---|---|---|
| 4bit 硬件ID | PB0 | GPIO_Input | 上拉输入 | ID_bit0 |
| PB1 | GPIO_Input | 上拉输入 | ID_bit1 | |
| PB10 | GPIO_Input | 上拉输入 | ID_bit2 | |
| PB11 | GPIO_Input | 上拉输入 | ID_bit3 | |
| 3路按键 | PB3 | GPIO_Input | 上拉输入 | Key1 |
| PB4 | GPIO_Input | 上拉输入 | Key2 | |
| PB5 | GPIO_Input | 上拉输入 | Key3 | |
| 报警输出 | PB6 | GPIO_Output | 推挽输出 | Alarm_Out1 |
| PB7 | GPIO_Output | 推挽输出 | Alarm_Out2 | |
| 流水灯 | PC6 | GPIO_Output | 推挽输出 | LED1 |
| PC7 | GPIO_Output | 推挽输出 | LED2 | |
| PC8 | GPIO_Output | 推挽输出 | LED3 | |
| PC9 | GPIO_Output | 推挽输出 | LED4 | |
| 继电器 | PC13 | GPIO_Output | 推挽输出 | Relay1 |
| PD2 | GPIO_Output | 推挽输出 | Relay2 | |
| PC12 | GPIO_Output | 推挽输出 | Relay3 | |
| PA15 | GPIO_Output | 推挽输出 | Relay4 | |
| USART3 | PC10 | USART3_TX | 复用推挽 | 串口发送 |
| PC11 | USART3_RX | 复用输入 | 串口接收 | |
| 调试 | PA13 | SWDIO | SWD调试 | Serial Wire |
| PA14 | SWCLK | SWD调试 | Serial Wire | |
| ADC1 | PA0 | ADC1_IN0 | 模拟输入 | ADC通道0 |
| PA1 | ADC1_IN1 | 模拟输入 | ADC通道1 | |
| 内部 | ADC1_IN16 | 片内温度 | 温度传感器 | |
| 内部 | ADC1_IN17 | 内部参考电压 | Vrefint 1.2V |
5.2 在 CubeMX 中配置 GPIO
5.2.1 输入引脚配置(按键 + 硬件ID)
在芯片引脚图上,依次点击 PB0、PB1、PB3、PB4、PB5、PB10、PB11,选择 GPIO_Input。
配置完成后,在左侧 System Core → GPIO 中设置每个输入引脚的详细参数:
| 参数 | 值 | 说明 |
|---|---|---|
| GPIO mode | Input mode | 浮空/上拉/下拉可选 |
| GPIO Pull-up/Pull-down | Pull-up | 上拉,未按下时为高电平 |
| Maximum output speed | Low (默认) | 输入引脚不需要速度设置 |
5.2.2 输出引脚配置(LED + 继电器 + 报警)
在芯片引脚图上,依次点击以下引脚并选择 GPIO_Output:
- 流水灯: PC6, PC7, PC8, PC9
- 继电器: PC13, PD2, PC12, PA15
- 报警输出: PB6, PB7
在 System Core → GPIO 中设置输出引脚参数:
| 参数 | 值 | 说明 |
|---|---|---|
| GPIO mode | Output Push Pull | 推挽输出 |
| GPIO Pull-up/Pull-down | No pull-up and no pull-down | 输出无需上下拉 |
| Maximum output speed | Low | LED/继电器控制无需高速 |
| Initial state | Low (或根据硬件需求) | 初始低电平,LED熄灭/继电器断开 |
PA15 注意事项: PA15 是 JTDI 引脚,当 Debug 设置为 Serial Wire 时,PA15 已从 JTAG 释放,可作为 GPIO 使用。但需确保 SYS → Debug = Serial Wire(不是 JTAG),否则 PA15 被占用。
PB3/PB4 注意事项: PB3(JTDO) 和 PB4(JNTRST) 同理,Serial Wire 模式下这两个引脚也可作为 GPIO 使用。

步骤6:USART3 异步串口 + 全局中断配置
6.1 启用 USART3
在左侧 Connectivity 下拉菜单中,点击 USART3,进入串口配置面板。
在 Mode 下拉框中选择 Asynchronous(异步模式),即可启用 USART3 的 TX/RX 功能。

6.2 USART3 参数配置
在 Parameter Settings 标签页中,按以下参数配置:
| 参数 | 值 | 说明 |
|---|---|---|
| Baud Rate | 115200 | 常用波特率 |
| Word Length | 8 Bits | 8位数据位(含或不含校验) |
| Parity | None | 无校验位 |
| Stop Bits | 1 | 1位停止位 |
| Data Direction | Receive and Transmit | 全双工收发 |
| Over Sampling | 16 Samples | 16倍过采样(更稳定) |
8N1 说明: 8Bits + None Parity + 1 Stop Bit = 8N1,是最常用的串口通信格式。
当 Parity = None 时,Word Length = 8 Bits 实际数据宽度为 8bit;
当 Parity = Even/Odd 时,Word Length 需设为 9 Bits(8bit数据 + 1bit校验)。
配置完成后,PC10 和 PC11 引脚会自动被标记为 USART3_TX 和 USART3_RX。
6.3 使能 USART3 全局中断
在 USART3 配置面板中,切换到 NVIC Settings 标签页,勾选 USART3 global interrupt 的 Enabled 复选框。

为什么要使能全局中断?
HAL 库的串口中断接收模式 (HAL_UART_Receive_IT) 依赖于 USART 全局中断。如果不使能此中断,调用HAL_UART_Receive_IT()后数据到达时不会触发中断回调,接收功能完全失效。
这是初学者最容易遗漏的配置项!
6.4 USART3 中断接收代码开发
6.4.1 HAL_UART_Receive_IT 的"一次性"特性
⚠ 关键知识:
HAL_UART_Receive_IT()是一次性触发的,即调用一次只接收指定数量的字节后自动关闭中断接收。要实现持续接收,必须在回调函数中再次调用HAL_UART_Receive_IT()。
/* main.c - 在 main() 函数的 while(1) 之前启动中断接收 */
/* USER CODE BEGIN 2 */
// 启动 USART3 中断接收,每次接收 8 字节
HAL_UART_Receive_IT(&huart3, RxBuffer, 8);
/* USER CODE END 2 */
6.4.2 中断回调函数实现
/* main.c - 在 USER CODE BEGIN 4 区域内实现回调 */
/* USER CODE BEGIN 4 */
// USART3 接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3)
{
// 处理接收到的 8 字节数据
ProcessReceivedData(RxBuffer, 8);
// ⚠ 必须再次调用!否则下次数据到达时不会触发中断
HAL_UART_Receive_IT(&huart3, RxBuffer, 8);
}
}
/* USER CODE END 4 */
6.4.3 全局变量定义
/* main.c - USER CODE BEGIN PV 区域 */
/* USER CODE BEGIN PV */
uint8_t RxBuffer[8]; // USART3 中断接收缓冲区
/* USER CODE END PV */
6.4.4 串口发送封装
/* 用户自定义函数 - 可放在 USER CODE BEGIN 0 区域 */
/* USER CODE BEGIN 0 */
// USART3 发送字符串
void USART3_SendString(const char *str)
{
HAL_UART_Transmit(&huart3, (uint8_t *)str, strlen(str), HAL_MAX_DELAY);
}
// USART3 发送数据块
void USART3_SendData(uint8_t *data, uint16_t len)
{
HAL_UART_Transmit(&huart3, data, len, 100); // 超时100ms
}
/* USER CODE END 0 */
步骤7:ADC1 DMA 循环采集配置
7.1 启用 ADC1
在左侧 Analog 下拉菜单中,点击 ADC1,进入 ADC 配置面板。
在 IN0 和 IN1 的复选框勾选启用(对应 PA0 和 PA1 的外部模拟输入通道)。
同时,在 IN16(Temperature Sensor)和 IN17(Internal Reference Voltage)的复选框也需要勾选,以启用片内温度传感器和内部参考电压采集。

7.2 ADC1 参数详细配置
在 Parameter Settings 标签页中配置以下参数:
| 参数 | 值 | 说明 |
|---|---|---|
| Clock Prescaler | Asynchronous clock mode divided by 4 | ADC时钟不超过14MHz |
| Resolution | 12 bits | 12位分辨率(0~4095) |
| Data Alignment | Right | 数据右对齐 |
| Scan Conversion Mode | Enabled | 多通道必须开启扫描模式 |
| Continuous Conversion Mode | Enabled | 连续转换(ADC自动循环扫描所有通道) |
| Discontinuous Conversion Mode | Disabled | 不使用间断模式 |
| DMA Continuous Requests | Enabled | DMA连续请求(ADC每次转换完都触发DMA传输) |
| End Of Conversion Selection | EOC Flag at the end of all channel conversions | 所有通道转换完才触发EOC |
| Number Of Conversion | 4 | 4个通道:IN0, IN1, IN16, IN17 |
ScanConvMode 必须开启: 多通道采集时,ADC 会按通道排序依次转换(扫描),ScanConvMode=Enabled 是 DMA 多通道采集的前提条件。
ContinuousConvMode 必须开启: ADC完成一轮4通道扫描后自动开始下一轮,实现循环采集,无需软件反复触发。
DMAContinuousRequests 必须开启: 每次 ADC 转换完成都触发 DMA 请求,DMA 自动搬运数据到缓冲区,无需 CPU 参与。
7.2.1 通道排序配置(Rank)
在 Number Of Conversion = 4 下方,会显示 4 个 Rank 配置项:
| Rank | 通道 | 采样时间 | 说明 |
|---|---|---|---|
| Rank 1 | Channel 0 (PA0) | 55.5 Cycles | 外部模拟输入1 |
| Rank 2 | Channel 1 (PA1) | 55.5 Cycles | 外部模拟输入2 |
| Rank 3 | Channel 16 (Temperature Sensor) | 239.5 Cycles | 片内温度(采样时间要长!) |
| Rank 4 | Channel 17 (Internal Reference Voltage) | 239.5 Cycles | 内部参考电压(采样时间要长!) |
⚠ 温度传感器和内部参考电压的采样时间必须 ≥ 17.1μs!
STM32F1 的片内温度传感器和参考电压从使能到稳定需要约 17.1μs 的启动时间,采样周期数必须足够长。
当 ADC 时钟为 12MHz 时,239.5 Cycles ≈ 239.5/12MHz ≈ 19.96μs > 17.1μs,满足要求。
55.5 Cycles ≈ 55.5/12MHz ≈ 4.6μs,用于外部通道足够(外部信号源阻抗较低时)。
7.3 DMA 配置
在 ADC1 配置面板中,切换到 DMA Settings 标签页,点击 Add 按钮添加一条 DMA 通道:
| 参数 | 值 | 说明 |
|---|---|---|
| DMA Request | ADC1 | 自动关联 |
| Stream / Channel | DMA1_Channel1 | ADC1 固定使用 DMA1 Channel1 |
| Direction | Peripheral To Memory | ADC外设 → 内存缓冲区 |
| Mode | Circular | 循环模式(缓冲区写满后自动回到起始地址继续写) |
| Increment | Memory: Enabled | 内存地址递增(每个通道数据写入不同位置) |
| Peripheral: Disabled | 外设地址不变(ADC数据寄存器地址固定) | |
| Data Width | Peripheral: Half Word (16 Bits) | ADC 12位数据,半字传输 |
| Memory: Half Word (16 Bits) | 与外设宽度一致 |

DMA Circular 模式说明:
Normal模式:DMA 传输指定数量的数据后自动停止,需要软件重新启动。Circular模式:DMA 传输完指定数量后自动回到缓冲区起始位置继续传输,实现"永不停歇"的循环采集。- 本项目使用 Circular 模式,配合 ADC 的 ContinuousConvMode 和 DMAContinuousRequests,实现 ADC→DMA→缓冲区的全自动循环采集。
- 缓冲区大小必须是 通道数 × 2(因为每个通道占一个 Half Word = 2字节),4通道 × 2字节 = 8字节最小缓冲区。
7.4 ADC1 DMA 采集代码开发
7.4.1 缓冲区定义
/* main.c - USER CODE BEGIN PV 区域 */
/* USER CODE BEGIN PV */
#define ADC_DMA_BUF_SIZE 4 // 4个通道
// ADC DMA 双缓冲区(可选:双缓冲防数据撕裂)
uint16_t ADCRes[ADC_DMA_BUF_SIZE]; // DMA 当前写入缓冲区
uint16_t ADCResCopy[ADC_DMA_BUF_SIZE]; // 软件拷贝缓冲区(用于安全读取)
/* USER CODE END PV */
7.4.2 启动 ADC DMA 采集
/* main.c - USER CODE BEGIN 2 区域 */
/* USER CODE BEGIN 2 */
// 启动 ADC1 DMA 循环采集
// 注意:缓冲区大小 = 通道数 × 2字节,传给函数的是字节大小
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&ADCRes, ADC_DMA_BUF_SIZE);
/* USER CODE END 2 */
⚠ 关键注意:
HAL_ADC_Start_DMA()的第三个参数Length是 Half Word 的数量(即数据项数),不是字节总数!
4 通道 = 4 个 Half Word = 4 个 uint16_t = 8 字节内存,但 Length 传 4。如果误传 8(字节大小),DMA 会传输 8 个 Half Word = 16 字节,超出缓冲区,导致内存溢出!
7.4.3 安全读取 ADC 数据
/* main.c - while(1) 主循环中 */
/* USER CODE BEGIN 3 */
// 方法1:直接读取(适用于单缓冲,可能存在数据撕裂风险)
uint16_t adc_ch0 = ADCRes[0]; // Channel 0 (PA0)
uint16_t adc_ch1 = ADCRes[1]; // Channel 1 (PA1)
uint16_t adc_temp = ADCRes[2]; // Channel 16 (Temperature)
uint16_t adc_vref = ADCRes[3]; // Channel 17 (Vrefint)
// 方法2:拷贝后读取(更安全,防止 DMA 正在写入时读取到半更新数据)
memcpy(ADCResCopy, ADCRes, sizeof(ADCRes));
uint16_t adc_ch0_safe = ADCResCopy[0];
// 计算实际电压值 (参考电压 Vdda = 3.3V)
float voltage_ch0 = (float)adc_ch0 / 4095.0f * 3.3f;
// 计算芯片内部温度 (STM32F1公式)
// V25 = 1.43V (25°C时温度传感器输出), Avg_Slope = 4.3mV/°C
float temperature = ((float)adc_temp / 4095.0f * 3.3f - 1.43f) / 0.0043f + 25.0f;
/* USER CODE END 3 */
7.4.4 ADC DMA 中断回调(可选)
/* main.c - USER CODE BEGIN 4 区域 */
/* USER CODE BEGIN 4 */
// DMA 半传输完成回调(Half Transfer)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
// 可在此拷贝前半缓冲区数据
}
// DMA 传输完成回调(Full Transfer)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
// 一轮4通道全部采集完毕,可在此做数据处理标记
ADCDataReady = 1; // 数据就绪标志
}
}
/* USER CODE END 4 */
步骤8:NVIC 中断分组配置
8.1 NVIC 优先级分组理论
STM32F103 的 NVIC 支持 4 位优先级(2^4 = 16 级),这 4 位需要在 抢占优先级(Preemption Priority) 和 次级优先级(Sub Priority / 响应优先级) 之间分配。
CubeMX 中通过 System Core → NVIC → Priority Group 下拉框选择分组方式:
| 分组 | 抢占位数 | 次级位数 | 抢占级数 | 次级级数 | 说明 |
|---|---|---|---|---|---|
| Group 0 | 0 bit | 4 bit | 0 级 | 16 级 | 无抢占,全部靠响应优先级排队 |
| Group 1 | 1 bit | 3 bit | 2 级 | 8 级 | |
| Group 2 | 2 bit | 2 bit | 4 级 | 4 级 | 常用,平衡抢占和响应 |
| Group 3 | 3 bit | 1 bit | 8 级 | 2 级 | |
| Group 4 | 4 bit | 0 bit | 16 级 | 0 级 | 本项目使用 |
8.2 本项目选择 Group 4
本项目配置 NVIC 优先级分组为 Group 4(4位全给抢占优先级,无次级优先级)。
为什么选 Group 4?
- 报警项目中断源不多(USART3 + ADC DMA),但需要确保高优先级中断能立即打断低优先级中断(抢占)。
- Group 4 提供 16 级抢占优先级(0~15),数值越小优先级越高。
- 无次级优先级意味着:同一抢占优先级的中断不能互相抢占,按硬件默认顺序排队。
- 对于简单的嵌入式项目,Group 4 足够清晰且配置简单。
8.3 各中断优先级分配
| 中断源 | 抢占优先级 | 说明 |
|---|---|---|
| USART3 global interrupt | 6 | 串口通信,中等优先级 |
| DMA1 Channel1 interrupt | 5 | ADC DMA传输完成,略高于串口 |
| TIMx (如使用) | 10 | 定时器,低优先级 |
优先级分配原则:
- DMA > 串口 > 定时器 > 其他: DMA传输需要及时响应避免数据丢失,串口通信其次。
- 0 为最高优先级,15 为最低优先级(Group 4 中)。
- 不要把所有中断都设为最高优先级(0)!否则失去分组的意义,且可能导致低优先级任务长期得不到执行。
- SysTick 中断默认优先级为 15(最低),不应随意提高。

步骤9:Project Manager 工程导出配置
9.1 Project 设置
点击 CubeMX 顶部菜单栏的 Project Manager 标签页,进入工程管理配置界面。
在 Project 标签页配置:
| 配置项 | 值 | 说明 |
|---|---|---|
| Project Name | STM32F103RCT6_Alarm | 工程名称(英文,无空格) |
| Project Location | F:/upstudy/stu_STM32103/STM32F103RCT6_Alarm | 工程存放路径(自定义) |
| Toolchain / IDE | MDK-ARM V5 | Keil uVision5 工程格式 |
Toolchain 选择说明:
MDK-ARM V5:生成 Keil uVision5 工程(.uvprojx),最常用。MDK-ARM:生成 Keil uVision4 工程(.uvproj),旧格式。STM32CubeIDE:生成 STM32CubeIDE 工程(Eclipse IDE)。Makefile:生成 Makefile 工程(适用于 GCC 工具链)。IAR:生成 IAR Embedded Workbench 工程。
9.2 Code Generator 设置
在 Code Generator 标签页配置:
| 配置项 | 值 | 说明 |
|---|---|---|
| Generate peripheral initialization as a pair of .c/.h files | ✓ 勾选 | 每个外设生成独立的 .c/.h 文件,代码结构清晰 |
| Keep User Code when re-generating | ✓ 勾选 | 重新生成代码时保留 USER CODE 区域内的用户代码 |
| Set all free pins as analog | ✓ 勾选 | 未使用的引脚设为模拟模式,降低功耗和干扰 |
这三个选项非常关键!
Generate peripheral initialization as a pair of .c/.h files: 不勾选时所有初始化代码堆在 main.c 里,非常混乱。勾选后每个外设有独立文件(如usart.c/h,adc.c/h,gpio.c/h),便于管理和维护。Keep User Code when re-generating: CubeMX 重新生成代码时,只更新框架代码,USER CODE BEGIN/END 区域内的用户代码会被完整保留。不勾选则重新生成会覆盖所有修改!Set all free pins as analog: 未使用的 GPIO 设为模拟输入模式,可显著降低功耗(GPIO浮空输入会有漏电流),同时减少外部干扰耦合。
9.3 Advanced Settings(可选)
在 Advanced Settings 标签页,可以选择每个外设使用的驱动库类型:
| 外设 | 选项 | 建议 |
|---|---|---|
| GPIO | HAL / LL | HAL(本项目用HAL) |
| USART3 | HAL / LL | HAL |
| ADC1 | HAL / LL | HAL |
HAL vs LL:
- HAL (Hardware Abstraction Layer):高层抽象库,API 简单易用,但代码体积较大、执行效率较低。适合初学者和一般项目。
- LL (Low Layer):底层直接操作寄存器库,代码精简、执行高效,但需要深入理解寄存器含义。适合对性能和代码体积有极致要求的项目。
- 本项目统一使用 HAL 库。
9.4 生成代码
配置完成后,点击 CubeMX 顶部工具栏的 ⚙ GENERATE CODE 按钮(或 Project → Generate Code),CubeMX 将自动生成完整的 MDK-ARM V5 工程。
生成成功后会弹出提示框,可直接点击 Open Project 打开 Keil 工程,或点击 Open Folder 查看工程目录。
步骤10:业务代码开发全套流程
10.1 HAL 库 USER CODE 保护机制
CubeMX 生成的代码文件中,所有允许用户添加代码的位置都用 USER CODE BEGIN xxx 和 USER CODE END xxx 标记包围:
/* USER CODE BEGIN 0 */
// ← 你的代码写在这里,重新生成时不会被覆盖
/* USER CODE END 0 */
void MX_GPIO_Init(void)
{
// CubeMX 生成的初始化代码
// ⚠ 不要在这里修改!重新生成会被覆盖!
/* USER CODE BEGIN 1 */
// ← 你可以在这里添加代码
/* USER CODE END 1 */
}
⚠ 铁律:所有业务代码必须写在 USER CODE BEGIN/END 之间!
在这些标记之外写的代码,下次 CubeMX 重新生成时会被无警告地完全覆盖删除!
这是初学者最容易犯的错误,导致辛苦写的业务代码一键丢失。
10.2 完整业务代码框架
以下展示一个典型的 main.c 业务代码结构,所有代码均在 USER CODE 区域内:
/* Includes ----------------------------------- */
#include "main.h"
#include "usart.h"
#include "adc.h"
#include "gpio.h"
/* USER CODE BEGIN Includes */
#include <string.h>
#include <stdio.h>
/* USER CODE END Includes */
/* Private variables ------------------------- */
/* USER CODE BEGIN PV */
#define ADC_DMA_BUF_SIZE 4
#define UART_RX_BUF_SIZE 8
uint16_t ADCRes[ADC_DMA_BUF_SIZE]; // ADC DMA 缓冲区
uint8_t RxBuffer[UART_RX_BUF_SIZE]; // USART3 接收缓冲区
uint8_t ADCDataReady = 0; // ADC 数据就绪标志
/* USER CODE END PV */
/* Private function prototypes --------------- */
/* USER CODE BEGIN PFP */
void USART3_SendString(const char *str);
void ProcessReceivedData(uint8_t *data, uint16_t len);
/* USER CODE END PFP */
/* USER CODE BEGIN 0 */
// USART3 发送字符串封装
void USART3_SendString(const char *str)
{
HAL_UART_Transmit(&huart3, (uint8_t *)str, strlen(str), HAL_MAX_DELAY);
}
// 处理 USART3 接收数据
void ProcessReceivedData(uint8_t *data, uint16_t len)
{
// 示例:回显接收到的数据
HAL_UART_Transmit(&huart3, data, len, 100);
}
/* USER CODE END 0 */
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init(); // ⚠ DMA 必须在 ADC 之前初始化!
MX_ADC1_Init();
MX_USART3_UART_Init();
/* USER CODE BEGIN 2 */
// 启动 ADC1 DMA 循环采集
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)&ADCRes, ADC_DMA_BUF_SIZE);
// 启动 USART3 中断接收
HAL_UART_Receive_IT(&huart3, RxBuffer, UART_RX_BUF_SIZE);
// 开机提示
USART3_SendString("System Ready\r\n");
/* USER CODE END 2 */
while (1)
{
/* USER CODE BEGIN 3 */
// 流水灯演示
static uint32_t ledTick = 0;
if (HAL_GetTick() - ledTick >= 500) // 每500ms切换
{
ledTick = HAL_GetTick();
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_6); // LED1
}
// 按键检测(简化示例,无消抖)
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET)
{
// Key1 按下
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); // Alarm_Out1
}
// ADC 数据处理
if (ADCDataReady)
{
ADCDataReady = 0;
float voltage = (float)ADCRes[0] / 4095.0f * 3.3f;
// 可在此发送 ADC 数据到串口
}
/* USER CODE END 3 */
}
}
/* USER CODE BEGIN 4 */
// USART3 接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3)
{
ProcessReceivedData(RxBuffer, UART_RX_BUF_SIZE);
// ⚠ 必须再次调用!
HAL_UART_Receive_IT(&huart3, RxBuffer, UART_RX_BUF_SIZE);
}
}
// ADC DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
if (hadc->Instance == ADC1)
{
ADCDataReady = 1;
}
}
/* USER CODE END 4 */
10.3 DMA 初始化顺序问题
⚠ 极重要!DMA 必须在 ADC 之前初始化!
CubeMX 生成的 main.c 中,函数调用顺序为:MX_GPIO_Init(); MX_DMA_Init(); ← 必须在此位置 MX_ADC1_Init(); ← DMA 已就绪,ADC 才能正常使用 DMA MX_USART3_UART_Init();如果 MX_DMA_Init() 在 MX_ADC1_Init() 之后,ADC DMA 配置会失败,
HAL_ADC_Start_DMA()返回 HAL_ERROR。
CubeMX V6.x 默认已自动处理此顺序,但手动修改 main.c 时务必注意!
10.4 按键消抖代码示例
/* USER CODE BEGIN 0 */
// 带消抖的按键检测函数
// 返回:1 = 按键按下确认,0 = 未按下或抖动中
uint8_t Key_Scan(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin)
{
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 第一次检测:低电平
{
HAL_Delay(20); // 消抖延时20ms
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 第二次确认
{
// 等待按键释放
while (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET);
HAL_Delay(20); // 释放消抖
return 1; // 确认按下
}
}
return 0; // 未按下
}
/* USER CODE END 0 */
消抖原理: 机械按键在按下/释放瞬间会产生 5~20ms 的抖动信号(电平快速跳变),如果不消抖,一次按键可能被识别为多次。
消抖方法:检测到低电平后延时 20ms 再次确认,确认后等待释放再延时 20ms。
HAL_Delay() 的局限: 在中断回调中不能使用 HAL_Delay()(它依赖 SysTick 中断,在中断中调用会导致死锁)。中断中的按键处理应使用定时器消抖方案。
10.5 工程编译与烧录
- 编译: 在 Keil 中点击
Project → Build Target(F7),确认 0 Error, 0 Warning。 - 烧录: 点击
Flash → Download(F8),ST-Link 将程序烧录到芯片 Flash。 - 调试: 点击
Debug → Start/Stop Debug Session(Ctrl+F5),进入在线调试模式。
附录:常见问题与避坑指南
Q1: 烧录一次后 ST-Link 无法连接?
原因: SYS → Debug 设置为 No Debug,程序运行后 PA13/PA14 被释放为 GPIO,调试器无法连接。
解决:
- 方法1:按芯片复位按钮(RESET)的同时点击 Keil 烧录,可能可以抢在程序初始化 GPIO 之前连接。
- 方法2:使用 STM32 ST-LINK Utility,按住复位 → Connect → Erase Full Chip → 烧录空程序。
- 方法3:从源头预防:每次新建 CubeMX 工程,SYS → Debug =
Serial Wire!
Q2: USART3 中断接收不工作?
常见原因:
- NVIC 中 USART3 global interrupt 未勾选 Enabled → 使能中断。
HAL_UART_Receive_IT()未在 main() 中调用 → 启动中断接收。- 回调函数中未再次调用
HAL_UART_Receive_IT()→ 一次性特性,只触发一次就停止。
Q3: ADC DMA 采集数据全为 0?
常见原因:
- MX_DMA_Init() 在 MX_ADC1_Init() 之后调用 → DMA 未就绪,ADC DMA 配置失败。
- 温度传感器通道未使能:需要先调用
HAL_ADCEx_Calibration_Start()校准,并确保 TSVREFE 位已使能(CubeMX 勾选 IN16/IN17 后自动处理)。 - ADC 采样时间不足:温度传感器采样时间必须 ≥ 17.1μs。
HAL_ADC_Start_DMA()的 Length 参数传了字节大小而非 Half Word 数量 → 缓冲区溢出。
Q4: CubeMX 重新生成代码后业务代码丢失?
原因: 业务代码写在了 USER CODE BEGIN/END 标记之外。
解决: 严格遵守规则,所有用户代码只写在 USER CODE BEGIN xxx ~ USER CODE END xxx 之间。CubeMX 生成时只更新标记之外的框架代码。
Q5: PA15 / PB3 / PB4 作为 GPIO 不工作?
原因: 这三个引脚默认为 JTAG 功能(JTDO/JNTRST/JTDI),需要设置 SYS → Debug = Serial Wire 才能释放 PA15 和 PB3/PB4(PB4 在 SWD 模式下仍需注意)。
解决: 确保 SYS → Debug = Serial Wire,不要选 JTAG (4 pins)。
附录:CubeMX 配置速查表
| 配置项 | 值 | 菜单路径 |
|---|---|---|
| 芯片 | STM32F103RCT6 | MCU Selector |
| Debug | Serial Wire | System Core → SYS |
| HSE | Crystal/Ceramic Resonator | System Core → RCC |
| PLL Mul | ×9 | Clock Configuration |
| SYSCLK | 72 MHz (PLLCLK) | Clock Configuration |
| AHB | /1 → 72MHz | Clock Configuration |
| APB1 | /2 → 36MHz | Clock Configuration |
| APB2 | /1 → 72MHz | Clock Configuration |
| USART3 Mode | Asynchronous | Connectivity → USART3 |
| USART3 Baud | 115200 8N1 | Connectivity → USART3 → Parameter Settings |
| USART3 IRQ | Enabled | Connectivity → USART3 → NVIC Settings |
| ADC1 Channels | IN0, IN1, IN16, IN17 | Analog → ADC1 |
| ADC1 ScanConv | Enabled | Analog → ADC1 → Parameter Settings |
| ADC1 ContinuousConv | Enabled | Analog → ADC1 → Parameter Settings |
| ADC1 DMAContinuous | Enabled | Analog → ADC1 → Parameter Settings |
| ADC1 DMA Mode | Circular | Analog → ADC1 → DMA Settings |
| NVIC Group | Group 4 (4bit preempt) | System Core → NVIC |
| Toolchain | MDK-ARM V5 | Project Manager → Project |
| Periph .c/.h | ✓ Generate pairs | Project Manager → Code Generator |
| Keep User Code | ✓ Keep | Project Manager → Code Generator |
| Free pins analog | ✓ Set analog | Project Manager → Code Generator |
更多推荐

所有评论(0)