你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【JESSE】STM32初学(寄存器版)——GPIO操作[2]

[复制链接]
@乔木 提问时间:2017-8-22 22:09 /


好几个学弟跟我说看不懂我的代码,既然是本着跟初学者一块学习的目的开帖子,那我们就讲的细致一点(小弟献丑,大神勿喷)。

上个帖子上传的附件工程中,我写的C文件主要有两个,一个是“jesse_pin.c”,一个是“jesse_led.c”。
先说说“jesse_pin.c”,这个文件主要是用于操作GPIO,例如初始化,复用设置,写GPIO寄存器和读GPIO寄存器。
  1. /************************************************************************************/
  2. void jesse_gpio_pin_init(uint8_t GPIOx,uint32_t BITx,uint32_t MODE,uint32_t OTYPE,uint32_t OSPEED,uint32_t PUPD)
  3. {  
  4.         GPIO_TypeDef *curgpio=0;
  5.         curgpio = (GPIO_TypeDef*)(AHB1PERIPH_BASE+GPIOx*0x0400U);        

  6.         RCC->AHB1ENR |= 0x01U<<GPIOx;                      //开启相应引脚时钟
  7.         curgpio->MODER&=~(3U<<(BITx*2));                   //先清除原来的设置
  8.         curgpio->MODER|=MODE<<(BITx*2);                    //设置新的模式
  9.         if((MODE==0x01)||(MODE==0x02))                        //如果是输出模式/复用功能模式,则需要设置输出速度和类型
  10.         {  
  11.                 curgpio->OSPEEDR&=~(3U<<(BITx*2));        //清除原来的设置
  12.                 curgpio->OSPEEDR|=(OSPEED<<(BITx*2));   //设置新的速度值  
  13.                 curgpio->OTYPER&=~(1U<<BITx);                 //清除原来的设置
  14.                 curgpio->OTYPER|=OTYPE<<BITx;                 //设置新的输出模式
  15.         }  
  16.         curgpio->PUPDR&=~(3U<<(BITx*2));                     //先清除原来的设置
  17.         curgpio->PUPDR|=PUPD<<(BITx*2);                      //设置新的上下拉
  18. }

  19. /************************************************************************************/
  20. void jesse_device_pin_init(GPIO_PIN_INIT* pin)               
  21. {
  22.         jesse_gpio_pin_init(pin->GPIOx,pin->BITx,pin->MODE,pin->OTYPE,pin->OSPEED,pin->PUPD);
  23. }

  24. /************************************************************************************/
  25. //        设置引脚的复用功能函数
  26. void jesse_gpio_pin_af(uint8_t GPIOx,uint32_t BITx,uint8_t Alternate)
  27. {
  28.         GPIO_TypeDef * curgpio;
  29.         uint8_t regpos,bitpos;
  30.         
  31.         curgpio = (GPIO_TypeDef*)(AHB1PERIPH_BASE+GPIOx*0x0400U);
  32.         regpos  = BITx/8;
  33.         bitpos  = BITx%8;
  34.         curgpio->AFR[regpos] &= ~(0x0f<<bitpos*4);
  35.         curgpio->AFR[regpos] |=  (Alternate<<bitpos*4);
  36. }

  37. /************************************************************************************/
  38. void jesse_device_pin_af(GPIO_PIN_INIT* pin, uint8_t Alternate)
  39. {
  40.         jesse_gpio_pin_af(pin->GPIOx,pin->BITx,Alternate);
  41. }

  42. /************************************************************************************/
  43. //        写引脚函数
  44. void jesse_gpio_pin_write(uint8_t GPIOx,uint32_t BITx,uint32_t Value)
  45. {
  46.         GPIO_TypeDef *curgpio=0;
  47.         curgpio = (GPIO_TypeDef *)(AHB1PERIPH_BASE+GPIOx*0x0400U);
  48.         
  49.         curgpio->ODR &= ~(0x01<<BITx);                                                                                                //清除相应位
  50.         curgpio->ODR |=  Value<<BITx;                                                                                                        //写入相应位
  51. }

  52. /************************************************************************************/
  53. void jesse_device_pin_write(GPIO_PIN_INIT* pin, uint32_t Value)
  54. {
  55.         jesse_gpio_pin_write(pin->GPIOx,pin->BITx,Value);
  56. }

  57. /************************************************************************************/
  58. //        读引脚函数
  59. uint8_t jesse_gpio_pin_read(uint8_t GPIOx,uint32_t BITx)
  60. {
  61.         GPIO_TypeDef *curgpio=0;
  62.         curgpio = (GPIO_TypeDef *)(AHB1PERIPH_BASE+GPIOx*0x0400U);
  63.         
  64.         if((curgpio->IDR&(0x01U<<BITx))==(0x01U<<BITx))                                //读取BITx的状态值
  65.                 return PIN_HIGH;                                                                                                                                                //如果读取的值为1,则返回高                                                                                                                                
  66.         else
  67.                 return PIN_LOW;                                                                                                                                                        //返回低
  68. }

  69. /************************************************************************************/
  70. uint8_t  jesse_device_pin_read(GPIO_PIN_INIT* pin)
  71. {
  72.         return jesse_gpio_pin_read(pin->GPIOx,pin->BITx);
  73. }

  74. /************************************************************************************/
  75. GPIO_PIN_FUN jesse_gpio_pin=
  76. {
  77.         jesse_device_pin_init,
  78.         jesse_device_pin_af,
  79.         jesse_device_pin_write,
  80.         jesse_device_pin_read,
  81. };
  82. /************************************************************************************/
