1、GPIO编程总结
- 使能 GPIO 端口时钟;
- 初始化 GPIO 目标引脚为推挽输出模式;
- 编写简单测试程序,控制 GPIO 引脚输出高、低电平。
这部分宏控制 LED 亮灭的操作是直接向 BSRR 寄存器写入控制指令来实现的,对 BSRR 低 16 位 写 1 输出高电平,对 BSRR 高 16 位写 1 输出低电平,对 ODR 寄存器某位进行异或操作可反转位
的状态。
HAL_GPIO_Init 函数向寄存器写入参数,完成 GPIO 的初始化。
所有程序都必须设置好系统的时钟再进行其他操作。
库自带基于滴答时钟延时 HAL_Delay 单位为 ms,直接调用即可。
__HAL_RCC_GPIOB_CLK_ENABLE() 开始GPIOB外设时钟。
SystemInit 函数在 STM32 HAL 库的“system_stm32f1xx.c”文件中定义了,而我们的工程已经包含该文
件。
“assert_param”实际是一个宏,在库函数中它用于检查输入参数是否符合要求,若不符合要求则执行某个函数输出警告。
这段代码的意思是,假如我们不定义“USE_FULL_ASSERT”宏,那么“assert_param”就是一个
空的宏 (#else 与 #endif 之间的语句生效),没有任何操作。从而所有库函数中的 assert_param 实际
上都无意义,我们就当看不见好了。
假如我们定义了“USE_FULL_ASSERT”宏,那么“assert_param”就是一个有操作的语句 (#if 与
#else 之间的语句生效),该宏对参数 expr 使用 C 语言中的问号表达式进行判断,若 expr 值为
真,则无操作 (void 0),若表达式的值为假,则调用“assert_failed”函数,且该函数的输入参数为
“FILE”及“LINE”,这两个参数分别代表“assert_param”宏被调用时所在的“文件名”
及“行号”。
但库文件只对“assert_failed”写了函数声明,没有写函数定义,实际用时需要用户来定义,我们
一般会用 printf 函数来输出这些信息,
%p打印的是指针的地址,%s打印的是指针指向的值。
#include<stdio.h>
char a[] = “Hello World”;
void test(char* b)
{
printf(“%p\n”,b);
printf(“%p\n”,&b);
}
int main()
{
printf(“%p\n”,a);
printf(“%p\n”,&a[0]);
printf(“%p\n”,&a);
test(a);
return 0;
}
2、这 是 一 种 名 为 “Doxygen” 的 注 释 规 范, 如 果 在 工 程 文 件 中 按 照 这 种 规 范 去 注 释, 可以使用 Doxygen 软件自动根据注释生成帮助文档。我们所说非常重要的库帮助文档
《STM32F103xx_User_Manual.chm》,就是由该软件根据库文件的注释生成的。关于 Doxygen 注
释规范本教程不作讲解。
3、#ifndef __LED_H
#define __LED_H
/* 此处省略头文件的具体内容 /
#endif / end of __LED_H */
在头文件的开头,使用“#ifndef”关键字,判断标号“__LED_H”是否被定义,若没有被定义,则从
“#ifndef”至“#endif”关键字之间的内容都有效,也就是说,这个头文件若被其它文件“#include”,
它就会被包含到其该文件中了,且头文件中紧接着使用“#define”关键字定义上面判断的标号
“__LED_H”。当这个头文件被同一个文件第二次“#include”包含的时候,由于有了第一次包含中
的“#define __LED_H”定义,这时再判断“#ifndef __LED_H”,判断的结果就是假了,从“#ifndef”至加粗样式“#endif”之间的内容都无效,从而防止了同一个头文件被包含多次,编译时就不会出现“redefine
(重复定义)”的错误了。
一般来说,我们不会直接在 C 的源文件写两个“#include”来包含同一个头文件,但可能因为头文
件内部的包含导致重复,这种代码主要是避免这样的问题。如“bsp_led.h”文件中使用了“#include
“stm32F103xx.h””语句,按习惯,可能我们写主程序的时候会在 main 文件写“#include“bsp_led.h” 及 #include“stm32F103xx.h””,这个时候“stm32F103xx.h”文件就被包含两次了,如果没有这种
机制,就会出错。
至于为什么要用两个下划线来定义“__LED_H”标号,其实这只是防止它与其它普通宏定义
重复了,如我们用“GPIO_PIN_0”来代替这个判断标号,就会因为 stm32F103xx.h 已经定义了
GPIO_PIN_0,结果导致“bsp_led.h”文件无效了,“bsp_led.h”文件一次都没被包含。
4、GPIO 输入—按键检测
按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一下子断开,
使用按键时会产生图 13_1 中的带波纹信号,需要用软件消抖处理滤波,不方便输入检测。本实
验板连接的按键带硬件消抖功能,见图 13_2a 和图 13_2b,它利用电容充放电的延时,消除了波
纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。
从按键的原理图可知,这些按键在没有被按下的时候,GPIO 引脚的输入状态为低电平 (按键所在
的电路不通,引脚接地),当按键按下时,GPIO 引脚的输入状态为高电平 (按键所在的电路导通,
引脚接到电源)。只要我们检测引脚的输入电平,即可判断按键是否被按下。
编程要点
- 使能 GPIO 端口时钟;
- 初始化 GPIO 目标引脚为输入模式 (浮空输入);
- 编写简单测试程序,检测按键的状态,实现按键控制 LED 灯。
在这里我们定义了一个 Key_Scan 函数用于扫描指定按键的状态。GPIO 引脚的输入电平可通过读
取 IDR 寄存器对应的数据位来感知,而 STM32 HAL 库提供了库函数 HAL_GPIO_ReadPin 来获取
位状态,该函数输入 GPIO 端口及引脚号,函数返回该引脚的电平状态,高电平返回 1,低电平
返回 0。Key_Scan 函数中以 HAL_GPIO_ReadPin 的返回值与自定义的宏“KEY_ON”对比,若检
测到按键按下,则使用 while 循环持续检测按键状态,直到按键释放,按键释放后 Key_Scan 函数
返回一个“KEY_ON”值;若没有检测到按键按下,则函数直接返回“KEY_OFF”。若按键的硬
件没有做消抖处理,需要在这个 Key_Scan 函数中做软件滤波,防止波纹抖动引起误触发。
5、启动文件简介
启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作: - 初始化堆栈指针 SP(__initial_sp)
- 初始化 PC 指针(Reset_Handler)
- 初始化中断向量表(__Vectors)
- 配置系统时钟(SystemInit)
- 调用 C 库函数 _main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界。
启动文件使用的 ARM 汇编指令汇总
IF,ELSE,ENDIF 汇编条件分支语句,跟 C 语言的 if else 类似
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部 SRAM 的大
小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你
写的程序出现了莫名奇怪的错误,并进入了硬 fault 的时候,这时你就要考虑下是不是栈不够大,
溢出了。栈是由高向低生长的。
堆主要用来动态内存的分配,像 malloc() 函数申请的内存就在堆上面。这个在 STM32 里面用的
比较少。堆是由低向高生长的,跟栈的生长方向相反。
6、中断服务程序
在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一
样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这
里只是提前占了一个位置而已。
如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函
数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且
在这个空函数中无线循环,即程序就死在这里。