• 🏠 回到主页

    蓝桥杯嵌入式学习笔记

    个人嵌入式学习笔记,蓝桥杯部分,仅供学习参考,不允许售卖。


    提示:本篇相当部分代码及语法解释来自百度文心一言及阿里通义灵码,chatgpt AI生成,并且有相当多的部分摘抄了网络博文和正点原子的HAL库开发手册,本篇笔记由本人在相当长的学习过程中逐渐整理成型,缺少精力对各部分内容来源进行标注,本文档中绝大部分的代码已通过实践验证,如果存在知识点错误或内容有更好的表述形式,欢迎通过邮箱指正 yang92636@qq.com 欢迎您在git代码托管平台上提交Issues,笔者已经参与过了蓝桥杯比赛,本文后续更新幅度和修改范围会非常小,甚至不更新,您可以通过GITEE/GITCODE网址:https://gitee.com/weigo6/MCU_LEARN_LIB 或者 https://gitcode.com/weigo6/MCU_LEARN_LIB 查看最新文档(欢迎来点个Star),希望本份笔记能够为你提供帮助!

    跳转本文对应的仓库:与蓝桥杯比赛嵌入式赛道相关的内容整理

    一、HAL库常用函数

    在写HAL库代码时,所有用户代码放在user code begin和user code end中,否则重新生成代码会被覆盖

    1.GPIO

    __HAL_RCC_GPIOF_CLK_ENABLE();//使能GPIOF时钟

    __HAL_RCC_USART2_CLK_ENABLE();//使能串口 2 时钟 __HAL_RCC_TIM1_CLK_ENABLE();//使能 TIM1 时钟 __HAL_RCC_DMA1_CLK_ENABLE();//使能 DMA1 时钟

    HAL_GPIO_Init();//GPIO初始化

    2.NVIC中断

    在HAL库中,中断服务函数定义在底层,不需要编写,通常只需要编写中断回调函数,无需清理中断标记位。

    第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行; 第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。

    中断分组表:

    AIRCR[10:8]bit[7:4]分配情况分配结果
    01110:40 位抢占优先级,4 位响应优先级
    11101:31 位抢占优先级,3 位响应优先级
    21012:22 位抢占优先级,2 位响应优先级
    31003:13 位抢占优先级,1 位响应优先级
    40114:04 位抢占优先级,0 位响应优先级

    中断分组函数在HAL_Init内被调用。

    配置 IO 口外部中断的一般步骤:

    1. 使能 IO 口时钟。

    2. 调用函数 HAL_GPIO_Init 设置 IO 口模式,触发条件,使能 SYSCFG 时钟以及设置 IO 口与中断线的映射关系。

    3. 配置中断优先级(NVIC),并使能中断。

    4. 在中断服务函数中调用外部中断共用入口函数 HAL_GPIO_EXTI_IRQHandler

    5. 编写外部中断回调函数 HAL_GPIO_EXTI_Callback

    GPIO与中断线的映射关系图

    中断函数调用示例:

    中断处理函数定义

    该函数进行了清除了中断标志位,因此在编写函数过程中只需要写函数HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)控制逻辑。

    \(^o^)/~

    Note
    函数调用关系:EXTIx_IRQHandler——>HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_x)——>HAL_GPIO_EXTI_Callback(GPIO_Pin);

    3.TIM

    更新中断就是定时器溢出中断,定时器就是计数器。

    与定时器的值有关的函数

    等价函数

    image-20240205201034443

    常用函数

    这里列出单独使能/关闭定时器中断和使能/关闭定时器方法: __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);//使能句柄指定的定时器更新中断 __HAL_TIM_DISABLE_IT (htim, TIM_IT_UPDATE);//关闭句柄指定的定时器更新中断 __HAL_TIM_ENABLE(htim);//使能句柄 htim 指定的定时器 //似乎不起作用,使用上面的代码 __HAL_TIM_DISABLE(htim);//关闭句柄 htim 指定的定时器

    涉及到初始化的一些代码

    定时器编码器

    输入捕获

    当你设置的捕获开始的时候,cpu会将计数寄存器的值复制到捕获比较寄存器中并开始计数,当再次捕捉到电平变化时,这是计数寄存器中的值减去刚才复制的值就是这段电平的持续时间,你可以设置上升沿捕获、下降沿捕获、或者上升沿下降沿都捕获。 需要开启NVIC中断

    PWM

     

    4.ADC

    ADC单通道采集

    使用示例:获取 ADC1 通道 ch 的转换结果,先取 times 次,然后取平均

     

    5.I2C

    I2C代码根据所使用的I2C外设模块,通常由厂商提供。HAL库IIC初始化可同过CubeMX软件图形化完成。

    软件模拟iic的一些通用代码

    硬件IIC

    硬件IIC通过CubeMX设置,默认开启I2C即可

    使用STM32硬件I2C驱动AHT10温湿度传感器的代码

     

    6.UART串口通信

    初始化、发送接收和中断回调函数

    传输中断函数

    串口DMA传输

    空闲中断

    如果要实现数据不定长收发,需要开启串口空闲中断

    使用串口DMA实现不定长数据接收(串口和DMA均由CubeMX配置,无需另外代码)

    使用示例

    DMA

    RTC

    RTC使用示例(串口定时打印当前时间)

    SysTick函数

    关于uwTick

    HAL_IncTick() 是 STM32 HAL(硬件抽象层)库中的一个函数,用于在 SysTick 中断服务例程(ISR)内部递增一个名为 uwTick 的全局变量的值。这个全局变量用作系统的滴答计数器(tick counter),通常以毫秒为单位递增。HAL_IncTick() 函数的主要目的是提供一个简单而通用的机制,用来追踪自系统启动以来经过的时间,以及实现基于时间的操作,比如延时。

    具体来说,HAL_IncTick() 函数的工作机制如下:

    在实际应用中,开发者通常不需要直接调用 HAL_IncTick(),因为这个函数已经在 HAL 库的 SysTick 中断服务例程模板中被调用。开发者需要确保 SysTick 定时器正确配置,以及对应的中断服务例程已经包含了对 HAL_IncTick() 的调用。这样,开发者就可以利用 HAL_GetTick()HAL_Delay() 等函数实现时间相关的功能,而无需关心背后的细节。

    实际开发中,一些延迟应用并不需要开启定时器,可以使用SysTick 中断服务(不需要手动配置,只需要配置好系统时钟就可以)。

     

    微秒级延迟函数(根据系统主频不同需要修改)

    开启相应的定时器,把函数放在tim.c中

    软件重启函数,复位,使能/关闭全局中断

    NVIC_SystemReset();软件复位函数

    在软件复位过程中,程序仍然可以响应中断,为避免软重启失败。通常会在软重启前关闭所有中断。

    二、c语言语法

    1、sprintf函数

    sprintf 是一个在 C 语言中常用的函数,用于将格式化的数据写入字符串中。它的函数原型如下:

    sprintf函数会将传递给它的可变参数按照指定的格式进行格式化,并将结果写入str` 指向的字符串中。返回值是写入字符串的字符数(不包括字符串末尾的空字符)。

    sprintf 函数在 C 语言中是标准库函数,它定义在 stdio.h 头文件中。

    Tip
    注:在sprintf函数中打印百分号(%),您需要使用两个百分号(%%)。这是因为在sprintf函数中,百分号被用作转义字符,表示要插入格式化输出。

    输出结果

    sprintf所写入的字符串为char类型。而蓝桥杯屏幕显示函数所需的参量类型为uint8_t。建议强制类型转换以避免warning。不过实测该warning不影响程序运行。

    一种规范的写法:

    2、bool布尔类型

    在C语言中,布尔类型是一种基本的数据类型,用于表示逻辑值truefalse 在C99标准及其之后的标准中,C语言在<stdbool.h>头文件中提供了内置的布尔类型支持。当你包含这个头文件时,你可以使用bool关键字来声明布尔变量,使用true和false来表示布尔值。例如:

    请注意,truefalse在C99中是关键字,它们的值分别是1和0。同时,bool实际上是一个宏,通常被定义为_Bool或者int。

    3、结构体struct与typedef struct

    struct和typedef struct都是用来定义结构体的关键字

    在这个例子中,我们定义了一个名为Student的结构体,它包含三个成员:idname,和age。其中,id是一个整数,name是一个字符数组(可以存储一个长度为50的字符串),age也是一个整数,创建了一个名为stu1Student结构体实例,并为其成员赋值。

    在C语言中,使用typedefstruct来定义结构体类型的语法如下:

    在上述示例中,我们定义了一个名为Person的结构体类型,并使用别名Person来声明结构体实例p1。然后,我们可以使用点号.来访问结构体成员变量,例如p1.namep1.age

    结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结 构体的成员变量 BaudRate,方法是:Usart3->BaudRate;

    4、弱定义extern,__weak

    在C语言中,弱定义是一种允许同一个符号(变量或函数)在多个源文件中被定义,但在链接时只有一个定义会被保留的定义方式。弱定义使用extern关键字来声明变量或函数,并且在声明后面不跟任何分号。

    下面是一个使用弱定义的示例:

    假设我们有一个全局变量global_var,需要在多个源文件中共享。我们可以将其声明为弱变量,并在其中一个源文件中定义它:

    在其他源文件中,我们可以使用extern关键字来引用global_var变量,例如:

    在链接时,链接器会选择其中一个定义作为最终的定义,其他定义将被忽略。因此,在最终的可执行程序中,只有一个global_var变量会被定义,并且可以被所有源文件访问。

    需要注意的是,弱定义只能用于变量或函数,不能用于函数参数、结构体、枚举等其他类型。此外,弱定义必须保证只有一个强定义(使用static关键字定义的变量或函数),否则会导致链接错误。

    Caution
    需要注意的是:extern int global_var=10;此类写法是不允许的。

    __weak关键字用于修饰函数或变量,表示该函数或变量是弱定义的。

    大部分中断回调函数都被__weak关键字修饰

    具体来说,当你在代码中声明一个弱定义的函数或变量时,如果你没有在其它地方定义这个函数或变量,编译器会报错。

    而使用__weak关键字可以告诉编译器,这个函数或变量是弱定义的,如果在其它地方没有定义,则使用这个弱定义的函数或变量。

    __weak关键字通常用于在不同的模块之间共享函数或变量,特别是在嵌入式系统中,不同的模块可能会使用相同的函数或变量名,为了避免冲突,使用弱定义是一种有效的解决方法。

    需要注意的是,__weak关键字并不是C语言的标准化关键字,而是特定编译器或环境提供的扩展。

    5、__IO定义

    __IO是一个在嵌入式C语言中常见的关键字,特别是在与硬件相关的编程中。__IO通常用于指定一个变量或地址空间为输入输出(IO)空间。

    在大多数处理器架构中,内存被划分为几个不同的空间,例如:RAM、ROM、IO空间等。__IO关键字用于告诉编译器,某个变量或指针引用的地址位于IO空间,而不是常规的RAM或ROM空间。

    例如,当您在与硬件寄存器交互时,这些寄存器通常位于特殊的IO地址空间。在这种情况下,您可能会使用__IO关键字来定义一个指向这些寄存器的指针:

    在这个例子中,register_address是一个指向地址0x40000000的指针,并且这个地址被指定为IO空间。这样编译器就知道,当访问这个指针时,需要生成适用于访问IO空间的机器代码。

    __IO并不是C语言的标准关键字,而是特定于某些编译器和架构的扩展。

    6、 ifdef 条件编译

    在C语言中,#ifdef是一个预处理指令,它用于进行条件编译。#ifdef后面跟着一个宏名称,如果这个宏被定义了,那么#ifdef后面的代码就会被编译进去,否则这部分代码会被忽略。

    下面是一个简单的例子:

    在这个例子中,如果宏FEATURE_A被定义了,那么程序会输出"Feature A is enabled."。如果宏FEATURE_A没有被定义,那么程序会输出"Feature A is not enabled."。

    在实际开发中,我们通常会使用#ifdef来检查某些编译选项或者平台特性是否被定义,然后根据这些条件来选择性地编译代码。

    7、printf串口重定向

    这段 printf 函数支持的代码在初始化串口后使用,这段代码加入之后便可以通 过 printf 函数向串口发送我们需要的内容。

    8、switch case多路分支语法

    switch语句是一种多路选择结构,可以根据不同的条件选择不同的执行路径。

    这是最简单和最常用的switch语句结构。这里,expression是要评估的表达式,constant1constant2等是可能的值。如果expression的值等于某个case后面的常量,则执行相应的代码块。break语句用于退出switch语句。如果没有break,程序将继续执行下一个case

    9、goto语句与标签

    goto语句用于无条件地转移到程序中的另一部分。它通常用于跳出循环或提前退出函数。然而,使用goto语句需要谨慎,因为过度使用它可能会导致代码难以理解和维护。

    标签(Label)是一个代码标识符,后面跟一个冒号,它标识一个语句的位置。标签可以与goto语句一起使用,使程序跳转到该标签所在的语句。

    标签可以被多次引用,这意味着可以在代码中定义相同的标签多次。但是,一个标签只能被一个语句块内的 goto 语句引用,也就是说,goto语句只能跳转到最近的被引用的标签所在的位置。这个规则是为了确保代码的可读性和可维护性。如果一个标签被多个goto语句引用,就会导致代码的流程变得不清晰,而且也不容易理解。因此,一个标签只能被一个语句块内的goto语句引用,这样可以保证代码的流程更加清晰和易于理解。

    10、sscanf函数

    sscanf 是一个标准C库函数,用于从字符串中读取格式化输入。它的名字代表“string scan”,意即从字符串中扫描格式化的数据。sscanf 的工作方式与 scanf 类似,但它不是从标准输入(通常是键盘)读取数据,而是从一个给定的字符串中读取。

    sscanf 函数的返回值是成功读取并赋值的输入项数,如果输入结束或发生输入失败,则可能小于提供的指针数量。

    下面是一个简单的 sscanf 示例:

    在这个例子中,sscanf 函数尝试从 input 字符串中读取一个整数、一个浮点数和一个字符串,并将这些值存储在相应的变量中。格式字符串 "%d %f %19s" 指定了要读取的数据类型和格式:一个整数(%d)、一个浮点数(%f)和一个最多包含19个字符的字符串(%19s)。注意,字符串的读取长度被限制为19个字符,以避免缓冲区溢出。

    11、左移右移操作符<< >>

    1. 左移运算符 (<<)

      • 将左操作数的所有位向左移动指定的位数,右侧空出的位用0填充。

      • 有一个8位的二进制数 00001010 (十进制中的10)。如果我们将其左移1位,结果为 00010100 (十进制中的20)。

      • 左移操作相当于乘以2的某个幂。例如,左移1位相当于乘以2,左移2位相当于乘以4,以此类推。

    2. 右移运算符 (>>)

      • 将左操作数的所有位向右移动指定的位数。

      • 对于无符号整数,右侧溢出的位被丢弃,左侧空出的位用0填充。

      • 对于有符号整数,右移的处理方式依赖于具体的编译器或机器。在许多环境中,算术右移会保留符号位(即最左边的位),这意味着负数的右移会在左侧填充1,而正数或0的右移会在左侧填充0。但在某些环境中,也可能采用逻辑右移,即无论符号如何,都在左侧填充0。

      • 有一个8位的二进制数 00001010 (十进制中的10)。如果我们将其右移1位,结果为 00000101 (十进制中的5)。

      • 右移操作可以被视为整除2的某个幂。例如,右移1位相当于除以2,右移2位相当于除以4(忽略余数),以此类推。

    12、strcmp函数

    strcmp 函数是 C 语言标准库中 <string.h> 头文件提供的一个函数,用于比较两个字符串,其语法如下:

    strcmp函数会比较两个字符串str1str2,如果两个字符串相等,则返回0;

    如果str1小于str2,则返回一个负数;

    如果str1大于str2,则返回一个正数。

    以下是一个简单的示例代码,演示如何使用strcmp函数:

    在上面的示例代码中,我们比较了两个字符串str1str2,并根据strcmp函数的返回值输出不同的结果。

    13、strlen/sizeof函数

    在C语言中,strlensizeof 是两个不同的操作,它们用于不同的目的。

    1. strlen 函数用于计算字符串的长度,不包括 null 终止字符 '\0'。strlen 是 C 标准库中 <string.h> 头文件提供的函数。下面是它的语法:

    函数参数:

    返回值:

    用法示例:

    这段代码会输出字符串 "Hello, World!" 的长度,不包括结尾的 null 字符。

    1. sizeof 操作符用于计算变量或类型所占的字节数。它在编译时计算,而不是运行时。下面是它的用法:

    用法示例:

    这段代码会输出整型数组 arr 的大小(以字节为单位),因为 arr 包含 10 个整数,所以如果一个整数占 4 个字节,那么输出将会是 40 字节。

    • strlen 返回的是字符串的实际长度,直到遇到第一个 null 字符。

    • sizeof 返回的是数组分配的总字节数,对于字符串,这包括末尾的 null 字符。如果 sizeof 用于指针,它将返回指针本身的大小,而不是指针所指向的数据的大小。

    14、memset函数

    memset(rx_data, 0, sizeof(rx_data));memset常用于清空串口接收缓存,rx_data是自己定义用来存放串口数据的变量

    在C语言中,memset函数的语法如下:

    memset函数通常用于将一块内存区域的值设置为指定的值,可以是0、-1或其他特定值。例如,将一个数组的所有元素初始化为0,可以使用memset函数:

    这样就会把数组arr中的所有元素设置为0。

    15、strtok函数

    strtok 函数是 C 语言标准库中的一个用于分割字符串的函数。它可以用来将字符串分割成一系列的标记(token),分割是根据一组指定的分隔符来完成的。

    函数原型如下:

    参数:

    返回值:

    注意: strtok 函数使用一个静态缓冲区来存储当前的位置,所以它不是线程安全的。在多线程环境中应当使用 strtok_r,这是它的线程安全版本。

    strtok 的工作方式是,它在 str 指向的字符串中查找 delim 中的字符。当它找到一个这样的字符时,它会用 \0(空字符)替换掉,并返回指向当前标记起始位置的指针。在连续调用中,它会继续从上次停下来的地方开始查找下一个标记。

    示例代码:

    在上面的例子中,strtok 被用来分割字符串 str,每当遇到空格 " " 时,就会分割出一个新的标记。每个标记在控制台上单独一行打印出来。第一次调用 strtok 时,rest 指向要分割的字符串;之后,为了继续分割同一个字符串,我们传递 NULLstrtok

    strtok 函数找到一个标记时,它会在标记的末尾添加一个 \0(空字符)来终止这个标记。然后,strtok 返回一个指向这个标记起始位置的指针。这意味着,它返回的是一个指向原始字符串 str 内部的一个位置的指针,而不是创建一个新的字符串。

    这里的“当前标记”是指 strtok 根据提供的分隔符 delimstr 或其后续调用中剩余部分中找到的第一个标记。标记是指原字符串中由分隔符分隔的子字符串

    具体来说,假设你有以下字符串和调用:

    在第一次调用 strtok 后,原始字符串 str 在内存中的表示会被修改。strtok 会在第一个分隔符(,)的位置放置一个 \0 字符,因此原始字符串 str 现在看起来像两个字符串:"hello" 和 "world,this,is,a,test"。

    strtok 的返回值是指向原始字符串中第一个标记(在这个例子中是单词 "hello")的第一个字符的指针。因此,第一次调用 strtok 后,token 指向的是 "hello"。

    继续调用 strtok(NULL, ",") 会继续从上次停止的位置开始查找下一个标记,并重复相同的过程(在下一个分隔符处放置 \0,并返回指向当前标记的指针)。这意味着,随着 strtok 的每次调用,原始字符串 str 会被进一步分割,strtok 返回的指针将指向这些新形成的标记。

    这种方法的一个后果是 strtok 修改了原始字符串,所以如果你需要保留原始字符串未被修改的状态,你应该先对它进行复制。

    16、size_t

    size_t 是 C 和 C++ 编程语言中定义的一种数据类型。它是一个无符号整数类型,通常用于表示大小(如数组长度、字符串长度等)和基于内存的计算(如内存分配大小)。size_t 类型足够大,能够表示任何对象的大小,包括数组和字符串的最大可能大小。

    这个数据类型在 <stddef.h>(在 C 语言中)和 <cstddef>(在 C++ 中)头文件中定义。由于它是无符号的,size_t 类型的值永远不会是负数。

    size_t 的确切大小依赖于平台和编译器,但它必须至少能够表示编译器支持的最大对象大小。在许多 32 位系统上,size_t 是 32 位无符号整数;在 64 位系统上,它通常是 64 位无符号整数。

    使用 size_t 而不是 int 或其他整数类型进行内存相关的计算和表示,有助于提高代码的可移植性和安全性,因为它能够适应不同平台上的地址空间大小变化。

    下面是一些使用 size_t 的例子:

    例子 1:使用 size_t 作为数组索引和循环计数器

    例子 2:使用 size_t 接收 strlen 函数的返回值

    在这些例子中,使用 size_t 来处理与大小相关的值,可以确保代码在不同的系统和编译器配置中具有良好的兼容性和正确性。

    17、浮点数

    keil编译C/C++代码时,出现警告: #1035-D: single-precision operand implicitly converted to double-precision

    float代表浮点型数据类型,浮点型数据又分为单精度和双精度两种,1.0小写f或者大写F代表他是单精度的,如果1.0后面跟的是小写d后者大写D代表他是双精度的。

    可以忽略这个警告,也可以在所有的浮点数字后面加上f,警告就会消失。比如:float a = 1.01f;

    18、异或^运算

    C语言中的异或运算(XOR,表示为 ^)是一种二进制操作,它遵循这样的规则:如果两个比较的位不同,则结果为1;如果相同,则结果为0。异或运算有多种用途,下面是一些典型的例子:

    1. 值交换

    异或运算可以用于交换两个变量的值而不需要使用临时变量。这是一个巧妙的技巧,但要注意,如果两个变量引用的是同一内存地址,则会导致结果归零。

    1. 加密和解密

    由于异或操作的可逆性(即 A ^ B = CC ^ B = AC ^ A = B),它可以用于简单的加密和解密操作。如果你用相同的密钥(一串数)对数据进行两次异或操作,你会得到原始数据。

    1. 找出不重复的元素

    在一个数组中,如果每个元素都出现两次而只有一个元素出现一次,可以使用异或运算高效地找到这个只出现一次的元素。因为任何数与自身异或的结果为0,并且异或运算满足交换律和结合律。

    1. 比特翻转

    异或运算可以用于翻转指定的比特位。例如,将特定数与特定的掩码进行异或操作可以翻转该数中对应掩码位为1的所有比特位。

    异或运算因其独特的性质而在算法和位操作中广泛应用。

    19、关于字符串

    在C语言中,定义字符串可以使用字符数组来存储。以下是几种常见的定义字符串的方法:

    1. 使用字符数组:

    1. 显式指定字符数组大小:

    1. 使用指针来定义字符串:

    无论使用哪种方法,C语言中的字符串都是以null字符'\0'结尾的字符数组。这个null字符表示字符串的结束。在C语言中,字符串的末尾会自动加上这个null字符,所以我们通常不需要手动在字符串末尾添加'\0'。

    使用指针来定义字符串时,可以通过指针来访问字符串中的内容。以下是一种常见的方法:

    在上面的代码中,我们通过一个指针str指向字符串常量"Hello, World!",然后通过一个循环遍历这个字符串,输出每个字符。每次循环,我们会打印指针当前指向的字符,然后将指针后移,指向下一个字符。当指针指向字符串末尾的null字符'\0'时,循环结束。

    三、关于板子

    如何新建初始工程?

    开发板板载24Mhz晶振。cubeMX时钟树配置外部晶振为24Mhz。 官方例程将系统运行频率设定为80Mhz。

    Warning
    注意使用CubeMX建立项目时不能含有中文路径,否则建立工程会失败。

    1. 打开stm32cubemx,选择正确的芯片型号,新建工程。

    2. pinout&configuration栏选择System Core--->RCC--->HSE(高速外部时钟)选择Crystal/Ceramic Resonator(对应芯片引脚OSC_IN,OSC_OUT)

    3. pinout&configuration栏选择sys--->Debug选择serialwire(串口)

    4. 配置时钟树(Clock Configuration),输入频率设置24MHz,选择HSE,选择PLLCLK(时钟分频),将HCLK一项设定为80MHz(官方例程的系统运行频率)

    5. 在Project Manger栏下配置项目名称和路径(不能有中文),Toolchain/IDE选择MDK-ARM。在Code Generator中勾选每个外设生成.c,.h文件选项

    程序无法烧录?

    1. 边按住芯片复位键边烧录代码

    2. 确定keil软件选择了正确的调试方式(CMSIS DAP),并确保设备管理器能够识别

    3. 在烧录(Debug)设置中选择了正确的芯片内存和地址范围

    4. 检查是否接错口子或者是连接线存在问题

    启用float浮点打印

    在cubeIDE菜单栏中,Project Properties -> C/C++ Build -> Settings -> Tool Settings -> MCU Settings,勾选Use float with printf ... -nano

    Tip
    默认情况下,sprintf函数不能打印小数。因此我们需要配置一下编译器,使其能够打印小数

    中文字体乱码

    keil软件在右上角扳手处(congrations)editor——>encoding(选择UTF-8)

    CubeIDE菜单栏edit——>set encoding...选择UTF-8

    CubeMX重新生产代码中文乱码:在环境变量中添加一行配置即可解决(仅Windows下),点击开始菜单,输入“环境变量”搜索,进入系统属性设置,点击系统属性下方的“环境变量”,进入环境变量配置页面。如图,点击新建,添加一个环境变量并保存即可。

    变量名:JAVA_TOOL_OPTIONS

    变量值:-Dfile.encoding=UTF-8

    串口发送汉字乱码原因

    在串口发送函数中直接写入汉字(GB2312)可以正确发送汉字字符,但是当CubeMX进行代码重新生成后,汉字部分会乱码,此时进行重新删除写入或是keil软件设置里修改汉字编码方式都无济于事。需要关闭编译器后重新打开后写入汉字,确定码入的汉字显示为宋体,确定串口

    KEIL软件烧录设置Rest and Run失效

    在烧录设置里转到Pack设置,将Enable取消勾选image-20240623132630064

    定时器中断导致程序死机

    在定时器中断回调函数中,禁用HAL_Delay();函数,HAL_Delay();调用的是系统默认的滴答时钟,该时钟默认中断优先级为最低,通常情况下定时器中断优先级比滴答定时器优先级高,进入定时器中断后进入delay延迟,会导致程序无法返回定时器中断而死机。千万千万需要注意各个中断函数之间优先级的关系。

    LCD闪屏问题

    避免LCD_Clear(Black);代码在while循环内反复执行

    LED显示紊乱

    由于LCD与LED有部分共同引脚,因此LCD刷新显示时会对LED显示会变得紊乱。这是由于LCD刷新时 修改 GPIOC->ODR 寄存器,所以只要在LCD显示前保存LCD刷新前保存GPIOC->ODR 寄存器的值即可。详见bsp led.c部分。

    CubeMX模块配置

    基础配置:

    1. 开启HSE外部晶振

    2. 配置时钟频率

    3. 分配功能引脚

    以下配置仅为示例

    GPIO外部中断

    配置NVIC,中断优先级分组规则 Priority Group 默认为4个比特位,一般情况下不改。 勾选刚刚配置的外部中断线,并配置抢占优先级 Preemption Priority 和响应优先级 Sub Priority

    LED配置

    按键配置

    按键的引脚模式为上拉模式输入模式(GPIO_Input)

    定时器编码器配置

    在Pinout&Configuration页面,将PA8、PA9分别配置为TIM1_CH1、TIM1_CH2

    在Pinout&Configuration -> Timers -> TIM1

    定时器输入捕获配置示例

    在Pinout&Configuration -> Timers -> TIM1

    输入捕获测量占空比配置示例 另外需要打开NVIC中断 image-20240215131848344

    定时器PWM配置

    在Pinout&Configuration -> Timers -> TIM1

    ADC 规则通道单通道采集

    配置引脚功能,ADC1——>IN11——>IN11 Single-ended,配置ADC1 11通道采集

    如果只是基本使用,ADC_Settings不需要修改。

    I2C配置

    软件模拟iic,使用两个 GPIO 口来模拟 SCL 时钟线和 SDA 数据线,编写 I2C 读和写时序逻辑,

    硬件iic:(待写)

    UART串口通信配置

    轮询使用配置引脚即可,例:PA9 –>USART1_Tx,PA10 –> USART1_RX,选择同步或异步。asynchronous 异步的 synchronous 同步的 该情况下会阻塞程序运行,所以一般开启中断。一般选择异步通信。

    串口中断模式需要打开NVIC中断,串口DMA模式需要打开DMASettings,手动点击Add添加DMA通道,根据需求配置DMA,配置完后需要修改NVIC。

    RTC时钟配置

    RTC使用时不用关注其引脚分配以及设置,只要使用CubeMX配置即可。勾选Active Clock Source和Active Calendar,配置Calendar time,设置基本时间日期。

    设置RTC_PRER寄存器中的同步预分频器和异步预分频器,把时钟的频率设置为1HZ。(同步异步分频相乘要为输入的时钟频率)

    日期设置时需要注意:year字段其值只能由0-255,因此如果需要表示年,那么年份前面的两位数字我 们可以自己设置,而不需要再借助CubeMX了。例如:当我们需要设置年份为2023年时,可以在CubeMX中 将year字段设置成23,每次读取完成后就手动加上20即可。(可以使用sprintf函数)

    四、嵌入式基础

    1.TIM定时器

    定时器工作频率=外部总线频率/(PSC+1) 定时频率 = 定时器工作频率/counter(ARR) = 外部总线频率/((psc+1)*counter-1) 计数器计数频率:CK_CNT = CK_PSC / (PSC + 1) 计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)= CK_PSC / (PSC + 1) / (ARR + 1)

    定时器的从模式:经过触发输入选择器而连接到从模式控制器,从而使得计数器的工作受到从模式控制器的控制或影响

    定时器自身输入通道1或通道2的输入信号,经过极性选择和滤波以后生成的触发信号,连接到从模式控制器,进而控制计数器的工作;顺便提醒下,来自通道1的输入信号经过上升沿、下降沿双沿检测而生成的脉冲信号进行逻辑相或以后的信号就是TI1F_ED信号,即TI1F_ED双沿脉冲信号。

    外部触发脚[ETR脚]经过极性选择、分频、滤波以后的信号,经过触发输入选择器,连接到从模式控制器。

    1、复位模式 【Reset mode】

    2、触发模式 【Trigger mode】

    3、门控模式 【Gate mode】

    4、外部时钟模式1 【External clock mode 1】

    5、编码器模式 【encode mode】

    输入捕获测量占空比原理:

    1. 信号从某个通道输入,比如通道1(CH1);经过滤波和边沿检测后产生两个一模一样的信号TI1FP1和TI1FP2,TI1FP1送给捕获通道IC1,TI1FP2送给捕获通道IC2;

    2. 定时器设置为复位模式,将TI1FP1作为复位触发信号,将捕获通道IC1设置为上升沿触发,这样每当TI1FP1上升沿到来的时候,就将定时器复位;当首次检测到TI1FP1的上升沿,定时器复位,计数器CNT的值为0;

    3. IC2设置为下降沿触发,当TI1FP2的下降沿到来时,CCR2记录CNT寄存器此时的值X; 当IC1再次检测到TI1FP1上升沿的时候,CCR1记录CNT此时CNT寄存器的值Y;

    4. X可以理解为高电平持续的时间,Y可以理解为整个信号的周期,X/Y就是信号的占空比了。

    5. 基本思想就是让两个捕获通道来检测同一个信号,捕获通道IC1检测信号的上升沿,捕获通道IC2检测信号的下降沿,第一个上升沿来复位定时器,第二个上升沿来记录信号的周期值。

    image-20240215132332377

    2.ADC

    模拟信号采样成数字信号

    ADC 转换采样率(采样率):是指完成一次从模拟量转换成数字量时 ADC 所用的时间的倒 数,即每秒从连续信号中提取并转换成离散数字量的信号个数。也就是 1/ TCONV

    ADC分辨率:使用一个 16 位的 ADC 去采集一个 10V 的满量程信号(假设此 ADC 能测量 10V 的电压信号,即输入电压为 10V),这个 16 位的 ADC 满刻度(最大值) 时的数字量为 2^16=65536,当 AD 的数字量为 65536 时表示采集到了 10V,当 AD 的数字量为 256 时,表示采集到了 10V* 256 /65536 =0.0391V,此 ADC 的分辨率是10V ∗ 1 /65536 。

    转换时间:TCONV = 采样时间(TSMPL) + 逐次逼近时间(TSAR) 逐次逼近时间(TSAR)是由分辨率决定的。

    ADC 的位数越高,其分辨率就越高。可通过降低分辨率来缩短转换时间,因为转换时间缩短,我们可以做到的采样率就越高。

    ADC 输入范围:VREF– ≤ VIN ≤ VREF+。通常为0-3.3v

    当有多个通道需要采集信号时必须开启扫描模式,此时 ADC 将会按设定的顺序轮流采集各通道信号,单通道转换不需要使用此功能。

    在 ADC 的 20 个多路复用模拟通道中,可以分为规则通道组(也可以称为常规通道组)和注入通道组。规则通道组最多可以安排 16 个通道,注入通道组最多可以安排 4 个通道。我们一般使用的是规则通 道,而注入通道可以以抢占式的方式打断规则通道的采样。

    转换序列:一个常规转换组最多由 16 个转换构成。一个注入转换组最多由 4 个转换构成。常规转换必须在 ADC_SQRy(y 为 1~4)寄存器中选择转换序列的常规通道及其顺序,转换总数必须写入 ADC_SQR1 寄存器中的 L[3:0]位。注入转换必须在 ADC_JSQR 寄存器中选择转换序列的注入 通道及其顺序,转换总数必须写入 ADC_JSQR 寄存器中的 JL[1:0] 位。注入通道的转换可以打断常规通道的转换, 在注入通道被转换完成之后,常规通道才得以继续转换。

    以规则转换为例,以一个寄存器来说明,例如,ADCx_SQR1 寄存器的 SQ1[4:0] 控制着规则序列中的第 1 个转换,SQ4[4:0]控制着规则序列的第 4 个转换,如果通道 8 想在第 3 个转换,则在 SQ3[4:0]写入 8 即可,其它的寄存器也类似。

    image-20240204195307323

    End Of Conversion Selection 用于配置转换方式结束选择,可选择单通道转换完成后 EOC 标志位置位或者所有通道转换成后 EOC 置位,也可以选择转换序列结束后 EOS 置位(配置为 End of sequence of conversion)

    3.I2C

    I2C多用于板间芯片数据通信,是由数据线 SDA 和时钟线 SCL 构成的串行总线,可发送和接收数据。

    I2C 总线有如下特点:

    1. 总线由串行数据线 SDA 和串行时钟线 SCL 构成,数据线用来传输数据,时钟线用来同 步数据收发。

    2. I2C 设备都挂接到 SDA 和 SCL 这两根线上,总线上每一个设备都有一个唯一的地址识 别,即器件地址,所以 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C 设备。

    3. 数据线 SDA 和时钟线 SCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电 压,所以当总线空闲的时候,这两条线路都是高电平状态。

    4. 总线上数据的传输速率在标准模式下可达 100kbit/s ,在快速模式下可达 400kbit/s,在高速模式下可达 3.4Mbit/s

    5. 总线支持设备连接,在使用 I2C 通信总线时,可以有多个具备 I2C 通信能力的设备挂载 在上面,同时支持多个主机和多个从机。

    屏幕截图 2024-02-06 150207

    只有主机在发送开始和结束信号时,才会在时钟线为高时控制数据线。

    起始位:在 SCL 为高电平期间,SDA 出现下降沿时就表示起始位,起始信号是一种电平跳变时序信号,而不是一个电平信号。

    停止位:当 SCL 为高电平期间,SDA 出现上升沿就表示为停止位,停止信号是一种电平跳变时序信号,而不是一个电平信号。

    数据传输:在 SCL 串行时钟的配合下,在 SDA 上逐位地串行传送每一位数据。I2C 总线通过 SDA 数据线来传输数据,通过 SCL 时钟线进行数据同步,SDA 数据线在 SCL 的每个时钟周期传输一位数据。I2C 总线进行数据传送时,SCL 为高电平期间,SDA 上的数据有效;SCL 为低电平期间, SDA 上的数据无效。

    空闲状态:I2C 总线的 SDA 和 SCL 两条信号线同时处于高电平时,规定为总线的空闲状态。

    应答信号为SDA低电平,非应答信号为SDA高电平。

    写时序:主机发送起始信号,主机接着发送送从机地址+0(写操作位) 组成的 8bit 数据,对应设备地址的从机就会发出应答信号,主机向从机发送数据包,大小为8bit。主机每发送完一个字节数据,都要等待从机的应答信号。当主机向从机发送一个停止信号时,数据传输结束。

    读时序:主机发出起始信号,接着发送 从机地址+1(读操作位) 组成的 8bit 数据,对应设备地址的从机就会发出应答信号,并向主机返回 8bit 数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,直到主机发出非应答信号,从机才会停止发送数据,当主机发出非应答信号后,紧接着主机会发出停止信号,停止 I2C 通信。

    4.DMA

    DMA:直接存储器访问,作用是实现数据的直接传输,避免占用过多的CPU资源

    DMA 配置参数包括:通道地址、优先级、数据传输方向、存储器/外设数据宽度、存储器/ 外设地址、数据传输量等。

    5.RTC

    RTC本质上是一个独立的定时器,通常情况下需要外接一个32.768KHZ的晶振和匹配电容(10~33pf),由于时间是不停止的,为了满足这一要求,所以RTC实时时钟有两种供电方式:

    1)在设备正常运行的时候,RTC实时时钟模块是由MCU主电源进行供电。

    2)在主电源停止供电的时候,RTC实时时钟由备份电源(纽扣电池)来进行供电,保证当MCU停止供电的情况下,RTC不受影响,保持正常工作。

    实时时钟(RTC)模块是一个独立的BCD码定时器/计数器,除了可以正常的提供日历功能外,还可以对MCU进行唤醒。并且在MCU复位后,RTC的寄存器是不允许正常访问的(无法对RTC寄存器进行写操作,但可以进行读操作寄存器)。

    特性: (1)可以直接提供,秒,分钟,小时(12/24小时制)、星期几、日期、月份、年份的日历

    (2)具有闹钟功能,并且可以对闹钟进行日期编程。

    (3)具有自动唤醒单元,可以周期性的更新事件显示

    (4)RTC模块的中断源为:闹钟A,闹钟B,唤醒,时间戳以及入侵检测

    (5)RTC模块具有独立备份区域,可以对发生入侵事件的时间进行保存。

    RTC写保护

    1. 在系统复位后,需要把电源控制寄存器(PWR_CR)的DBP位置1,以使能RTC寄存器的写访问。

    2. 上电复位后,需要通过向写保护寄存器(RTC_WPR)写入0XCA和0x53,来解除寄存器的写保护,写入一个错误的数值(除了0xCA和0x53)会再次激活写保护。

    日历初始化和配置

    1. 首先需要把初始化状态寄存器(RTC_ISR)中的INIT位置1,进入初始化模式,在此模式下,日历计数器将停止工作并且寄存器中的值是可以被更新的。

    2. 配置为初始化模式后,RTC寄存器不能立即进入初始化状态,所以在配置为初始化模式后,必须轮询等待初始化寄存器(RTC_ISR)中的INIT位置1,才可以更新时间和日期。

    3. 设置RTC_PRER寄存器中的同步预分频器和异步预分频器,把时钟的频率设置为1HZ。(同步异步分频相乘要为输入的时钟频率)

    4. 设置RTC_TR,RTC_DR寄存器中的时间和日期,并在RTC_CR寄存器中的FMT位设置时间的格式(12小时制或24小时制)

    5. 对初始化寄存器(RTC_ISR)中的INIT位清0则退出初始化模式,当初始化模式序列完成后,日历开始计数。

    RTC闹钟配置

    1. 把控制寄存器(RTC_CR)中的闹钟A和闹钟B的使能位清零,关闭闹钟A和闹钟B。

    2. 轮询等待初始化状态寄存器(RTC_ISR)寄存器中的闹钟写入标志位置1,进入闹钟的编程模式。

    3. 根据需要,对闹钟A寄存器(RTC_ALRMAR)和闹钟B寄存器(RTC_ALRMBR)的闹钟值和产生闹钟的条件进行编译。

    4. 把控制寄存器(RTC_CR)中的闹钟A(ALRAE)和闹钟B(ALRBE)的使能位置1,使能闹钟A和闹钟B。

    5. 设置闹钟中断。

    6. 编写闹钟中断服务函数。

    读取日历

    由于日历和时间寄存器都存在影子寄存器,所以在读取时间和日历值之前,必须保证影子寄存器的数据和上层寄存器的值同步(等待日历和时间标志位被置1,RTC_ISR[5]),才能读取时间和日历寄存器。

    五、BSP适用于蓝桥杯嵌入式开发板的函数

    0.头文件写法示意.h

    可以创建一个include.h头文件,将所有本工程需要的头文件和全局变量(extern)写在里面。

    1.led.c

    硬件原理图:

    image-20240130152324553

    Warning
    由于LCD与LED的部分引脚是重合的,初始化完成LCD后,还需要强制关闭LED;操作完LCD,再次操作LED时需要重置所有LED的状态,不然 LED的工作状态就会出现问题; 每次使用LED时一定要记得将PD2拉高拉低,也就是打开关闭锁存器;

    使用上面的led_displayrollbackLedByLocation时需要注意将LED_Init写在LCD_Init,不然显示效果会出问题,同时参照下节lcd.c中修改官方库函数,在LCD刷新前保存GPIOC->ODR的值。

     

    2.lcd.c

    该部分函数来自蓝桥杯官方LCD驱动库。 LCD屏幕显示一共分为9行,Line1~Line9。在官方库函数中已经完成了对所涉及引脚的GPIO的初始化。不需要自己设置。 在屏幕上正常显示内容需要以下几行:

    下面两行代码需要放在主循环前运行,否则会导致屏幕闪屏

    LCD屏幕的宽度是0~319,一个字符占到了16.将一个字符‘a’显示在第一行第一列需要这么写:

    LCD_DisplayChar接收的是Ascaii码,如果需要显示数字,可以在数字加上48,也可以加上‘0’,进行字符转换。

    LCD与LED存在显示冲突问题。

    由于LCD与LED有部分共同引脚,因此LCD刷新显示时会对LED显示会变得紊乱。这是由于LCD刷新时 修改 GPIOC->ODR 寄存器,所以只要在LCD显示前保存LCD刷新前保存GPIOC->ODR 寄存器的值即可。经过查找,官方提供的驱动中,LCD最低层代码分别为下面三函数,因此,只要修改该三函数即可:

    修改样例

    Note
    简单来说,需要在官方提供的屏幕驱动函数有关write的部分头尾分别添加uint16_t temp = GPIOC->ODR; GPIOC->ODR = temp;两句代码,在主程序中,如果对LCD进行刷屏操作,会导致有关的LED端口被修改,如果不修改此部分,LED代码总是要对8路GPIO端口进行同时控制。

    image-20240221213408564

    3.key.c

    image-20240130153512066

    按键扫描,含按键消抖

    另一种更方便的写法:

    该种写法的示例程序:

    4.tim.c

    image-20240204125118474

    5.b-adc.c

    因为adc.c文件由CubeMX生成,所以编写自己的程序不要创建adc.c

    R37与PB15直接相连接,位于ADC2的通道IN15 R38与PB12直接相连接,位于ADC1的通道IN11

    image-20240205193606783

    获取ADC通道值的样例(单次转换模式)

    在while循环中执行该函数可实现转动旋钮改变获取的ADC值

    获取ADC多通道值的样例(单次转换模式)

    6.i2c_hal.c

    PB6——SCL;PB7——SDA

    配置IIC PB6,PB7为GPIO OutPut,在主程序里调用I2CInit();

    image-20240206221106174

    ATC02的示例代码:

    AT24C02写入语句后需要5ms延迟,以保证正确写入。

    随着我们向MCP中输入的数越大,他对应的电阻也就越大,当我们传入0x7f时,对应的电阻就是100K。这里要注意的一点是,我们写进去的一个数字(0-127),读出来也是一个数字,转化为电阻阻值:R = 787.4 * read_resistor 欧,电压:3.3*(R/(R+10)) (假设外接的电压为3.3)

    7.uart.c

    usart1串口默认配置是PC4、PC5,在这里我们要将其改成PA9、PA10;usart.c文件由cubemx配置生成

    串口接收指定内容,返回参数,主程序需加HAL_UART_Receive_IT(&huart1,(uint8_t *)&rxbuff,1);

     

    8.rtc.c


    结语

    这是一份STM32系列单片机的HAL库学习手册,也提供了详尽的蓝桥杯嵌入式赛道的基于STM32G431RBT6官方开发平台的示例代码。

    祝您学业有成!

    🏠 我的博客