复制代码
有8个函数,其中四个是对另外四个的封装,为什么要添加个四个函数,后面我们可以根据实例讲讲。
我们就拿 jesse_gpio_pin_init 函数讲讲,这个明白了,其他的应该都能明白。
看过正点原子寄存器版本代码的同学应该会觉得眼熟,并不说抄它的,我也是在偶然的机会下才发现我写的这个函数跟他的有那么点像,但是处理机制还是不一样的。

初始化某个管脚,首先要知道的是:这个管脚属于哪个端口,第几个引脚.
1.PNG
我们并没有使用ST文件中关于GPIO的定义,而是根据GPIO_PORT地址的规律使用了自己的查找方式
2.PNG
GPIO的是在AHB1总线外设地址的基础上每隔0x0400往后偏移
3.PNG
如图,我们可以根据“jesse_pin.h”中的宏定义
再结合  curgpio = (GPIO_TypeDef*)(AHB1PERIPH_BASE+GPIOx*0x0400U)  这一句查找到传进来的是哪个端口,为什么这么做,这样子就可以利用移位操作开启该端口的时钟啊,看代码
RCC->AHB1ENR |= 0x01U<<GPIOx;                                                                                //开启相应引脚时钟
再看图
4.PNG
注意这里的GPIOx是上图中的 GPIO_A....等定义(是不是觉得有点取巧,有点绕)
原子使用的则是ST对GPIO的定义,这样子则是在开时钟这一步操作的时候会更麻烦一些。
对于PIN的设置,根据PIN0~15所对应的位在寄存器中的规律进行查找设置。
原子这处理这一步的时候,会比我的简单一些,它的处理可以传入同一个GPIO中的多个PIN,然后查找每个PIN,并对其设置,我的则是不行的。
说到这里,“jesse_pin.c”中的代码基本都能看懂了。
还有就是 jesse_gpio_pin 这个函数指针结构体了,也可以不使用它,直接调用函数。

剩下“jesse_led.c”这个文件,我觉得就初始化那段会让初学者比较懵
  1. /************************************************************************************/
  2. static GPIO_PIN_INIT jesse_led_pin_init[]=
  3. {
  4.         {GPIO_G,PIN6,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_2M,GPIO_PUPD_NONE},                        //LED1
  5.         {GPIO_D,PIN4,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_2M,GPIO_PUPD_NONE},                        //LED2
  6.         {GPIO_D,PIN5,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_2M,GPIO_PUPD_NONE},                        //LED3
  7.         {GPIO_K,PIN3,GPIO_MODE_OUT,GPIO_OTYPE_PP,GPIO_SPEED_2M,GPIO_PUPD_NONE},                        //LED4
  8. };

  9. /************************************************************************************/
  10. #define LED_NUM                (sizeof(jesse_led_pin_init)/sizeof(jesse_led_pin_init[0]))
  11. /************************************************************************************/

  12. const GPIO_PIN_FUN *jesse_led_pin=&jesse_gpio_pin;
  13. /************************************************************************************/
  14. void jesse_LED_Init(void)
  15. {
  16.         uint8_t i=0;
  17.         for(i=0;i<LED_NUM;i++)
  18.         {
  19.                 jesse_led_pin->jesse_device_pin_init(&jesse_led_pin_init[i]);
  20.                 jesse_led_pin->jesse_device_pin_write(&jesse_led_pin_init[i],PIN_HIGH);
  21.         }
  22. }
