@乔木 发表于 2017-8-22 22:09:33

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

接上一篇:【JESSE】STM32初学(寄存器版)——GPIO操作

好几个学弟跟我说看不懂我的代码,既然是本着跟初学者一块学习的目的开帖子,那我们就讲的细致一点(小弟献丑,大神勿喷)。
上个帖子上传的附件工程中,我写的C文件主要有两个,一个是“jesse_pin.c”,一个是“jesse_led.c”。先说说“jesse_pin.c”,这个文件主要是用于操作GPIO,例如初始化,复用设置,写GPIO寄存器和读GPIO寄存器。
/************************************************************************************/
void jesse_gpio_pin_init(uint8_t GPIOx,uint32_t BITx,uint32_t MODE,uint32_t OTYPE,uint32_t OSPEED,uint32_t PUPD)
{
      GPIO_TypeDef *curgpio=0;
      curgpio = (GPIO_TypeDef*)(AHB1PERIPH_BASE+GPIOx*0x0400U);      

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

/************************************************************************************/
void jesse_device_pin_init(GPIO_PIN_INIT* pin)               
{
      jesse_gpio_pin_init(pin->GPIOx,pin->BITx,pin->MODE,pin->OTYPE,pin->OSPEED,pin->PUPD);
}

/************************************************************************************/
//      设置引脚的复用功能函数
void jesse_gpio_pin_af(uint8_t GPIOx,uint32_t BITx,uint8_t Alternate)
{
      GPIO_TypeDef * curgpio;
      uint8_t regpos,bitpos;
      
      curgpio = (GPIO_TypeDef*)(AHB1PERIPH_BASE+GPIOx*0x0400U);
      regpos= BITx/8;
      bitpos= BITx%8;
      curgpio->AFR &= ~(0x0f<<bitpos*4);
      curgpio->AFR |=(Alternate<<bitpos*4);
}

/************************************************************************************/
void jesse_device_pin_af(GPIO_PIN_INIT* pin, uint8_t Alternate)
{
      jesse_gpio_pin_af(pin->GPIOx,pin->BITx,Alternate);
}

/************************************************************************************/
//      写引脚函数
void jesse_gpio_pin_write(uint8_t GPIOx,uint32_t BITx,uint32_t Value)
{
      GPIO_TypeDef *curgpio=0;
      curgpio = (GPIO_TypeDef *)(AHB1PERIPH_BASE+GPIOx*0x0400U);
      
      curgpio->ODR &= ~(0x01<<BITx);                                                                                                //清除相应位
      curgpio->ODR |=Value<<BITx;                                                                                                      //写入相应位
}

/************************************************************************************/
void jesse_device_pin_write(GPIO_PIN_INIT* pin, uint32_t Value)
{
      jesse_gpio_pin_write(pin->GPIOx,pin->BITx,Value);
}

/************************************************************************************/
//      读引脚函数
uint8_t jesse_gpio_pin_read(uint8_t GPIOx,uint32_t BITx)
{
      GPIO_TypeDef *curgpio=0;
      curgpio = (GPIO_TypeDef *)(AHB1PERIPH_BASE+GPIOx*0x0400U);
      
      if((curgpio->IDR&(0x01U<<BITx))==(0x01U<<BITx))                              //读取BITx的状态值
                return PIN_HIGH;                                                                                                                                                //如果读取的值为1,则返回高                                                                                                                              
      else
                return PIN_LOW;                                                                                                                                                      //返回低
}

/************************************************************************************/
uint8_tjesse_device_pin_read(GPIO_PIN_INIT* pin)
{
      return jesse_gpio_pin_read(pin->GPIOx,pin->BITx);
}

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

初始化某个管脚,首先要知道的是:这个管脚属于哪个端口,第几个引脚.

我们并没有使用ST文件中关于GPIO的定义,而是根据GPIO_PORT地址的规律使用了自己的查找方式

GPIO的是在AHB1总线外设地址的基础上每隔0x0400往后偏移

如图,我们可以根据“jesse_pin.h”中的宏定义
再结合curgpio = (GPIO_TypeDef*)(AHB1PERIPH_BASE+GPIOx*0x0400U)这一句查找到传进来的是哪个端口,为什么这么做,这样子就可以利用移位操作开启该端口的时钟啊,看代码
RCC->AHB1ENR |= 0x01U<<GPIOx;                                                                              //开启相应引脚时钟
再看图

注意这里的GPIOx是上图中的 GPIO_A....等定义(是不是觉得有点取巧,有点绕)
原子使用的则是ST对GPIO的定义,这样子则是在开时钟这一步操作的时候会更麻烦一些。
对于PIN的设置,根据PIN0~15所对应的位在寄存器中的规律进行查找设置。
原子这处理这一步的时候,会比我的简单一些,它的处理可以传入同一个GPIO中的多个PIN,然后查找每个PIN,并对其设置,我的则是不行的。
说到这里,“jesse_pin.c”中的代码基本都能看懂了。
还有就是 jesse_gpio_pin 这个函数指针结构体了,也可以不使用它,直接调用函数。

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

/************************************************************************************/
#define LED_NUM                (sizeof(jesse_led_pin_init)/sizeof(jesse_led_pin_init))
/************************************************************************************/

const GPIO_PIN_FUN *jesse_led_pin=&jesse_gpio_pin;
/************************************************************************************/
void jesse_LED_Init(void)
{
      uint8_t i=0;
      for(i=0;i<LED_NUM;i++)
      {
                jesse_led_pin->jesse_device_pin_init(&jesse_led_pin_init);
                jesse_led_pin->jesse_device_pin_write(&jesse_led_pin_init,PIN_HIGH);
      }
}GPIO_PIN_INIT 这个是在头文件中定义的结构体类型
typedef struct
{
      uint8_tGPIOx;
      uint32_t BITx;
      uint32_t MODE;
      uint32_t OTYPE;
      uint32_t OSPEED;
      uint32_t PUPD;
}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))
它将计算出数组中PIN的个数,然后以数组下标查找。

