好几个学弟跟我说看不懂我的代码,既然是本着跟初学者一块学习的目的开帖子,那我们就讲的细致一点(小弟献丑,大神勿喷)。
上个帖子上传的附件工程中,我写的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[regpos] &= ~(0x0f<<bitpos*4);
- curgpio->AFR[regpos] |= (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_t jesse_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[0]))
- /************************************************************************************/
- 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[i]);
- jesse_led_pin->jesse_device_pin_write(&jesse_led_pin_init[i],PIN_HIGH);
- }
- }
复制代码 GPIO_PIN_INIT 这个是在头文件中定义的结构体类型
- typedef struct
- {
- uint8_t GPIOx;
- 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[0]))
它将计算出数组中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”文件进行处理的,这个工程可能会比较乱。 另一个则是沿用上一个帖子的工程,不过使用了中断处理
|