复制代码
GPIO_PIN_INIT 这个是在头文件中定义的结构体类型
  1. typedef struct
  2. {
  3.         uint8_t  GPIOx;
  4.         uint32_t BITx;
  5.         uint32_t MODE;
  6.         uint32_t OTYPE;
  7.         uint32_t OSPEED;
  8.         uint32_t PUPD;
  9. }GPIO_PIN_INIT;
复制代码
它包含了初始化一个PIN所需要的各个参数(没有引脚复参数)
jesse_led_pin_init[],这是个结构体类型数组,它的每一个元素都是结构体,这样就可以将每个PIN的初始化参数打包访问。
jesse_LED_Init()这个初始化函数则可以将数组中每个PIN进行循环设置,这主要是靠这个宏定义
#define LED_NUM                (sizeof(jesse_led_pin_init)/sizeof(jesse_led_pin_init[0]))
它将计算出数组中PIN的个数,然后以数组下标查找。

还有最后一个问题,不说明白可能会成为一个坑。
这里的每个PIN的MODE我都设置为推挽输出模式,然后我在LED_Toggle函数中读取了寄存器中的内容,PIN一旦设置为推挽输出模式,那么输出的MOS
则有可能会导通,那么PIN将会输出一个稳定的电平,这将影响IDR中的内容。
我们看图
5.PNG
我们这里读取的引脚电平是引脚输出电平,引脚并没有电平信号输入,所以我们设置为推挽输出,如果是IIC等需要读取外部电平的时候则是不能这么设置的,这样读取出来的数据很可能是错的。

解决完遗留的问题,我们还可以讲讲按键。

DIS板卡中有两个按键,一个是复位键,另一个便是用户按键
6.PNG
图中我们可以看到按键引脚进行了下拉(什么是下拉?我们放文末讨论讨论),并且有一个电容消抖(实际上这个电容并没有焊接)。
使用按键一样是配置相应引脚的寄存器,不过这次需要配置的寄存器就少很多了,因为我们是读取按键的状态,所以对输出配置的寄存器我们可以不用管。硬件做了下拉处理,所以上下拉的寄存器也可以不用配置,保持‘00’(No pull-up,pull-down)即可,筛选之后,我们只用配置MODER这个寄存器即可。
GPIOA->MODER &= ~(0x01);
还有就是打开GPIOA的时钟
RCC->AHB1ENR |= 0x01;

做好相应的配置工作之后,我们就可以在while中一直读取按键的状态
  1. if(key_read())
  2.         {
  3.             led_toggle(LED1);
  4.         }
复制代码

Key_read()便是查看按键状态的函数,如果按键按下,这个函数便回返回‘1’,接下来便是执行led_toggle(LED1)这一步。
Key_read()这个函数是怎么样操作的呢。
  1. uint8_t key_read(void)
  2. {
  3.     if((GPIOA->IDR&(0x0001)) == (0x0001))
  4.     {
  5.         key_delay(20000);     //延时消抖
  6.         if((GPIOA->IDR&(0x0001)) == (0x0001))
  7.             return KEY;
  8.     }
  9.     return 0;                                //这一步最好不要省略,如果函数有返回值,
  10.                                                     //无论是何种情况都要返回一个确定的值,
  11.                                                     //否则有可能会出现一个不确定的值,从而影响程序运行
  12. }
复制代码

代码里其实是在不停的查询GPIOAIDR寄存器,(GPIOA->IDR&(0x0001)) == (0x0001)这句的意思便是,读出IDR中的内容,如果按键按下,IDR寄存器中对应Pin0的位(最低位)将会置1(按键按下,为高电平),将寄存器中16位的数据与上0x0001,如果最低位为‘1’,那么条件成立,接着便是执行if中的内容。
key_delay(20000);这个函数就是延时,让单片机做一些空操作,为的就是消耗单片机的时间,避开因为按键按下而带来的电平抖动。
延时之后再一次检测IDR寄存器中的内容,为的是确保按键按下,避免一些误操作。如果按键真的按下,函数将会返回KEY(这是个宏)的值(非零值)。

这里主要接触到两个问题
一:上下拉问题
         我们以开漏模式来探讨一下,开漏模式更容易说明上拉这个问题。
         开漏模式便是漏极开路,以三极管便是集电极开漏输出的结构,如图