还有最后一个问题,不说明白可能会成为一个坑。
这里的每个PIN的MODE我都设置为推挽输出模式,然后我在LED_Toggle函数中读取了寄存器中的内容,PIN一旦设置为推挽输出模式,那么输出的MOS
则有可能会导通,那么PIN将会输出一个稳定的电平,这将影响IDR中的内容。
我们看图

我们这里读取的引脚电平是引脚输出电平,引脚并没有电平信号输入,所以我们设置为推挽输出,如果是IIC等需要读取外部电平的时候则是不能这么设置的,这样读取出来的数据很可能是错的。

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

DIS板卡中有两个按键,一个是复位键,另一个便是用户按键。图中我们可以看到按键引脚进行了下拉(什么是下拉?我们放文末讨论讨论),并且有一个电容消抖(实际上这个电容并没有焊接)。使用按键一样是配置相应引脚的寄存器,不过这次需要配置的寄存器就少很多了,因为我们是读取按键的状态,所以对输出配置的寄存器我们可以不用管。硬件做了下拉处理,所以上下拉的寄存器也可以不用配置,保持‘00’(No pull-up,pull-down)即可,筛选之后,我们只用配置MODER这个寄存器即可。GPIOA->MODER &= ~(0x01);还有就是打开GPIOA的时钟RCC->AHB1ENR |= 0x01;
做好相应的配置工作之后,我们就可以在while中一直读取按键的状态if(key_read())
      {
            led_toggle(LED1);
      }
