附录A CubeMX编程指南
A 附录 A:CubeMX 编程指南¶
A.1 本章知识导图¶
图 A-1
A.2 CubeMX 简介与安装¶
A.2.1 工具定位¶
STM32CubeMX 是 ST 官方提供的图形化初始化代码生成工具。其核心作用是:
用鼠标点选完成芯片所有外设的初始化配置,然后一键生成可直接编译的 C 工程框架,开发者只需在框架中填写业务逻辑。
CubeMX 解决的核心痛点:
表 A-1 A.2.1 工具定位
| 痛点 | 传统手写方式 | CubeMX 方式 |
|---|---|---|
| 时钟树配置 | 手动计算 PLL,易出错 | 填目标频率,自动计算 |
| 引脚复用冲突 | 翻数据手册逐一核查 | 图形界面实时检测并高亮冲突 |
| 外设初始化代码 | 手写数十行结构体赋值 | 图形配置后全部自动生成 |
| FreeRTOS 移植 | 手动复制源码、修改配置 | 勾选即可,自动配置调度器 |
上表对 A.2.1 工具定位中各方案的特性进行了横向对比,便于读者根据实际需求选择最合适的技术路线。
A.2.2 安装方式¶
CubeMX 提供两种使用方式,功能完全相同:
图 A-2 A.2.2 安装方式
本课程推荐使用 STM32CubeIDE,因为它将 CubeMX 配置、代码编辑、编译、烧录、调试集成在同一窗口,无需在多个软件间切换。
A.2.3 主界面布局¶
STM32CubeMX(或 CubeIDE 的 .ioc 编辑界面)分为五个主要区域:
图 A-3 A.2.3 主界面布局
A.3 创建工程的完整流程¶
以 Blue Pill(STM32F103C8T6)LED 闪烁 为例,演示从零创建一个 CubeMX 工程的六个步骤。
A.3.1 步骤① 选择目标芯片¶
打开 STM32CubeIDE → File → New → STM32 Project → 在芯片选型界面:
图 A-4 A.3.1 步骤① 选择目标芯片
A.3.2 步骤② 系统配置(SYS / RCC)¶
在 Pinout & Configuration 标签页左侧的 System Core 类别中:
配置 SYS(调试接口):
图 A-5 A.3.2 步骤② 系统配置(SYS / RCC)
配置 RCC(时钟源):
图 A-6 A.3.2 步骤② 系统配置(SYS / RCC)
完成后,芯片图上 PC14/PC15(晶振引脚)和 PA13/PA14(SWD 引脚)会自动变为绿色,无需手动分配。
A.3.3 步骤③ 引脚功能分配¶
在芯片引脚图上,鼠标左键点击 PC13 引脚 → 从弹出菜单中选择 GPIO_Output:
图 A-7 A.3.3 步骤③ 引脚功能分配
A.3.4 步骤④ 时钟树配置¶
切换到 Clock Configuration 标签页:
图 A-8 A.3.4 步骤④ 时钟树配置
A.3.5 步骤⑤ 外设参数配置¶
对于本示例(LED 闪烁),GPIO 已在步骤③配置完毕。若有其他外设(如 USART),在 Pinout & Configuration 左侧列表中展开对应类别进行配置(详见第 4 节)。
A.3.6 步骤⑥ 工程设置与代码生成¶
切换到 Project Manager 标签页:
图 A-9 A.3.6 步骤⑥ 工程设置与代码生成
A.4 生成代码的结构与编写规范¶
A.4.1 工程目录结构¶
CubeMX 生成的工程具有固定目录结构:
图 A-10 A.4.1 工程目录结构
规则:只在
Core/Src/和Core/Inc/中编写用户代码,Drivers/目录内容不要手动修改。
A.4.2 main.h 中的引脚宏定义¶
CubeMX 根据 User Label 在 main.h 中自动生成宏定义:
/* main.h(CubeMX 自动生成,勿手动修改此区域) */
#define LED_Pin GPIO_PIN_13 /* 来自 User Label: LED */
#define LED_GPIO_Port GPIOC
在 main.c 中使用宏定义而非硬编码引脚,好处:
/* 推荐写法(使用宏定义)*/
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
/* 不推荐写法(硬编码,电路改动后需逐处修改)*/
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
A.4.3 USER CODE BEGIN / END 编写规范¶
CubeMX 生成的 main.c 中遍布成对的注释标记,用户代码必须写在标记之间:
int main(void)
{
/* USER CODE BEGIN 1 */
/* 在 HAL_Init 之前的变量声明或早期初始化 */
uint32_t tick_count = 0;
/* USER CODE END 1 */
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* GPIO 初始化完成后,主循环之前的一次性操作 */
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); /* LED 灭 */
/* USER CODE END 2 */
while (1)
{
/* USER CODE BEGIN WHILE */
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
/* USER CODE END 3 */
}
}
表 A-2 常见 USER CODE 区域说明:
| 区域标记 | 位置 | 适合写什么 |
|---|---|---|
USER CODE BEGIN Includes |
头文件区 | 添加自定义头文件 |
USER CODE BEGIN PD |
宏定义区 | 添加 #define 常量 |
USER CODE BEGIN PV |
全局变量区 | 声明全局变量 |
USER CODE BEGIN 0 |
函数实现前 | 定义辅助函数 |
USER CODE BEGIN 1 |
main 开头 | 局部变量声明 |
USER CODE BEGIN 2 |
初始化后 | 一次性启动逻辑 |
USER CODE BEGIN WHILE |
while(1) 内 | 主循环业务逻辑 |
⚠️ 警告:写在
USER CODE BEGIN/END之外的代码,在下次执行 "Generate Code" 时会被 CubeMX 覆盖删除。
A.4.4 中断回调函数的编写规范¶
HAL 库采用弱符号回调(weak callback)机制:HAL 驱动内部定义了 __weak 修饰的空回调函数,用户在 USER CODE BEGIN 0 区域重写同名函数即可覆盖:
/* 在 main.c 的 USER CODE BEGIN 0 区域重写 */
/* GPIO 外部中断回调(按键按下触发)*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == BTN_Pin)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
/* 定时器溢出回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
A.5 常用外设配置实战¶
A.5.1 GPIO — 输出与输入¶
LED 输出(推挽输出):
CubeMX 配置:PC13 → GPIO_Output,Mode = Output Push Pull,Label = LED
/* 主循环中控制 LED */
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET); /* 低电平 → 亮 */
HAL_Delay(500);
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET); /* 高电平 → 灭 */
HAL_Delay(500);
/* 或使用翻转,更简洁 */
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
HAL_Delay(500);
按键输入(上拉输入):
CubeMX 配置:PA0 → GPIO_Input,Pull = Pull-up,Label = BTN
/* 轮询方式读取按键 */
if (HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET)
{
/* 按键按下(低电平有效,因为接了上拉) */
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
}
else
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
}
A.5.2 USART — 串口通信¶
CubeMX 配置步骤:
图 A-11 A.5.2 USART — 串口通信
生成代码中自动出现 MX_USART1_UART_Init(),用户调用 HAL API:
/* USER CODE BEGIN Includes */
#include <string.h> /* for strlen */
/* USER CODE END Includes */
/* USER CODE BEGIN 2 */
char msg[] = "Hello from STM32!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
/* USER CODE END 2 */
/* 接收单字节(阻塞模式)*/
uint8_t rx_byte;
HAL_UART_Receive(&huart1, &rx_byte, 1, HAL_MAX_DELAY);
/* 接收完成后用中断回调(非阻塞,推荐)*/
HAL_UART_Receive_IT(&huart1, &rx_byte, 1); /* 启动中断接收 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) /* 接收完成回调 */
{
if (huart->Instance == USART1)
{
/* 处理 rx_byte,然后重新启动接收 */
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
}
}
A.5.3 TIM — 定时器与 PWM¶
基本定时(产生周期性中断):
图 A-12 A.5.3 TIM — 定时器与 PWM
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim2); /* 启动定时器中断 */
/* USER CODE END 2 */
/* 定时器溢出回调(每 1 秒触发一次)*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
PWM 输出(控制舵机 / LED 亮度):
图 A-13
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); /* 启动 PWM 输出 */
/* USER CODE END 2 */
/* 修改占空比(舵机位置控制)*/
/* 脉宽 1ms(0°) ~ 2ms(180°),对应 ARR=19999 时,CCR = 1000 ~ 2000 */
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1500); /* 中间位置 90° */
A.5.4 I2C — 总线通信¶
图 A-14 A.5.4 I2C — 总线通信
/* 向从设备地址 0x68(如 MPU6050)发送数据 */
uint8_t data[2] = {0x6B, 0x00}; /* 寄存器地址,数据 */
HAL_I2C_Master_Transmit(&hi2c1, 0x68 << 1, data, 2, HAL_MAX_DELAY);
/* 从从设备读取 6 字节 */
uint8_t buf[6];
HAL_I2C_Master_Receive(&hi2c1, 0x68 << 1, buf, 6, HAL_MAX_DELAY);
/* 读写指定寄存器(Mem 方式,更常用)*/
HAL_I2C_Mem_Read(&hi2c1, 0x68 << 1, /* 设备地址 */
0x3B, /* 寄存器地址(加速度 X 高字节)*/
I2C_MEMADD_SIZE_8BIT,
buf, 6, HAL_MAX_DELAY);
A.5.5 ADC — 模拟信号采集¶
图 A-15 A.5.5 ADC — 模拟信号采集
/* USER CODE BEGIN 2 */
HAL_ADC_Start(&hadc1); /* 启动 ADC 转换 */
/* USER CODE END 2 */
/* 读取 ADC 值(阻塞轮询)*/
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint32_t adc_val = HAL_ADC_GetValue(&hadc1); /* 12位,0 ~ 4095 */
/* 换算为电压(参考电压 3.3V)*/
float voltage = adc_val * 3.3f / 4095.0f;
A.5.6 FreeRTOS 多任务配置¶
2.5.6.1 启用 FreeRTOS¶
在 CubeMX 中:Middleware and Software Packs → FreeRTOS → CMSIS_V2
图 A-16 启用 FreeRTOS
⚠️ 启用 FreeRTOS 后,CubeMX 会自动将 HAL 时基从 SysTick 改为 TIM(如 TIM1),因为 FreeRTOS 自身需要独占 SysTick。
2.5.6.2 创建任务¶
在 CubeMX 的 FreeRTOS 配置页 → Tasks and Queues → Add Task:
图 A-17 创建任务
生成代码后,在 freertos.c 的 USER CODE BEGIN 区填写任务逻辑:
/* freertos.c - USER CODE BEGIN Header_StartLedTask */
void StartLedTask(void *argument)
{
/* USER CODE BEGIN StartLedTask */
for (;;)
{
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
osDelay(500); /* FreeRTOS 延时:让出 CPU,其他任务可以运行 */
}
/* USER CODE END StartLedTask */
}
2.5.6.3 任务间通信¶
信号量(二值信号量,用于任务同步):
在 CubeMX → FreeRTOS → Timers & Semaphores → Add Binary Semaphore,命名为 btnSemaphore。
/* 中断回调中释放信号量(通知任务有按键事件)*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == BTN_Pin)
{
osSemaphoreRelease(btnSemaphoreHandle);
}
}
/* 按键处理任务:等待信号量 */
void StartBtnTask(void *argument)
{
for (;;)
{
osSemaphoreAcquire(btnSemaphoreHandle, osWaitForever); /* 阻塞等待 */
HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
}
}
消息队列(用于传递数据):
在 CubeMX → FreeRTOS → Timers & Semaphores → Add Queue,命名 adcQueue,长度 4,Item Size uint32_t。
/* ADC 采集任务:发送数据到队列 */
void StartAdcTask(void *argument)
{
for (;;)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint32_t val = HAL_ADC_GetValue(&hadc1);
osMessageQueuePut(adcQueueHandle, &val, 0, 0);
osDelay(10);
}
}
/* 数据处理任务:从队列接收数据 */
void StartProcessTask(void *argument)
{
uint32_t val;
for (;;)
{
osMessageQueueGet(adcQueueHandle, &val, NULL, osWaitForever);
/* 处理 val ... */
}
}
2.5.6.4 osDelay 与 HAL_Delay 的区别¶
表 A-3 osDelay 与 HAL_Delay 的区别
| 函数 | 底层实现 | 使用场景 |
|---|---|---|
HAL_Delay(ms) |
SysTick 忙等待(或 TIM 基准轮询) | 不可在 FreeRTOS 任务中使用,会阻塞整个调度器 |
osDelay(ms) |
FreeRTOS vTaskDelay(),任务挂起 |
FreeRTOS 任务中必须使用此函数,让出 CPU |
A.5.7 重新配置工程¶
2.5.7.1 修改配置的工作流¶
当硬件设计发生变化(如增加传感器、修改引脚)或需要启用新外设时:
图 A-18 修改配置的工作流
2.5.7.2 用户代码保护机制验证¶
以下场景演示 USER CODE 保护是否生效:
图 A-19 用户代码保护机制验证
2.5.7.3 常见问题与最佳实践¶
表 A-4 常见问题与最佳实践
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 代码被覆盖 | 用户代码写在 USER CODE 标记之外 | 严格遵守 BEGIN/END 规范 |
| 引脚冲突(红色高亮) | 两个外设分配了同一引脚 | 在 CubeMX 中重新分配其中一个引脚 |
| 烧录后程序不运行 | SYS 未配置 SWD,或 BOOT0 未正确设置 | 确认 SYS→Debug→Serial Wire 已启用 |
| FreeRTOS 任务卡死 | 在任务中使用了 HAL_Delay |
全部替换为 osDelay |
| 堆栈溢出崩溃 | 任务栈空间不足 | 增大 Stack Size,或使用 uxTaskGetStackHighWaterMark 监控 |
| HAL_Delay 不准 | FreeRTOS 占用了 SysTick | CubeMX 已自动切换时基到 TIM,无需手动处理 |
2.5.7.4 使用 Coolify 配置容器运行 PicSimLab 仿真¶
PicSimLab 是一种用于嵌入式/传感器仿真的轻量化仿真环境。可通过 Coolify 将 PicSimLab 以容器形式部署,便于在云或本地服务器上提供仿真服务给学生和实验环境。
示例 docker-compose.yml(作为 Coolify 的仓库部署或本地测试):
version: "3.8"
services:
picsimlab:
image: picsimlab/picsimlab:latest # 若无官方镜像,改为自己构建的镜像名
restart: unless-stopped
ports:
- "8080:8080" # Web UI 或 API 端口
volumes:
- ./picsimlab-work:/home/picsimlab/work # 持久化仿真工程与数据
environment:
- TZ=Asia/Shanghai
在 Coolify 中部署步骤(简要):
- 打开 Coolify 控制台,点击 Create → Application。
- 选择 "Container"(容器)或使用 "Repository" 并指向包含 docker-compose.yml 的仓库(推荐)。
- 如果选 Container:填写 Image(例如 picsimlab/picsimlab:latest),设置端口映射 8080,配置环境变量与卷挂载(将宿主路径映射到容器的 /home/picsimlab/work)。
- 如果选 Repository:指定分支与 Docker Compose 文件路径,Coolify 会根据 docker-compose.yml 构建并部署服务。
- 部署完成后,通过 Coolify 提供的域名或映射端口访问 PicSimLab Web 界面(例如 http://
:8080 或应用子域)。
注意: - 若没有官方镜像,请在仓库中包含 Dockerfile 或 docker-compose.yml,由 Coolify 从源码构建镜像。 - 保证宿主机有必要的端口、磁盘和 CPU 权限来运行仿真。
A.5.8 本章在线测试(10 题)¶
Quiz results are saved to your browser's local storage and will persist between sessions.
1) 在 CubeMX 中,若要使用 ST-Link 下载与调试,SYS -> Debug 应设置为哪一项?
2) Blue Pill 标准 72MHz 配置中,外部晶振通常设置为:
3) 在引脚视图中,红色高亮通常表示:
4) 下列哪个步骤用于让 CubeMX 自动计算主频相关参数?
5) 关于 User Label(例如给 PC13 标记为 LED),正确的是:
6) 在 FreeRTOS 任务函数中应优先使用哪个延时函数?
7) 重新生成代码时,哪类代码最容易被覆盖?
8) 下列关于工程目录的说法哪些正确?
9) 若新增 USART1 后重新 Generate Code,最合理的预期是:
10) 当 CubeMX 中出现引脚冲突时,优先处理方式是:
Quiz Progress
0 / 0 questions answered (0%)
0 correct