7.jpg
图一有两个三极管,前面那个三极管是反向之用,后面那个三极管集电极开路。
对于图一,如果前面的三极管输入为‘0’,那么第一个三极管便会截至,导致后面的三极管导通,使输出直接接地。当前面的三极管输入为‘1’的时候呢,前面导通,后面截止,这时候输出便是一个高阻态。这就相当于图二的模型,开关闭合,输出接地,开关打开,输出便不确定了了。
上拉便是如图三所示,输出接一个电阻到VCC。当开关断开时,输出会被拉到接近VCC的电平,这个时候估计会有人会产生这样的疑惑:VCC通过一个电阻到输出,电阻不就分压了,输出不就应该是个低电平吗?电阻分压的前提是VCC到输出有一定电流,前面我们分析了输出到地的三极管是截至的,这时候输出到地之间接近断路,就算有电流也是极小的,故此时输出便被拉到接近VCC的电平。
我们分析了开漏上拉,那开漏能下拉吗?
我们要注意,开漏模式下如果没有进行上拉,那么输出端口是没有驱动能力的,可以导通到地,但是输出不了一个明确的高电平,所以如果在外部下拉的引脚上开启开漏模式,那么输出就可能会出现问题。
下拉更多情况下是用于得到一个低电平,例如这个程序中的按键,如果不进行下拉(外部或内部),那么在按键没有按下的情况下,这个引脚是浮空的,引脚的电平是不确定的,那读取电平的时候多少会出现问题。

二:按键消抖问题
我们平时用到的开关一般都是机械弹性开关,按键按下的时候并不会马上稳定的接通,会有几毫秒到几十毫秒的抖动时间。在这个程序中,如果不对这种抖动进行处理,那这个按键就会非常不好使。
我们用软件对按键进行消抖一般有两种方案,一是重采样,即程序中所用的方法,延时一段时间之后再进行一次检测;二是持续采样,即多次采样,然后进行处理,最后以处理结果判断按键是否按下。
还有就是进行硬件消抖,即接上原理图中的电容,用电容的充放电特性来对抖动过程中产生的毛刺进行平滑处理。
并没有说那种方式好,这都需要大家亲自去实验。

最后上传两个工程,一个工程是没有使用“jesse_pin.c”文件进行处理的,这个工程可能会比较乱。
另一个则是沿用上一个帖子的工程,不过使用了中断处理




project1.rar

下载

194.2 KB, 下载次数: 5, 下载积分: ST金币 -1

project2.rar

下载

185.97 KB, 下载次数: 4, 下载积分: ST金币 -1

收藏 1 评论11 发布时间:2017-8-22 22:09

举报

11个回答
乖乖妮 回答时间:2017-8-23 08:46:59
手动点赞一个
zero99 回答时间:2017-8-23 09:29:12
自动点个赞   
andey 回答时间:2017-8-23 10:30:40
提示: 作者被禁止或删除 内容自动屏蔽
@乔木 回答时间:2017-8-23 10:42:47

谢谢大佬
子曰好人 回答时间:2017-8-23 10:57:29
把自己的经验分享出来也是对自己所学的一个总结,向楼主学习
@乔木 回答时间:2017-8-23 11:18:49
子曰好人 发表于 2017-8-23 10:57
把自己的经验分享出来也是对自己所学的一个总结,向楼主学习

一起学习说不定还能让大神指点指点,弥补所缺
jxchen 回答时间:2018-1-8 21:00:47
JESSE 你好:
                  你描述的按鍵是指用戶按鍵?
Image x1.jpg
jxchen 回答时间:2018-1-8 23:51:17
JESSE 你好:
                 我有一個問題,程式規劃成 PC12 OUTPUT, PC13 INPUT ,INPUT 讀取按鍵無法正常讀取
                 剛你下列所講是否會有此問題產生

还有最后一个问题,不说明白可能会成为一个坑。
这里的每个PIN的MODE我都设置为推挽输出模式,然后我在LED_Toggle函数中读取了寄存器中的内容,PIN一旦设置为推挽输出模式,那么输出的MOS
则有可能会导通,那么PIN将会输出一个稳定的电平,这将影响IDR中的内容。
@乔木 回答时间:2018-1-23 08:58:14
jxchen 发表于 2018-1-8 23:51
JESSE 你好:
                 我有一個問題,程式規劃成 PC12 OUTPUT, PC13 INPUT ,INPUT 讀取按鍵無法正 ...

不好意思,这学期一直在忙毕业设计,没来论坛,所以现在才看到您的点评。
两个引脚设置正确的情况下,至少在我的学习过程中没有遇到过互相干扰的状况。
我在LED_Toggle中读取引脚电平就是为了获取引脚的输出状态,或者说我就是要获取是那个MOS导通来判断LED的亮灭情况,可能在引脚电平翻转的情况下用这种方式有点不合理。
感谢您的点评
mmuuss586 回答时间:2018-12-5 14:42:51

学习下;
avioshigang1540 回答时间:2018-12-10 17:32:03
看不到下载的连接

所属标签

相似问题

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版