STM32工程模板

  • 一,时钟配置
  • 1.什么是时钟?
  • 2.时钟的作用
  • 3.配置步骤
  • 二,SYS配置
  • 三,工程配置
  • 1.项目设置(Project)
  • 2.代码生成器(Code Generator)
  • 3.高级设置
  • 四,NVIC配置
  • 五,Keil配置
  • 1.五个文件的含义
  • 2.配置Keil
  • 3.修改移植错误
  • 4.头文件保护机制
  • 一,时钟配置

    1.什么是时钟?

    在电子学和微控制器领域,时钟信号是一种周期性变化的电子信号,用于同步系统中的操作和数据传输。时钟信号通常是方波,并具有特定的频率,频率决定了系统的运行速度。

    2.时钟的作用

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

    3.配置步骤

    配置时钟的主要步骤

    1. 选择时钟源
      双击下图access ro mcu selector (进入mcu选择器)

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

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

    为什么这样选?下节讲

    1. 配置PLL
      按照下图:选择Clock Configuration(时钟配置)–选择外部时钟HSE–选择PLL–将频率改为180–按下回车键(一定要按)

      为什么要配置PLL?PLL即相位锁定环,可以调整时钟频率,生成所需的系统时钟。PLL能够倍频或分频输入时钟,产生更高或更低的频率。

    2. 设置时钟树
      配置系统时钟、AHB时钟、APB1时钟和APB2时钟,确保各外设时钟频率正确。这是分配时钟资源的关键步骤。

    3. 使能外设时钟
      为需要使用的外设使能时钟信号。只有使能了相应的时钟,外设才能正常工作。
      对于4,5步骤如果用到需要时钟的外设就要进行配置,这里只是搭建模板,不用配置

    二,SYS配置

    SYS配置主要涉及调试接口的选择和设置
    调试接口是什么?在ARM 微控制器中,两种常见的调试端口:

    1. Serial Wire Debug Port(SW – DP):串行线(Serial Wire)调试(Debug)接口(Port)
    2. 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)

    1. STM32Cube MCU Software Package and Embedded Software Package(STM32Cube MCU 软件包和嵌入式软件包)
      将所有使用的库复制到项目文件夹
      仅复制必要的库文件
      在工具链项目配置文件中添加必要的库文件作为引用
      勾第二个
    2. 生成文件(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

    1. 首先在工程文件下新建APP文件:用户自定义的应用程序代码目录,用于存放用户编写的应用逻辑代码和功能模块实现。
    2. 将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行依次是:

    1. 开始编译项目STM_DEMO_01

    2. 使用V5.06 update 7(build 960)编译器,编译结果所在文件夹是C:\Keil_v5|ARM\ARMCC\Bin(这个编译器是需要设置的,文件夹是搭建环境时设置的)

    3. 编译目标是STM32_DEMO_01

    4. 正在编译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 ),再在指令后面加入自定义的宏名即可,(就是我注释掉的部分,宏名可以随意,但一般遵循以下惯例)

    1. 字母大写:将头文件名(如 stm32f4xx_hal.h )全部转换为大写字母。
    2. 符号替换:用下划线(_)替换文件名中的点号(. ) 。例如 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
    
    

    作者:百里东风

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32工程模板详解

    发表回复