STM32工程模板详解
STM32工程模板
一,时钟配置
1.什么是时钟?
在电子学和微控制器领域,时钟信号是一种周期性变化的电子信号,用于同步系统中的操作和数据传输。时钟信号通常是方波,并具有特定的频率,频率决定了系统的运行速度。
2.时钟的作用
- 控制CPU执行指令
CPU 执行指令是按照一定的步骤进行的,每个指令都有一个指令周期。时钟信号将这个周期划分为若干个时钟周期(也称为 T 周期),一个简单的指令可能需要几个时钟周期来完成,包括取指令、指令译码、执行指令等阶段。在每个时钟周期的上升沿或下降沿,CPU 会根据时钟信号的节奏,按照预定的逻辑顺序依次完成这些阶段的操作(现代微控制器(如STM32的内核)通常采用流水线技术来提高指令执行效率。在流水线中,不同的指令在不同的阶段同时进行处理。例如,当一条指令在执行阶段时,下一条指令可能正在取指令阶段,时钟信号保证了它们各自按照自己的节奏进行,不会相互干扰。) - 内存访问
在读取数据时,CPU 会在时钟信号的特定时刻向内存发送地址信号,然后在经过一定的延迟后,在另一个时钟信号的上升沿或下降沿读取内存输出的数据。写入数据时也是类似的过程,CPU 在时钟信号的控制下将数据和地址信号发送到内存,内存则在合适的时钟时刻将数据写入指定的地址。 - 确定运行速度
时钟频率直接影响系统的运行速度。较高的时钟频率意味着更快的处理速度,但也意味着更高的功耗。 - 驱动外设
许多外设(如定时器、USART、SPI等)需要时钟信号来工作。例如,串口在发送数据时,微控制器按照时钟信号的节奏将数据位一位一位地发送出去;在接收数据时,也根据时钟信号来采样接收线上的数据,以正确地解析出接收到的数据。对于其他外设,如 SPI、I2C 等接口设备,时钟信号也是保证数据同步传输的关键因素。
3.配置步骤
配置时钟的主要步骤
- 选择时钟源
双击下图access ro mcu selector (进入mcu选择器)

在下图左上角commercial part number搜索芯片型号,我选的是STM32F429VGT6

按照下图选择外部时钟(HSE/LSE),并选择为Crystal/Ceramic Resonator(晶体 / 陶瓷谐振器)

为什么这样选?下节讲
-
配置PLL
按照下图:选择Clock Configuration(时钟配置)–选择外部时钟HSE–选择PLL–将频率改为180–按下回车键(一定要按)

为什么要配置PLL?PLL即相位锁定环,可以调整时钟频率,生成所需的系统时钟。PLL能够倍频或分频输入时钟,产生更高或更低的频率。 -
设置时钟树
配置系统时钟、AHB时钟、APB1时钟和APB2时钟,确保各外设时钟频率正确。这是分配时钟资源的关键步骤。 -
使能外设时钟
为需要使用的外设使能时钟信号。只有使能了相应的时钟,外设才能正常工作。
对于4,5步骤如果用到需要时钟的外设就要进行配置,这里只是搭建模板,不用配置
二,SYS配置
SYS配置主要涉及调试接口的选择和设置
调试接口是什么?在ARM 微控制器中,两种常见的调试端口:
- Serial Wire Debug Port(SW – DP):串行线(Serial Wire)调试(Debug)接口(Port)
- JTAG Debug Port(JTAG – DP):JTAG调试接口
两种接口区别如下
| 项目 | SW-DP | JTAG-DP |
|---|---|---|
| 引脚需求 | 仅需2个引脚 (SWCLK, SWDIO),SWCLK:主机到从机的时钟信号,SWDIO:双向数据信号 | 4个引脚(4pin)或5个引脚(5pin,含复位)TCK:测试时钟,TMS:测试模式选择,TDI:测试数据输入,TDO:测试数据输出,TRST:测试复位(可选,5pin模式才有) |
| 适用于 | ST-Link调试器 | J-Link等高级调试器 |
| 优势 | 引脚占用少,速度快 | 功能更完整,支持高级调试 |
上面提到了调试器,什么是调试器?调试器可以帮助开发人员查找和解决程序中的错误、分析程序的运行行为以及优化系统性能,ST-Link调试器支持SW接口,J-Link等高级调试器支持JATG接口
使用建议
使用ST-Link时:选择Serial Wire
使用J-Link时:可选JTAG或Trace Asynchronous
注意:在CubeMX中选择的调试协议必须与MDK中选择的协议一致(下文讲)
调试问题:如果遇到烧录和调试失败,尝试切换协议
按照下图进行配置,我用的是CMSIS-DAP调试器,支持SW和JTAG两种接口,但我选Serial Wire(线少),注意配置完成后芯片右上角的两个引脚变成绿色

