[原创] 探讨下怎样让I/O口配置代码更简洁

cruelfox   2017-1-14 23:38 楼主
我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 该引脚为复用功能。 AF.PNG 单独阅读代码,是不能从 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 配置的部分:
  1. // file: gpio_config.c
  2. #include "stm32f0xx.h"
  3. #include "gpiodef.h"
  4. #define USE_PA3 PIN_OUT
  5. #define USE_PA6 PIN_OUT
  6. #define USE_PA13 PIN_AF
  7. #define ASSIGN_SWDIO_PA13
  8. #define USE_PA14 PIN_AF
  9. #define ASSIGN_SWCLK_PA14
  10. #define USE_PA9 PIN_AF
  11. #define ASSIGN_USART1_TX_PA9
  12. #define USE_PA10 PIN_AF|PIN_PULLUP
  13. #define ASSIGN_USART1_RX_PA10
  14. #define USE_PA7 PIN_AF
  15. #define ASSIGN_SPI1_MOSI_PA7
  16. #define USE_PA5 PIN_AF
  17. #define ASSIGN_SPI1_SCK_PA5
  18. #define USE_PA4 PIN_AF
  19. #define ASSIGN_SPI1_NSS_PA4
  20. #include "gpiodef.c"
这个 C 文件包含了一个 .h 文件,其中定义了 PIN_OUT, PIN_AF, PIN_PULLUP 这样的宏;然后用 #define 来书写需要用到的I/O引脚,没有写的会默认成模拟功能。最后一行 #include 的文件里面,才包含产生机器代码的地方。这段代码编译之后,产生一个函数 gpio_config(), 机器代码如下:
  1. 00000000 <gpio_config>:
  2. 0: 4b0c ldr r3, [pc, #48] ; (34 <gpio_config+0x34>)
  3. 2: 229c movs r2, #156 ; 0x9c
  4. 4: 6959 ldr r1, [r3, #20]
  5. 6: 03d2 lsls r2, r2, #15
  6. 8: 430a orrs r2, r1
  7. a: 615a str r2, [r3, #20]
  8. c: 4a0a ldr r2, [pc, #40] ; (38 <gpio_config+0x38>)
  9. e: 2301 movs r3, #1
  10. 10: 425b negs r3, r3
  11. 12: 6013 str r3, [r2, #0]
  12. 14: 4909 ldr r1, [pc, #36] ; (3c <gpio_config+0x3c>)
  13. 16: 2290 movs r2, #144 ; 0x90
  14. 18: 05d2 lsls r2, r2, #23
  15. 1a: 6011 str r1, [r2, #0]
  16. 1c: 2188 movs r1, #136 ; 0x88
  17. 1e: 0049 lsls r1, r1, #1
  18. 20: 6251 str r1, [r2, #36] ; 0x24
  19. 22: 2180 movs r1, #128 ; 0x80
  20. 24: 0349 lsls r1, r1, #13
  21. 26: 60d1 str r1, [r2, #12]
  22. 28: 4a05 ldr r2, [pc, #20] ; (40 <gpio_config+0x40>)
  23. 2a: 6013 str r3, [r2, #0]
  24. 2c: 4a05 ldr r2, [pc, #20] ; (44 <gpio_config+0x44>)
  25. 2e: 6013 str r3, [r2, #0]
  26. 30: 4770 bx lr
  27. 32: 46c0 nop ; (mov r8, r8)
  28. 34: 40021000 .word 0x40021000
  29. 38: 48001400 .word 0x48001400
  30. 3c: ebeb9a7f .word 0xebeb9a7f
  31. 40: 48000400 .word 0x48000400
  32. 44: 48000800 .word 0x48000800
因为 gpiodef.c 这个文件很冗长(当然了,不是手写出来的),通篇是条件编译命令,这里只能看编译结果了。代码还是很短的,其实就是直接操作 MODER, AFRH, AFRL, PUPDR 这些寄存器。在下面的回贴里面,我会详细讲述实现的方法细节。 大伙对我这个设计怎么看?是否能做得更简洁? 本帖最后由 cruelfox 于 2017-1-16 17:18 编辑

回复评论 (43)

2推荐 cruelfox 

gpiodef.c 这个文件是个模板,把所有可能用到的AF设置都要包含进去。具体在编译的时候,根据用到的引脚来确定寄存器的值。在 gpio_config() 函数里面,可以这么写: GPIOA->MODER = GPIOA_MODER15|GPIOA_MODER14|......GPIOA_MODER0 GPIOA->AFRL = GPIOA_AFR7|GPIOA_AFR6|......GPIOA_AFR0 每个引脚的值再用一个宏定义。这里的宏定义不是写程序的时候直接写的,而是由条件编译确定。例如,想配置 PA4 为 SPI1_NSS 功能,在主程序中写定义 #define ASSIGN_SPI1_NSS_PA4 而在后面处理这个宏(gpiodef.c里面)的时候可以这么来做: #ifdef ASSIGN_SPI1_NSS_PA4 #define GPIOA_AFR4 0<<16 #endif 条件编译指令判断到 ASSIGN_SPI1_NSS_PA4 被定义了,于是定义 GPIOA_AFR4 这个宏,值是 0. 这样 GPIOA->AFRL 的相关位就得到了定义。类似的,如果想配置 PA4 为 USART2_CK 功能,在就主程序中定义 #define ASSIGN_USART2_CK_PA4 而 gpiodef.c 里面因为还有 #ifdef ASSIGN_USART2_CK_PA4 #define GPIOA_AFR4 1<<16 #endif 这段,就会把 GPIOA_AFR4 宏定义,值为1。 注意,如果 PA4 引脚没有定义 AF 功能,那么 GPIOA_AFR4 这个宏就不会被定义,这样后面编译不能通过。所以还需要补充一个默认值: #if !defined GPIOA_AFR4 #define GPIOA_AFR4 0 #endif 这样如果发现没有定义必要的宏,就定义一个默认值。 前帖中已说过,这样简单处理的问题是如果不小心把宏定义拼错了,就不会被条件编译指定判断到,导致不希望的结果但是没有错误和警告。所以还需要加一重保险,在我现在的代码里面,实际是这样的: #ifdef ASSIGN_USART2_CK_PA4 #ifdef ENABLE_PA4_AF #error "PA4: multiple AF assignments" #else #define ENABLE_PA4_AF 1 #endif #endif 这里不直接定义 GPIOA_AFR4 这个宏,而先定义 ENABLE_PA4_AF,并判断是否已定义(避免写了两个复用功能分配到同一个引脚)。然后,在处理 USE_PA4 宏的时候,和 PIN_AF 宏做双重检查: #if defined USE_PA4 #ifdef ENABLE_PA4_AF #if !((USE_PA4) & PIN_AF) #error "PA4 use: PIN_AF not set" #endif #define GPIOA_MODER4 GPIO_MODER_BF(MODE_AF,4) #define GPIOA_AFR4 GPIO_AFR_BF(ENABLE_PA4_AF,4) #else #define GPIOA_AFR4 0 #if (USE_PA4) & PIN_OUT #define GPIOA_MODER4 GPIO_MODER_BF(MODE_OUTPUT,4) #elif (USE_PA4) & PIN_IN #define GPIOA_MODER4 GPIO_MODER_BF(MODE_INPUT,4) #elif (USE_PA4) & PIN_ANALOG #define GPIOA_MODER4 GPIO_MODER_BF(MODE_ANALOG,4) #else #error "PA4 use: mode undefined" #endif #endif #if (USE_PA4) & PIN_PULLUP #define GPIOA_PUPDR4 GPIO_PUPDR_BF(PULL_UP,4) #elif (USE_PA4) & PIN_PULLDOWN #define GPIOA_PUPDR4 GPIO_PUPDR_BF(PULL_DOWN,4) #else #define GPIOA_PUPDR4 0 #endif #if (USE_PA4) & PIN_OPENDRAIN #define GPIOA_OTYPER4 1<<4 #else #define GPIOA_OTYPER4 0 #endif #else #define GPIOA_MODER4 GPIO_MODER_BF(MODE_ANALOG,4) #define GPIOA_AFR4 0 #define GPIOA_PUPDR4 0 #define GPIOA_OTYPER4 0 #endif 这也是条件编译最关键的一段。 为了能工作,在 gpiodef.h 中事先定义一些必要的宏: #define PIN_AF 0x8000 #define PIN_OUT 0x4000 #define PIN_IN 0x2000 #define PIN_ANALOG 0x1000 #define PIN_PULLUP 0x0200 #define PIN_PULLDOWN 0x0100 #define PIN_OPENDRAIN 0x0080 用它们来书写 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),那么这个寄存器写操作就可以去掉了,也用条件编译指令预先判断一下。 附件是我做好的 STM32F030C8 的相关文件,供查看。 本帖最后由 cruelfox 于 2017-1-15 10:31 编辑

    gpiodef.c (2017-1-15 10:30 上传)

    83.79 KB, 阅读权限: 5, 下载次数: 3

    gpiodef.h (2017-1-15 10:30 上传)

    311 Bytes, 阅读权限: 5, 下载次数: 2

点赞  2017-1-14 23:39
又改进了一下,把写需要写两行的 AF 功能定义简化成一行就可以用的了:
  1. #include "stm32f0xx.h"
  2. #include "gpiodef.h"
  3. #define USE_PA3 PIN_OUT
  4. #define USE_PA6 PIN_OUT
  5. #define USE_PA13 PA13_AF_SWDIO
  6. #define USE_PA14 PA14_AF_SWCLK
  7. #define USE_PA9 PA9_AF_USART1_TX
  8. #define USE_PA10 PA10_AF_USART1_RX|PIN_PULLUP
  9. #define USE_PA7 PA7_AF_SPI1_MOSI
  10. #define USE_PA5 PA5_AF_SPI1_SCK
  11. #define USE_PA4 PA4_AF_SPI1_NSS
  12. #include "gpiodef.c"
这样看代码一目了然分配了哪些引脚。 当然,不能在一行代码上分配多个引脚仍然是局限。 本帖最后由 cruelfox 于 2017-1-15 13:03 编辑
点赞  2017-1-14 23:39
宏定义,这已经是最好的了。
效率和通用程度 总是有些冲突需要平衡的。所以核心底层 各家都是这样写的。
自己于我等普通人, 没别的办法,唯熟而已。 如果能理解好 配置的设计思想,就已经不错了。
熙熙攘攘的世界,我们不懈前行......
点赞  2017-1-15 18:22
为什么没有人参与讨论呢? 本来想抛砖引玉的……难道我周末发的帖子没有人注意么?
@辛昕  @白丁  @damiaa  @汤权  @dontium   不好意思,拉人啦
点赞  2017-1-16 16:24
哈哈,周末出去逛街了,还真没看见你的帖子,不过我对这个主题还是有要说的,之前用stm32的时候都是使用寄存器版本的,直接操作寄存器,那时候没听说过cubemx,也不喜欢用库函数,直接使用寄存器配置外设的时候的确是比较麻烦,我是专门写了一个外设库,包括常用的CLOCK、GPIO、USART、SPI、TIM、PWM、ADC库函数等等,里面有一个GPIO的初始化函数如下:
  1. void iGPIO_Init(GPIO_TypeDef* PORT,u16 PINS,u8 MODE,u8 Frequency)
  2. {
  3.         unsigned char i,temp;
  4.         GPO(PORT,PINS,1);
  5.         MODE |= Frequency;
  6.         for(i=0;i<=15;i++)
  7.         {
  8.                 if((PINS&((u16)1<<i)) != 0)
  9.                 {
  10.                         PINS &=         ~((u16)1<<i);
  11.                         temp =                  i%8;
  12.                         temp <<=         2;
  13.                         if(i >= 8)
  14.                         {
  15.                                 PORT->CRH &= ~((u32)0xf << temp);
  16.                                 PORT->CRH |= (u32)MODE << temp;
  17.                         }
  18.                         else
  19.                         {
  20.                                 PORT->CRL &= ~((u32)0xf << temp);
  21.                                 PORT->CRL |= (u32)MODE << temp;
  22.                         }
  23.                         if(PINS == (u16)0)
  24.                                 break;
  25.                 }
  26.         }
  27. }

我基本上都是使用这个函数的,效率可能不是很高,但是用到现在没什么问题,使用例子如下:

  1.         iGPIO_Init(GPIOB,GPIO_Pin_6|GPIO_Pin_7,iGPIO_Mode_Out_PP,iGPIO_Speed_2MHz);

是不是和库函数很相似?我就是参照了库函数的。写的不好,多多指教。
亚里士缺德
点赞  2017-1-16 16:44
引用: 汤权 发表于 2017-1-16 16:44
哈哈,周末出去逛街了,还真没看见你的帖子,不过我对这个主题还是有要说的,之前用stm32的时候都是使用寄 ...

这样比库函数用法要先定义一个结构变量,再设置结构体成员,然后传递给函数来得直接一些。程序简化成一行代码可配置相同属性的一组I/O口。
复用功能(AF)怎么设的呢? 我觉得配AF是麻烦得多的事情,每个型号都不完全一样。
另外,恰好我没写过 F1x 的程序,只用了 F0x, F4x系列,它们的GPIO寄存器表和 F1x 差别大。 写成统一的接口移植起来快,直接写寄存器的就不好弄了。
点赞  2017-1-16 17:25
引用: cruelfox 发表于 2017-1-14 23:39
gpiodef.c 这个文件是个模板,把所有可能用到的AF设置都要包含进去。具体在编译的时候,根据用到的引脚 ...

我和楼主一样,总觉得ST的例程、库对寄存器的操作过于烦琐,有些情况下,只为操作一个寄存器就拐弯调用了两个函数。而对CPU来讲,每调用一个函数,都要有压栈、跳转等指令,特别是跳转指令,要占用较多的时间。

对于ST的库的形式,确实编译后的代码量非常大。

所以,有时候把程序编完了,就将ST的函数修改到最简形式或直接自己再编写个函数。


但是,那些商人们考虑问题和咱们考虑问题的思路是不一样的。
-----------   占用CPU资源多吗? ------  升级你的CPU啊,
-----------     代码太大吗?   ----------  增加你的存储容量啊!

----------- 只有你觉得原来的需要跑着吃力,他们才有钱赚!



商人们比做技术的人聪明啊!
点赞  2017-1-16 18:23
再者,程序也有易读性的要求,多数情况下,易读的程序都会“罗嗦”一些。
点赞  2017-1-16 18:26
引用: dontium 发表于 2017-1-16 18:23
我和楼主一样,总觉得ST的例程、库对寄存器的操作过于烦琐,有些情况下,只为操作一个寄存器就拐弯调用了 ...

为了快速上手、换型号的方便移植而牺牲代码效率。
好处是加快了开发进度,降低学习门槛,对推广MCU的应用当然是有利的。只要给一套能用,很少bug,即使不那么好用的库,也比读了参考手册自己写要省去很多工夫。我觉得是各有取舍吧,是要缩短项目研发周期,把握更多机会,还是不放过细节,做到极致?
PC发展历史上比这更甚,早期都是用汇编节省内存,后来软件大了C、FORTRAN、Pascal等成为主流,也毕竟是生成机器码,现在Java, .NET这些,程序都暴大。不同时代的程序员的风格完全变了。
点赞  2017-1-16 20:38
引用: dontium 发表于 2017-1-16 18:26
再者,程序也有易读性的要求,多数情况下,易读的程序都会“罗嗦”一些。

其实我做这个自动工具的目的就是想让程序需要开发时候写的部分写得很简化,额外的工作虽然麻烦点但是只做一次。
同时,让写出来的这部分又带有文档的功能,一看就知道对应硬件连接设计成什么样。
点赞  2017-1-16 20:41
引用: cruelfox 发表于 2017-1-16 20:41
其实我做这个自动工具的目的就是想让程序需要开发时候写的部分写得很简化,额外的工作虽然麻烦点但是只做 ...

我很赞成你这种做法。我也是喜欢钻牛角尖的人,程序做好了还想再好,总想着既要节省空间又要提高速度。

所以,有时研究C语句的编译后的汇编代码量及代码效率,觉得语句的写法对编译后的结果影响重大,不同的编译器可能存在很大差异。

利用编译器优化的办法,并不能达到最佳!
点赞  2017-1-16 21:00
stm32是迎合时代发展的产物,价格优惠,外设丰富,开发简单方便。
这三样就够了 节约的几个月的开发时间很可能决定一家创业公司的生死了
物致DIY 欢迎你的加入~ QQ群:646461928 公众号:智物知心致成 小店
点赞  2017-1-16 21:19
楼主的做法确实值得很多写单片机代码的人好好的思考一下,用ST的方法做开发是快,但是当你换TI,NXP之类的时候开发就快不起来了,反而楼主的这种方法是万能法,不论是st ti还是NXP 甚至ARM7 ARM9 拿来手册 直接从底层写到逻辑层 妥妥的
物致DIY 欢迎你的加入~ QQ群:646461928 公众号:智物知心致成 小店
点赞  2017-1-16 21:21
刚刚看到楼主的帖子。我谈几点个人的观点:

1、寄存器操作其实更加直观,就像是一把尖刀一样,直插“敌人心脏”。所以,操作它,不对就是不对,没什么废话。

2、库函数就像先拨开了皮,才能将刀子插进“敌人心脏”。形象的讲,像寄存器穿了一层衣服。是将寄存器操作封装了一下,其底层操作还是在操作寄存器。只不过它,从层次上看,站的高一些。但是,程序在debug的时候,还是要回归到寄存器去看寄存器的值对不对。。。。

当然,有的人喜欢用寄存器,有的人喜欢用库函数,这些都是个人喜好,无可厚非。目的都是实现项目要求。
就像楼主说的,过一会儿F1,过一会儿F0,再过几天又是F4.每次对GPIO这块配置挺麻烦的。是的,确实是这样的,但是就目前来看,这一块,我是没有想到好的办法快速的办法,但是还是将自己平时的习惯表述下。
一般的,如果stm32的gpio与其他外设如can转换芯片相连。那么在h文件中我一般会按照如下的方式定义
  1. <blockquote><blockquote>#define CAN1_TJA1050_RX_GPIO_PROT   GPIOA
在相对应的C文件中,我习惯性还是用库函数。
  1. //GPIO初始化
  2. GPIO_InitTypeDef GPIO_InitStructure;

  3. GPIO_InitStructure.GPIO_Pin = CAN1_TJA1050_RX_GPIO_PIN;
  4. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  5. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  6. GPIO_Init(CAN1_TJA1050_RX_GPIO_PROTB, &GPIO_InitStructure);

  7. GPIO_PinAFConfig(CAN1_TJA1050_RX_GPIO_PROT,CAN1_TJA1050_RX_PinSoruce,GPIO_AF_CAN);

  8. //在NVIC初始化配置之前使用预编译
  9. #if CAN1_RX_IRQ_ENALE


  10. ......//NVIC配置

  11. #endif
这样写,不敢说是最好的额,但是因为对于我们的项目,有时候,板子需要稍稍的变动或者改版,有可能GPIO也随着有些变动。所以,这种情况下,我只需要修改相关的io即可。

其实这种写法,和一般的使用库函数的配置基本没什么区别。

当然了,目前还是我没有再好的办法。

目前来说,我觉得更会把时间花在功能的实现上,在功能的实现多花功夫。
这就是我的一一些观点,不合适之处,还行提示哈。

点赞  2017-1-16 21:28
引用: cruelfox 发表于 2017-1-16 17:25
这样比库函数用法要先定义一个结构变量,再设置结构体成员,然后传递给函数来得直接一些。程序简化成一行 ...

F1系列跟F0系列的很不相同;在复用方面,也不需要去配置GPIO_AFConfig; 只需要去使能外设时钟就行了;大多数时候都不需要去使能复用时钟
点赞  2017-1-16 22:55
好帖啊。支持一下。 一般使用库函数 好处是:一快,二升级方便。 比如F0的标准库1,0的和1.5的直接拷贝就OK,当时1.0的也不错,不过不支持STM32F072,所以拷贝一下升级了。 使用自己编写的函数 好处是:一自己看着舒服,二能体现程序员自己个性和风格,三能提供速度哦。 速度方面,初始化倒没什么,刚上电,慢点没关系,如果是要在中断里处理IO口,还有更甚者需要速度的话,是应该直接访问寄存器。 自己编写函数就要考虑未来移植的兼容性。如果这样,可以考虑函数里把F0,F1,F2,F3,F4,F7,L0,L1,L4全部加进去,用判断那种CPU的宏定义进行区分来编译。 #ifdef CPUISF0 ... #endif #ifdef CPUISF1 ... #endif ... #ifdef CPUISL4 ... #endif 这样做,就是太臃肿。 还有,寄存器的访问可以直接看库函数。从里面拷贝出来。这样就不用记忆了 两者结合起来,看着办吧。 本帖最后由 damiaa 于 2017-1-17 10:30 编辑
点赞  2017-1-17 10:10
引用: 皈依 发表于 2017-1-16 21:21
楼主的做法确实值得很多写单片机代码的人好好的思考一下,用ST的方法做开发是快,但是当你换TI,NXP之类的 ...

从ST换到TI, NXP,还是得读手册熟悉寄存器再写。凭脑子记住寄存器的用法比较难,记住各门MCU自己的库函数用发也还是难。
于是开一个新的MCU工程的时候,拿已有的代码作为基础,再修改来用是比较好弄的。
点赞  2017-1-17 10:21
引用: huaiqiao 发表于 2017-1-16 21:28
刚刚看到楼主的帖子。我谈几点个人的观点:

1、寄存器操作其实更加直观,就像是一把尖刀一样,直插“敌 ...

根据工程定义 CAN1_TJA1050_RX_GPIO_PROT, CAN1_TJA1050_RX_PinSoruce 这样的宏,确实是比较好的方式,修改电路之后只需要重写宏的定义指定相关引脚。只要细心地把这里写正确了,后面就不会弄错。
如果直接操作寄存器,就不能这么用了,这是不如用库函数的地方。
点赞  2017-1-17 10:28
引用: xueshawu 发表于 2017-1-16 22:55
F1系列跟F0系列的很不相同;在复用方面,也不需要去配置GPIO_AFConfig; 只需要去使能外设时钟就行了;大 ...

我记得有REMAP需要配置一下。 比如 USART1 可以用几组 pin, 也要选嘛?
点赞  2017-1-17 10:30
123下一页
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复