我DIY用了好几款STM32了(碰巧都是F0和F4系列的,没有用F1系列。F1系列的GPIO寄存器表有所不同,不能直接用本文的代码),每新做一块PCB,或者在Nucleo上试不同的应用,差不多都要把I/O口配置的代码重新写一次。或者不完全重写,拿已有的程序来改,核对每一个用到的管脚的连接,还是会消耗工夫。引脚最重要的属性是作为输入还是输出用,还是作为复用功能,这通常在画电路图的时候就安排好了,写程序只是对照设计文档来做。
STM32的GPIO模块由MODER寄存器决定引脚的功能,即四种选择:输出/输入/复用功能/模拟。16个引脚用1个32-bit的寄存器定义,每个引脚占2 bits, 默认00是输入功能。我常常是类似这么写的:
GPIOA->MODER = GPIO_MODER_MODER14_1|GPIO_MODER_MODER13_1 // PA14, PA13 AF (SWD), other input
|GPIO_MODER_MODER10_1|GPIO_MODER_MODER9_1 // PA10, PA9 AF (UART)
|GPIO_MODER_MODER8_0; // PA8 (LED)
对 MODER 寄存器初始化针对非数字输入用途的引脚(默认输入就不用配了),如果设成输出就要把对应2 bits设成01, 复用功能设成 10, 模拟用途则设为 11, 可以分别用 stm32fxxxx.h 头文件里面的 GPIO_MODER_MODERy_1, GPIO_MODER_MODERy_0, 以及 GPIO_MODER_MODERy 宏定义来书写。不用宏定义,上面这段也可以写成
GPIOA->MODER = 2<<28|2<<26|2<<20|2<<18|1<<16;
简洁了不少,但是可读性下降,因为 28, 26, 20, 18, 16 这几个数字没有直接对应端口号,需要大脑换算。不过嘛,这总比写成
GPIOA->MODER = 0x28290000;
的可读性强多了,直接写十六进值数的代码是很难排错和重用的(当然,要写成十进制的话……
)
类似 MODER 寄存器的还有设置上拉下拉的 PUPDR 寄存器,设置输出翻转速度的 OSPEEDR 寄存器。不过,重要性仅次于 MODER 寄存器的是设置引脚复用的具体功能。因为在 STM32 上,每个引脚最多可能有 16 种特殊硬件功能的复用选择,这在手册上会以表格列出,如下图这样。AF (Alternate Function)的编号从0到15, 在 AFRH 和 AFRL 寄存器中每4 bits用来指定一个引脚的复用功能选择,如果在配置 MODER 该引脚为复用功能。
单独阅读代码,是不能从 AFRx 寄存器的值反推出复用功能是哪个的。MCU上的硬件模块太多了。于是我在写程序的时候特意填加了注释。关于 AFRx 寄存器, stm32fxxxx.h 头文件里面的宏起不到什么帮助,反而是直接写十六进制数最直观:因为十六进制一位数就是4比特,例如
GPIOA->AFR[1]=0x000AA000 // PA14,13 as SWD, PA12,11 as USB
|0x00000770 // PA10,9 as USART1
|0x0000000C; // PA8 as SDIO
我把不同组的功能分散在几行来写,以便于添加注释,以后删改也容易一些。但是若写错了AF号,不对照手册也是不能发现的。
从网上找来的例子中,GPIO配置部分可能这么来写:
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource6,GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOB,GPIO_PinSource7,GPIO_AF_USART1);
我自己不会喜欢这样的代码,第一是绕了圈子,把简单的东西复杂化了,做了太多不必要的寄存器和内存操作;第二是源代码长度也增加了,需要多敲键盘,虽然读起来知道每一行写的要干什么。对于 AF 功能选择,用库函数也没有提供任何帮助,像上面 GPIO_AF_USART1 这个宏定义,如果用错了Pin位置也依然无法查错。
我想很少有人像我这样手动写寄存器来配置 GPIO 吧
…… 我猜想大多数人用的是图形化的工具来配的,然后,就由软件直接生成代码了…… 根本不是自己敲进去的
但是我还要坚持,也许是我难接受新生事物,呃——我从Visual C++开始就不喜欢IDE环境,偏好命令行操作和Makefile, 坚持把源代码和其它数据分开。我希望代码就是书写出来的,具有可读性的,容易维护的。
今天整了一天,算是有所改进了。这个办法是针对 STM32 的,这个思想也可以移用在其它 MCU 平台。我的设想是:用 #define 定义宏来指定复用功能,以及引脚的功能选择和其它属性。
例如,想把 PA9 设置为 USART1_TX 这个复用功能,就定义
#define ASSIGN_USART1_TX_PA9
当然,PA9 必须要具有这个选项才可以用,否则定义了也无效。类似的,定义
#define ASSIGN_SPI1_MOSI_PA7
来打开 PA7 的复用功能,设为 SPI1_MOSI. 对一般的输出引脚设定,支持如下的宏
#define USE_PB1_OUTPUT
#define USE_PA0_INPUT
#define USE_PC0_ANALOG
#define USE_PA0_PULLUP
#define USE_PB1_OPENDRAIN
分别设置输出模式、输入模式、模拟功能,还可以设置上拉、设置开漏输出。注意,仅仅是宏定义,不需要写任何操作寄存器的代码。只需调用一次 gpio_config() 函数即可完成所有 GPIO 口的初始设置。这个函数也是写好的,针对一个器件源程序是固定不变的(当然编译结果因配置而变)。
按这个起初的想法实践了,我发现一些问题:一旦在使用的时候拼写出错,那么定义就无效,期望的设置没有达到,然而编译器不会有任何错误或警告——因为定义一个不被用到的宏和没有定义是一样的。
于是为了防止手误,我要求在使用复用功能的时候,除了使用上面 #define ASSIGN_SPI1_MOSI_PA7 这样的宏之外,还必须再定义 #define USE_PA7_ALTFUNC 指定功能,一旦缺其一就会有错,算是保险一些了。副作用是又不那么简洁了。
不过终归是语句越长越容易拼写错,我偶然把下划线漏了敲了空格都没有一下子发现。后来,又把上面那段定义方式修改为这样:
#define USE_PB1 PIN_OUT | PIN_OD
#define USE_PA0 PIN_IN | PIN_PULLUP
#define USE_PC0 PIN_ANA
因为 USE_PB1 这样短的标识符拼错的概率就大大降低了。再用逻辑或组合预定义的值来实现选择功能,更紧凑一些。不过,设置复用功能仍然需要两个 #define ,占用两行代码。
下面是调试过的一个例子程序,关于 GPIO 配置的部分:
- // file: gpio_config.c
- #include "stm32f0xx.h"
- #include "gpiodef.h"
-
- #define USE_PA3 PIN_OUT
- #define USE_PA6 PIN_OUT
-
- #define USE_PA13 PIN_AF
- #define ASSIGN_SWDIO_PA13
-
- #define USE_PA14 PIN_AF
- #define ASSIGN_SWCLK_PA14
-
- #define USE_PA9 PIN_AF
- #define ASSIGN_USART1_TX_PA9
-
- #define USE_PA10 PIN_AF|PIN_PULLUP
- #define ASSIGN_USART1_RX_PA10
-
- #define USE_PA7 PIN_AF
- #define ASSIGN_SPI1_MOSI_PA7
-
- #define USE_PA5 PIN_AF
- #define ASSIGN_SPI1_SCK_PA5
-
- #define USE_PA4 PIN_AF
- #define ASSIGN_SPI1_NSS_PA4
-
- #include "gpiodef.c"
这个 C 文件包含了一个 .h 文件,其中定义了 PIN_OUT, PIN_AF, PIN_PULLUP 这样的宏;然后用 #define 来书写需要用到的I/O引脚,没有写的会默认成模拟功能。最后一行 #include 的文件里面,才包含产生机器代码的地方。这段代码编译之后,产生一个函数 gpio_config(), 机器代码如下:
- 00000000 <gpio_config>:
- 0: 4b0c ldr r3, [pc, #48] ; (34 <gpio_config+0x34>)
- 2: 229c movs r2, #156 ; 0x9c
- 4: 6959 ldr r1, [r3, #20]
- 6: 03d2 lsls r2, r2, #15
- 8: 430a orrs r2, r1
- a: 615a str r2, [r3, #20]
- c: 4a0a ldr r2, [pc, #40] ; (38 <gpio_config+0x38>)
- e: 2301 movs r3, #1
- 10: 425b negs r3, r3
- 12: 6013 str r3, [r2, #0]
- 14: 4909 ldr r1, [pc, #36] ; (3c <gpio_config+0x3c>)
- 16: 2290 movs r2, #144 ; 0x90
- 18: 05d2 lsls r2, r2, #23
- 1a: 6011 str r1, [r2, #0]
- 1c: 2188 movs r1, #136 ; 0x88
- 1e: 0049 lsls r1, r1, #1
- 20: 6251 str r1, [r2, #36] ; 0x24
- 22: 2180 movs r1, #128 ; 0x80
- 24: 0349 lsls r1, r1, #13
- 26: 60d1 str r1, [r2, #12]
- 28: 4a05 ldr r2, [pc, #20] ; (40 <gpio_config+0x40>)
- 2a: 6013 str r3, [r2, #0]
- 2c: 4a05 ldr r2, [pc, #20] ; (44 <gpio_config+0x44>)
- 2e: 6013 str r3, [r2, #0]
- 30: 4770 bx lr
- 32: 46c0 nop ; (mov r8, r8)
- 34: 40021000 .word 0x40021000
- 38: 48001400 .word 0x48001400
- 3c: ebeb9a7f .word 0xebeb9a7f
- 40: 48000400 .word 0x48000400
- 44: 48000800 .word 0x48000800
因为 gpiodef.c 这个文件很冗长(当然了,不是手写出来的),通篇是条件编译命令,这里只能看编译结果了。代码还是很短的,其实就是直接操作 MODER, AFRH, AFRL, PUPDR 这些寄存器。在下面的回贴里面,我会详细讲述实现的方法细节。
大伙对我这个设计怎么看?是否能做得更简洁?
本帖最后由 cruelfox 于 2017-1-16 17:18 编辑