三,工程配置
1.项目设置(Project)
按照下图设置
Project Name:设置项目的名称,将用于生成的文件和文件夹。名称应避免使用特殊字符(中文也是特殊字符)和空格。
Project Location:设置项目的存储位置。路径应避免使用特殊字符和空格,以确保兼容性。
Toolchain/IDE:选择用于开发项目的集成开发环境或工具链。这将决定生成的项目文件格式。
Min Version:V5.32
后面的堆和栈无需配置,挂个梯子可以自动配置

2.代码生成器(Code Generator)
- STM32Cube MCU Software Package and Embedded Software Package(STM32Cube MCU 软件包和嵌入式软件包)
将所有使用的库复制到项目文件夹
仅复制必要的库文件
在工具链项目配置文件中添加必要的库文件作为引用
勾第二个 - 生成文件(Generated Files)
为每个外设生成一对 “.c/.h” 文件用于外设初始化
重新生成时备份先前生成的文件
重新生成时保留用户代码
不重新生成时删除先前生成的文件
勾1,3,4
尤其是3,后面讲

3.高级设置
暂时不用配置
四,NVIC配置
本节只是讲模板,用不到中断,不配置
五,Keil配置
CobeMX配置完成后,按下图点击GENERATE CODE生成代码

出现下图,说明生成成功,点击Open Folder可以跳转的工程所在文件夹

可以看见生成了5个文件

如果你点击GENERATE CODE生成代码后出现了以下提示,说明没有生成MDK-ARM文件,可能是Java环境没有安装好,因为CobeMX是运行在Java之上的,下节再教

下面我将解释一下这五个文件,并配置Keil
1.五个文件的含义
.mxproject
包含项目的元数据和配置。它通常是由STM32CubeMX等工具生成的,用于描述工程的整体设置和生成代码的选项。
Core
包含核心代码,如主程序、系统初始化和中断服务例程等。包括主程序入口、系统初始化和中断处理代码。
Drivers
包含设备驱动程序,通常包括外设的初始化和控制代码,以及HAL/LL库的硬件抽象层。
MDK-ARM
用于存放ARM MDK (Keil) 集成开发环境相关的文件,包含工程文件如.uvprojx和.uvoptx,以及编译设置。
STM32_DEMO_01.ioc
I/O配置文件,用于描述微控制器的引脚和外设配置,定义外设的初始化参数和工作模式。
2.配置Keil
- 首先在工程文件下新建APP文件:用户自定义的应用程序代码目录,用于存放用户编写的应用逻辑代码和功能模块实现。

- 将APP添加到Keil中
将APP中的头文件(.h文件)的路径添加的Keil

将APP文件添加到工程中,添加完成时,工程左边出现了APP


在APP中添加mydefine.h

在桌面上复制调度器的.c.h文件到APP中,再在Keil中将调度器.c文件添加到APP中
下图在桌面上加入调度器文件(调度器的.c.h文件是固定的,在裸机调度器有教,会移植即可,具体代码放文章末)

下图在Keil中选取调度器.c文件到Keil中,mydefine.h文件也添加进APP

3.修改移植错误
刚才将调度器的.c.h 文件移植到了APP中,点击编译后会报很多错误,这是学会移植的关键,如何解决?

前面4行依次是:
-
开始编译项目STM_DEMO_01
-
使用V5.06 update 7(build 960)编译器,编译结果所在文件夹是C:\Keil_v5|ARM\ARMCC\Bin(这个编译器是需要设置的,文件夹是搭建环境时设置的)

-
编译目标是STM32_DEMO_01
-
正在编译scheduler.c文件
接下来看一下序号1的错误:…\APP\scheduler.c(5): error: #20: identifier “uint8_t” is undefined
uint8_t task_num;即task_num的类型uint8_t没有定义,双击跳转错误所在位置
发现错误在scheduler.c文件中,scheduler.c的开头只引用了scheduler.h头文件,我的思路是跳转到scheduler.h文件中,看看它里面有没有uint8_t的定义,在.c文件中右键
这里Insert’#include file’是导入头文件,Toggle Header/Code File意思是切换头文件(.h)/代码文件(.c),单击此键,可以切换到.c对应的.h文件,我们单击它

