我DIY用了好几款STM32了(碰巧都是F0和F4系列的,没有用F1系列。F1系列的GPIO寄存器表有所不同,不能直接用本文的代码),每新做一块PCB,或者在Nucleo上试不同的应用,差不多都要把I/O口配置的代码重新写一次。或者不完全重写,拿已有的程序来改,核对每一个用到的管脚的连接,还是会消耗工夫。引脚最重要的属性是作为输入还是输出用,还是作为复用功能,这通常在画电路图的时候就安排好了,写程序只是对照设计文档来做。
STM32的GPIO模块由MODER寄存器决定引脚的功能,即四种选择:输出/输入/复用功能/模拟。16个引脚用1个32-bit的寄存器定义,每个引脚占2bits,默认00是输入功能。我常常是类似这么写的:
GPIOA-MODER=GPIO_MODER_MODER14_1
GPIO_MODER_MODER13_1//PA14,PA13AF(SWD),otherinput
GPIO_MODER_MODER10_1
GPIO_MODER_MODER9_1//PA10,PA9AF(UART)
GPIO_MODER_MODER8_0;//PA8(LED)
对MODER寄存器初始化针对非数字输入用途的引脚(默认输入就不用配了),如果设成输出就要把对应2bits设成01,复用功能设成10,模拟用途则设为11,可以分别用stm32fxxxx.h头文件里面的GPIO_MODER_MODERy_1,GPIO_MODER_MODERy_0,以及GPIO_MODER_MODERy宏定义来书写。不用宏定义,上面这段也可以写成
GPIOA-MODER=
;
简洁了不少,但是可读性下降,因为28,26,20,18,16这几个数字没有直接对应端口号,需要大脑换算。不过嘛,这总比写成
GPIOA-MODER=0x;
的可读性强多了,直接写十六进值数的代码是很难排错和重用的(当然,要写成十进制的话……)
类似MODER寄存器的还有设置上拉下拉的PUPDR寄存器,设置输出翻转速度的OSPEEDR寄存器。不过,重要性仅次于MODER寄存器的是设置引脚复用的具体功能。因为在STM32上,每个引脚最多可能有16种特殊硬件功能的复用选择,这在手册上会以表格列出,如下图这样。AF(AlternateFunction)的编号从0到15,在AFRH和AFRL寄存器中每4bits用来指定一个引脚的复用功能选择,如果在配置MODER该引脚为复用功能。
单独阅读代码,是不能从AFRx寄存器的值反推出复用功能是哪个的。MCU上的硬件模块太多了。于是我在写程序的时候特意填加了注释。关于AFRx寄存器,stm32fxxxx.h头文件里面的宏起不到什么帮助,反而是直接写十六进制数最直观:因为十六进制一位数就是4比特,例如
GPIOA-AFR[1]=0xAA//PA14,13asSWD,PA12,11asUSB
0x00770//PA10,9asUSART1
0x0C;//PA8asSDIO
我把不同组的功能分散在几行来写,以便于添加注释,以后删改也容易一些。但是若写错了AF号,不对照手册也是不能发现的。
从网上找来的例子中,GPIO配置部分可能这么来写:GPIO_InitTypeDefGPIO_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吧……我猜想大多数人用的是图形化的工具来配的,然后,就由软件直接生成代码了……根本不是自己敲进去的
但是我还要坚持,也许是我难接受新生事物,呃——我从VisualC++开始就不喜欢IDE环境,偏好命令行操作和Makefile,坚持把源代码和其它数据分开。我希望代码就是书写出来的,具有可读性的,容易维护的。
今天整了一天,算是有所改进了。这个办法是针对STM32的,这个思想也可以移用在其它MCU平台。我的设想是:用#define定义宏来指定复用功能,以及引脚的功能选择和其它属性。
例如,想把PA9设置为USART1_TX这个复用功能,就定义#defineASSIGN_USART1_TX_PA9当然,PA9必须要具有这个选项才可以用,否则定义了也无效。类似的,定义#defineASSIGN_SPI1_MOSI_PA7来打开PA7的复用功能,设为SPI1_MOSI.对一般的输出引脚设定,支持如下的宏#defineUSE_PB1_OUTPUT#defineUSE_PA0_INPUT#defineUSE_PC0_ANALOG#defineUSE_PA0_PULLUP#defineUSE_PB1_OPENDRAIN分别设置输出模式、输入模式、模拟功能,还可以设置上拉、设置开漏输出。注意,仅仅是宏定义,不需要写任何操作寄存器的代码。只需调用一次gpio_config()函数即可完成所有GPIO口的初始设置。这个函数也是写好的,针对一个器件源程序是固定不变的(当然编译结果因配置而变)。按这个起初的想法实践了,我发现一些问题:一旦在使用的时候拼写出错,那么定义就无效,期望的设置没有达到,然而编译器不会有任何错误或警告——因为定义一个不被用到的宏和没有定义是一样的。
于是为了防止手误,我要求在使用复用功能的时候,除了使用上面#defineASSIGN_SPI1_MOSI_PA7这样的宏之外,还必须再定义#defineUSE_PA7_ALTFUNC指定功能,一旦缺其一就会有错,算是保险一些了。副作用是又不那么简洁了。不过终归是语句越长越容易拼写错,我偶然把下划线漏了敲了空格都没有一下子发现。后来,又把上面那段定义方式修改为这样:#defineUSE_PB1PIN_OUT
PIN_OD#defineUSE_PA0PIN_IN
PIN_PULLUP#defineUSE_PC0PIN_ANA因为USE_PB1这样短的标识符拼错的概率就大大降低了。再用逻辑或组合预定义的值来实现选择功能,更紧凑一些。不过,设置复用功能仍然需要两个#define,占用两行代码。
下面是调试过的一个例子程序,关于GPIO配置的部分:
//file:gpio_config.c
#include"stm32f0xx.h"
#include"gpiodef.h"
#defineUSE_PA3PIN_OUT
#defineUSE_PA6PIN_OUT
#defineUSE_PA13PIN_AF
#defineASSIGN_SWDIO_PA13
#defineUSE_PA14PIN_AF
#defineASSIGN_SWCLK_PA14
#defineUSE_PA9PIN_AF
#defineASSIGN_USART1_TX_PA9
#defineUSE_PA10PIN_AF
PIN_PULLUP
#defineASSIGN_USART1_RX_PA10
#defineUSE_PA7PIN_AF
#defineASSIGN_SPI1_MOSI_PA7
#defineUSE_PA5PIN_AF
#defineASSIGN_SPI1_SCK_PA5
#defineUSE_PA4PIN_AF
#defineASSIGN_SPI1_NSS_PA4
#include"gpiodef.c"
这个C文件包含了一个.h文件,其中定义了PIN_OUT,PIN_AF,PIN_PULLUP这样的宏;然后用#define来书写需要用到的I/O引脚,没有写的会默认成模拟功能。最后一行#include的文件里面,才包含产生机器代码的地方。这段代码编译之后,产生一个函数gpio_config(),机器代码如下:
00gpio_config:
0:4b0cldrr3,[pc,#48];(34gpio_config+0x34)
2:cmovsr2,#;0x9c
4:ldrr1,[r3,#20]
6:03d2lslsr2,r2,#15
8:aorrsr2,r1
a:astrr2,[r3,#20]
c:4a0aldrr2,[pc,#40];(38gpio_config+0x38)
e:movsr3,#1
10:bnegsr3,r3
12:strr3,[r2,#0]
14:ldrr1,[pc,#36];(3cgpio_config+0x3c)
16:0movsr2,#;0x90
18:05d2lslsr2,r2,#23
1a:strr1,[r2,#0]
1c:8movsr1,#;0x88
1e:lslsr1,r1,#1
20:strr1,[r2,#36];0x24
22:0movsr1,#;0x80
24:lslsr1,r1,#13
26:60d1strr1,[r2,#12]
28:4a05ldrr2,[pc,#20];(40gpio_config+0x40)
2a:strr3,[r2,#0]
2c:4a05ldrr2,[pc,#20];(44gpio_config+0x44)
2e:strr3,[r2,#0]
30:bxlr
32:46c0nop;(movr8,r8)
34:40021.word0x40021
38:.word0x
3c:ebeb9a7f.word0xebeb9a7f
40:48400.word0x48400
44:48800.word0x48800
因为gpiodef.c这个文件很冗长(当然了,不是手写出来的),通篇是条件编译命令,这里只能看编译结果了。代码还是很短的,其实就是直接操作MODER,AFRH,AFRL,PUPDR这些寄存器。
gpiodef.c这个文件是个模板,把所有可能用到的AF设置都要包含进去。具体在编译的时候,根据用到的引脚来确定寄存器的值。在gpio_config()函数里面,可以这么写:
GPIOA-MODER=GPIOA_MODER15
GPIOA_MODER14
......GPIOA_MODER0GPIOA-AFRL=GPIOA_AFR7
GPIOA_AFR6
......GPIOA_AFR0每个引脚的值再用一个宏定义。这里的宏定义不是写程序的时候直接写的,而是由条件编译确定。例如,想配置PA4为SPI1_NSS功能,在主程序中写定义#defineASSIGN_SPI1_NSS_PA4而在后面处理这个宏(gpiodef.c里面)的时候可以这么来做:#ifdefASSIGN_SPI1_NSS_PA4#defineGPIOA_AFR#endif条件编译指令判断到ASSIGN_SPI1_NSS_PA4被定义了,于是定义GPIOA_AFR4这个宏,值是0.这样GPIOA-AFRL的相关位就得到了定义。类似的,如果想配置PA4为USART2_CK功能,在就主程序中定义#defineASSIGN_USART2_CK_PA4而gpiodef.c里面因为还有#ifdefASSIGN_USART2_CK_PA4#defineGPIOA_AFR4#endif这段,就会把GPIOA_AFR4宏定义,值为1。
注意,如果PA4引脚没有定义AF功能,那么GPIOA_AFR4这个宏就不会被定义,这样后面编译不能通过。所以还需要补充一个默认值:#if!definedGPIOA_AFR4#defineGPIOA_AFR40#endif这样如果发现没有定义必要的宏,就定义一个默认值。前帖中已说过,这样简单处理的问题是如果不小心把宏定义拼错了,就不会被条件编译指定判断到,导致不希望的结果但是没有错误和警告。所以还需要加一重保险,在我现在的代码里面,实际是这样的:#ifdefASSIGN_USART2_CK_PA4#ifdefENABLE_PA4_AF#error"PA4:multipleAFassignments"#else#defineENABLE_PA4_AF1#endif#endif这里不直接定义GPIOA_AFR4这个宏,而先定义ENABLE_PA4_AF,并判断是否已定义(避免写了两个复用功能分配到同一个引脚)。然后,在处理USE_PA4宏的时候,和PIN_AF宏做双重检查:#ifdefinedUSE_PA4#ifdefENABLE_PA4_AF#if!((USE_PA4)PIN_AF)#error"PA4use:PIN_AFnotset"#endif#defineGPIOA_MODER4GPIO_MODER_BF(MODE_AF,4)#defineGPIOA_AFR4GPIO_AFR_BF(ENABLE_PA4_AF,4)#else#defineGPIOA_AFR40#if(USE_PA4)PIN_OUT#defineGPIOA_MODER4GPIO_MODER_BF(MODE_OUTPUT,4)#elif(USE_PA4)PIN_IN#defineGPIOA_MODER4GPIO_MODER_BF(MODE_INPUT,4)#elif(USE_PA4)PIN_ANALOG#defineGPIOA_MODER4GPIO_MODER_BF(MODE_ANALOG,4)#else#error"PA4use:modeundefined"#endif#endif#if(USE_PA4)PIN_PULLUP#defineGPIOA_PUPDR4GPIO_PUPDR_BF(PULL_UP,4)#elif(USE_PA4)PIN_PULLDOWN#defineGPIOA_PUPDR4GPIO_PUPDR_BF(PULL_DOWN,4)#else#defineGPIOA_PUPDR40#endif#if(USE_PA4)PIN_OPENDRAIN#defineGPIOA_OTYPER#else#defineGPIOA_OTYPER40#endif#else#defineGPIOA_MODER4GPIO_MODER_BF(MODE_ANALOG,4)#defineGPIOA_AFR40#defineGPIOA_PUPDR40#defineGPIOA_OTYPER40#endif这也是条件编译最关键的一段。为了能工作,在gpiodef.h中事先定义一些必要的宏:#definePIN_AF0x8#definePIN_OUT0x4#definePIN_IN0x2#definePIN_ANALOG0x1#definePIN_PULLUP0x#definePIN_PULLDOWN0x#definePIN_OPENDRAIN0x用它们来书写USE_Pxy宏的值,再到条件编译指令中用逻辑与去判断。一旦定义了USE_PA4,但是后面的值没有PIN_AF,PIN_OUT,PIN_IN,PIN_ANALOG当中的一个(比如因为拼写错误的原因),就会发生错误。而且,如果使用了PIN_AF,但没有定义某个有效的ASSIGN_xxx_yyy_PA4这样的宏(比如因为拼写错误,比如写成不存在的功能),也会检测到错误。以及如果定义了有效的ASSIGN_xxx_yyy_PA4,却没有定义USE_PA4为PIN_AF,也会报错。这样可以减少大部分的程序书写错误。
针对每一个器件上存在的GPIO,都需要对16个PIN写上面的代码(当然不是手写,用程序在自动生成就可以了,但AF功能是要一个一个描述的)。还有,对GPIO寄存器的初始化,如果用的是默认值(大部分都是0),那么这个寄存器写操作就可以去掉了,也用条件编译指令预先判断一下。点击阅读原文可以看到附件是我做好的STM32FC8的相关文件。大伙对我这个设计怎么看?是否能做得更简洁?
推荐阅读
干货
阅板无数,这块ARM板吸引我的不仅仅是颜值(上)
干货
阅板无数,这块ARM板吸引我的不仅仅是颜值(下)
干货
MCUXpresso的编码和调试
干货
常见RF指标的内在和意义
干货
教你DIY低成本物联网控制盒子
干货
小议运放构成的放大器的频响与稳定性
干货
讨厌的电感啸叫!别急,消除TA只需这三招儿
干货
米勒效应杂谈
干货
浅谈如何使用RL_RTX
干货
在STM32F-Disco上跑Basic体验AppleⅡ
干货
关于矩阵键盘,使用电子表格辅助编程
干货
DIY定时恒温饭盒
预览时标签不可点收录于合集#个上一篇下一篇