Key_read()便是查看按键状态的函数,如果按键按下,这个函数便回返回‘1’,接下来便是执行led_toggle(LED1)这一步。Key_read()这个函数是怎么样操作的呢。uint8_t key_read(void)
{
    if((GPIOA->IDR&(0x0001)) == (0x0001))
    {
      key_delay(20000);   //延时消抖
      if((GPIOA->IDR&(0x0001)) == (0x0001))
            return KEY;
    }
    return 0;                              //这一步最好不要省略,如果函数有返回值,
                                                    //无论是何种情况都要返回一个确定的值,
                                                    //否则有可能会出现一个不确定的值,从而影响程序运行
}
代码里其实是在不停的查询GPIOA的IDR寄存器,(GPIOA->IDR&(0x0001)) == (0x0001)这句的意思便是,读出IDR中的内容,如果按键按下,IDR寄存器中对应Pin0的位(最低位)将会置1(按键按下,为高电平),将寄存器中16位的数据与上0x0001,如果最低位为‘1’,那么条件成立,接着便是执行if中的内容。key_delay(20000);这个函数就是延时,让单片机做一些空操作,为的就是消耗单片机的时间,避开因为按键按下而带来的电平抖动。延时之后再一次检测IDR寄存器中的内容,为的是确保按键按下,避免一些误操作。如果按键真的按下,函数将会返回KEY(这是个宏)的值(非零值)。
这里主要接触到两个问题一:上下拉问题         我们以开漏模式来探讨一下,开漏模式更容易说明上拉这个问题。         开漏模式便是漏极开路,以三极管便是集电极开漏输出的结构,如图图一有两个三极管,前面那个三极管是反向之用,后面那个三极管集电极开路。对于图一,如果前面的三极管输入为‘0’,那么第一个三极管便会截至,导致后面的三极管导通,使输出直接接地。当前面的三极管输入为‘1’的时候呢,前面导通,后面截止,这时候输出便是一个高阻态。这就相当于图二的模型,开关闭合,输出接地,开关打开,输出便不确定了了。上拉便是如图三所示,输出接一个电阻到VCC。当开关断开时,输出会被拉到接近VCC的电平,这个时候估计会有人会产生这样的疑惑:VCC通过一个电阻到输出,电阻不就分压了,输出不就应该是个低电平吗?电阻分压的前提是VCC到输出有一定电流,前面我们分析了输出到地的三极管是截至的,这时候输出到地之间接近断路,就算有电流也是极小的,故此时输出便被拉到接近VCC的电平。我们分析了开漏上拉,那开漏能下拉吗?我们要注意,开漏模式下如果没有进行上拉,那么输出端口是没有驱动能力的,可以导通到地,但是输出不了一个明确的高电平,所以如果在外部下拉的引脚上开启开漏模式,那么输出就可能会出现问题。下拉更多情况下是用于得到一个低电平,例如这个程序中的按键,如果不进行下拉(外部或内部),那么在按键没有按下的情况下,这个引脚是浮空的,引脚的电平是不确定的,那读取电平的时候多少会出现问题。
二:按键消抖问题我们平时用到的开关一般都是机械弹性开关,按键按下的时候并不会马上稳定的接通,会有几毫秒到几十毫秒的抖动时间。在这个程序中,如果不对这种抖动进行处理,那这个按键就会非常不好使。我们用软件对按键进行消抖一般有两种方案,一是重采样,即程序中所用的方法,延时一段时间之后再进行一次检测;二是持续采样,即多次采样,然后进行处理,最后以处理结果判断按键是否按下。还有就是进行硬件消抖,即接上原理图中的电容,用电容的充放电特性来对抖动过程中产生的毛刺进行平滑处理。并没有说那种方式好,这都需要大家亲自去实验。
最后上传两个工程,一个工程是没有使用“jesse_pin.c”文件进行处理的,这个工程可能会比较乱。另一个则是沿用上一个帖子的工程,不过使用了中断处理
下一篇:【JESSE】STM32初学(寄存器版)——位段操作


乖乖妮 发表于 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

andeyqi 发表于 2017-8-23 10:30
顶一下

谢谢大佬:)

子曰好人 发表于 2017-8-23 10:57:29

把自己的经验分享出来也是对自己所学的一个总结,向楼主学习

@乔木 发表于 2017-8-23 11:18:49

子曰好人 发表于 2017-8-23 10:57
把自己的经验分享出来也是对自己所学的一个总结,向楼主学习

一起学习:)说不定还能让大神指点指点,弥补所缺:lol

jxchen 发表于 2018-1-8 21:00:47

JESSE 你好:
                  你描述的按鍵是指用戶按鍵?

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

看不到下载的连接
页: [1]
查看完整版本: 【JESSE】STM32初学(寄存器版)——GPIO操作[2]