发现scheduler.h中没有uint8_t的定义,但它又引用了mydefine.h头文件,我们可以跳转的mydefine.h中看看有没有uint8_t的定义:将鼠标放到mydefine.h-右键

这里open document ‘mydefine.h’意思是打开文档‘mydefine.h’,Toggle Header/Code File意思是切换头文件(.h)/代码文件(.c),单击此键,可以切换到.h对应的.c文件,我们单击open document ‘mydefine.h’打开mydefine.h,
发现原因:追溯头文件路径到mydefine.h,这里没有uint8_t的定义,如何解决呢?加入#include "main.h"头文件(自己按上文的方法追溯,可以找到uint.h的定义),编译一下,又报了两个错误

第一个错误是…\APP\mydefine.h(1): warning: #1-D: last line of file ends without a newline
#include “main.h”,即在main.h之后缺少线,我们在main.h后面点击一下回车,再编译一下即可

此时还剩下一个错误:…\APP\scheduler.c(16): error: #20: identifier “led_task” is undefined
{led_task, 1, 0}, 双击跳转到错误所在位置,
即调度器中的任务函数led_task没有定义,我们直接在调度器任务数组前面(为什么放前面不放后面?C语言基础)定义一个空led任务函数即可(这里只是教一下模板),编译发现无错误无警告

解决完报错后,我们再在mydefine.h中引用scheduler.h调度器文件,这样以后写底层只需引用mydefine.h即可

4.头文件保护机制
这时你可能会发现:mydefine.h引用了scheduler.h,而同时scheduler.h也引用了mydefine.h文件,如果我将一下代码注释掉再编译:
此报错…\APP\mydefine.h(2): error: #3: #include file “…\APP\scheduler.h” includes itself
#include "scheduler.h"意思是:mydefine.h引用了scheduler.h 文件,而scheduler.h 头文件中又包含了自身,这会造成无限递归包含,最终引发编译错误。
如何解决这种头文件重复包含的情况呢?头文件保护机制。
头文件保护机制是一种用于防止头文件被重复包含的技术,在 C 和 C++ 编程中广泛使用。它主要借助预处理指令(如#ifndef、#define和#endif ),再在指令后面加入自定义的宏名即可,(就是我注释掉的部分,宏名可以随意,但一般遵循以下惯例)
- 字母大写:将头文件名(如 stm32f4xx_hal.h )全部转换为大写字母。
- 符号替换:用下划线(_)替换文件名中的点号(. ) 。例如 stm32f4xx_hal.h 经转换为宏名时,点号要变为下划线。
本节结束
附录
scheduler.c
#include "scheduler.h"
// 全局变量,用于存储任务数量
uint8_t task_num;
typedef struct {
void (*task_func)(void);
uint32_t rate_ms;
uint32_t last_run;
} task_t;
void led_task()
{
}
// 静态任务数组,每个任务包含任务函数、执行周期(毫秒)和上次运行时间(毫秒)
static task_t scheduler_task[] =
{
{led_task, 1, 0}, // 定义一个任务,任务函数为 Led_Proc,执行周期为 10 毫秒,初始上次运行时间为 0
};
/**
* @brief 调度器初始化函数
* 计算任务数组的元素个数,并将结果存储在 task_num 中
*/
void scheduler_init(void)
{
// 计算任务数组的元素个数,并将结果存储在 task_num 中
task_num = sizeof(scheduler_task) / sizeof(task_t);
}
/**
* @brief 调度器运行函数
* 遍历任务数组,检查是否有任务需要执行。如果当前时间已经超过任务的执行周期,则执行该任务并更新上次运行时间
*/
void scheduler_run(void)
{
// 遍历任务数组中的所有任务
for (uint8_t i = 0; i < task_num; i++)
{
// 获取当前的系统时间(毫秒)
uint32_t now_time = HAL_GetTick();
// 检查当前时间是否达到任务的执行时间
if (now_time >= scheduler_task[i].rate_ms + scheduler_task[i].last_run)
{
// 更新任务的上次运行时间为当前时间
scheduler_task[i].last_run = now_time;
// 执行任务函数
scheduler_task[i].task_func();
}
}
}
scheduler.h
#ifndef SCHEDULER_H
#define SCHEDULER_H
#include "mydefine.h"
void scheduler_init(void);
void scheduler_run(void);
#endif
作者:百里东风