完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
在上一节的基础上,进一步改写代码,再引入官方标注库函数。虽然官方标准库慢慢式微,有一些别的库可能会取代它,但是并不妨碍我们继续拿官方库来写代码,吸取里边好的写法,强化下C语言技能,加深对寄存器的理解也是不错的。 本文模仿库函数,首先自定义库函数,然后一步一步改写代码,最终引入官方标准库函数。 实现流水灯 void delay(unsigned int a) { while(a--); } int main(void) { RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; GPIOB->CRH &= ~(0xf<<(0*4)|0xf<<(1*4)); GPIOB->CRH |= 0x3<<(0*4)|0x3<<(1*4); while(1) { GPIOB->ODR &= ~(1<<8); //PB8 = 0 GPIOB->ODR |= 1<<9; //PB9 = 1 delay(0xfffff); GPIOB->ODR &= ~(1<<9); //PB9 = 0 GPIOB->ODR |= 1<<8; //PB8 = 1 delay(0xfffff); } } 主要是增加了延时函数与while(1)的循环。通过寄存器,实现流水灯。这是单片机最基础的实验,意义等同于helloworld。这些代码如果有51基础,是很好理解的。但是存在问题:代码的扩展性与维护性较差,存在较多的复制粘贴,不太容易看懂,如果出现业务变更,如LED的引脚换了,那么修改工作太大。 接下来我们来自己从零开始,写一个库函数,目的是提高代码的扩展性和可维护性,具体表现是: 1方便移植,比较通用。 2读起来没那么费劲,不用翻着手册来读。 自定义IO初始化函数 在不考虑时钟的情况下,配置一个IO口(也就是引脚)需要知道以下信息: 1 PORT PA还是PB 2 PIN PB1还是PB2 3 模式 输入还是输出? 我们可以把这几个信息作为函数的参数,用一个函数来进行IO的初始化。例如初始化PB0位2Mhz的推挽输出: void easy_IO(GPIO_TypeDef *GPIOx,char pin,char cny,char mode) { uint8_t temp; if(mode != shuru) { GPIOx->CRL &= ~(0xf << pin*4); temp =(cny<<2)|(mode); GPIOx ->CRL |= temp << (pin*4); } } easy_IO(GPIOB,0,tuiwan,MHZ2);//调用 用这种方法可以比较方便地进行引脚的初始化,弊端在于,参数太多,容易出错。有没有更好的传参数的方法? 使用结构体 首先把需要的信息都放在一个结构体内。 typedef struct { Uint16_t pin; uint8_t mode;//输入还是输出,速度 uint8_t cny;//输出模式 }GPIO_InitST; 然后定义两个枚举类型,为参数可能的取值起一个好理解的名字: typedef enum { shuru = 0x00, MHZ10, MHZ2, MHZ50, }ModeEm; typedef enum { tuiwan = 0x00, kailou, futuiwan, fukailou, }CNYEm; 最后定义一个函数,用于IO的初始化: //自定义IO初始化函数 void myGPIO_Init(GPIO_TypeDef* GPIOx,GPIO_InitST* st) { if(st->mode != shuru)//暂时先只处理输出的情况 { uint8_t temp; if(st->pin<8)//P0-P7用CRL { GPIOx->CRL &= ~(0xf< temp = (st->cny<<2)|(st->mode); GPIOx->CRL |= temp< } else//P8及以上是CRH { GPIOx->CRH &= ~(0xf<<(st->pin-8)*4); temp = (st->cny<<2)|(st->mode); GPIOx->CRH |= temp<<(st->pin-8)*4; } } } 主函数中IO的初始化可以直接调用函数: int main(void) { GPIO_InitST myst; RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; myst.pin = 8; myst.mode = MHZ50; myst.cny = tuiwan; myGPIO_Init(GPIOB,&myst); myst.pin = 9; myGPIO_Init(GPIOB,&myst); while(1) { GPIOB->ODR &= ~(1<<8); //PB8 = 0 GPIOB->ODR |= 1<<9; //PB9 = 1 delay(0xfffff); GPIOB->ODR &= ~(1<<9); //PB9 = 0 GPIOB->ODR |= 1<<8; //PB8 = 1 delay(0xfffff); } } 编译程序,下载观察现象,跟流水灯一样。 使用独热码操作多个引脚 有两个不同引脚的话,需要调用两次初始化函数,能不能调用一次初始化的函数就初始化两个引脚?可以,代码仍然有改进的空间。 使用独热码one-hot code, 直观来说就是有多少个状态就有多少比特,而且只有一个比特为1,其他全为0的一种码制。 如果引脚0定义为0x01,引脚1定义为0x02,引脚2定义为0x04(而不是0x03),那么0x07就可以代表这三个引脚。想同时初始化三个引脚,只需要传入一个参数,0x07。 //自定义IO初始化函数 void myGPIO_Init(GPIO_TypeDef* GPIOx,GPIO_InitST* st) { uint16_t i=0, j = 0, pflg = 0; uint8_t temp; if(st->mode != shuru)//暂时先只处理输出的情况 { for(i = 0 ; i < 16 ; i++) { j = 0x01 << i; pflg = st->pin & j; if (pflg == j) { if(i<8)//P0-P7用CRL { GPIOx->CRL &= ~(0xf< temp = (st->cny<<2)|(st->mode); GPIOx->CRL |= temp< } else//P8及以上是CRH { GPIOx->CRH &= ~(0xf<<(i-8)*4); temp = (st->cny<<2)|(st->mode); GPIOx->CRH |= temp<<(i-8)*4; } } } } } 以上代码,主要思想是通过一个for循环,将传入的参数按位取出,并判断这一位是0还是1。如果是1,则需要对这一位对应的IO进行操作。 然后主函数的调用也需要做相应的修改,一次传入PB8与PB9两个引脚。 #define Pin_8 ((uint16_t)0x0100) /*!< Pin 8 selected */ #define Pin_9 ((uint16_t)0x0200) /*!< Pin 9 selected */ int main(void) { GPIO_InitST myst; RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; myst.pin = Pin_8| Pin_9; myst.mode = MHZ50; myst.cny = tuiwan; myGPIO_Init(GPIOB,&myst); while(1) { GPIOB->ODR &= ~(1<<8); //PB8 = 0 GPIOB->ODR |= 1<<9; //PB9 = 1 delay(0xfffff); GPIOB->ODR &= ~(1<<9); //PB9 = 0 GPIOB->ODR |= 1<<8; //PB8 = 1 delay(0xfffff); } } 改写引脚操作 在使用ODR寄存器的时候,总是需要关注那些不应该***作的引脚,怕误操作。现在由51单片机的思维改为STM32的思维。STM32提供了特别丰富,好用的寄存器,例如BSRR(端口位设置/清除)寄存器,对某个位写1代表把对应的IO设置为高电平,其它位写0,则不对其它位产生影响。 看数据手册 跟它类似的有个BRR寄存器,功能是端口位清零。 借助这两个寄存器,我们可以很方便的写出IO设置为1和IO设置为0的两个函数: void mySetbits(GPIO_TypeDef* GPIOx,uint16_t pin)//引脚设置1 { GPIOx->BSRR = pin; } void myResetbits(GPIO_TypeDef* GPIOx, uint16_t pin)//引脚设置0 { GPIOx-> BRR= pin; } 然后修改主函数内的死循环: while(1) { myResetbits(GPIOB,Pin_8); //PB8 = 0 mySetbits(GPIOB,Pin_9); //PB9 = 1 delay(0xfffff); myResetbits(GPIOB,Pin_9); //PB9 = 0 mySetbits(GPIOB,Pin_8); //PB8 = 1 delay(0xfffff); } 最终效果还是一样的,但代码看上去就比较爽心悦目了。事实上,这已经很接近于库函数的代码了。 引入官方库函数 官方的固件库提供了很多好用的函数,接下来由自定义的固件库转到官方标准固件库。 官方标准固件库(以后简称官方库)的函数可以查看文档《STM32固件库使用手册的中文翻译版.pdf》 官方库除了提供了详细的函数说明,还提供了使用的例子。另外,通过函数的跳转,可以看到官方库的源码,这些源码经过千锤百炼,都是学习单片机编程的好榜样。 与引脚相关的函数都在GPIO章节,读者可以自行查看。新建一个函数,IO_Init()。参考例子,稍作修改就能写完LED引脚的初始化函数。 void IO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(GPIOB, &GPIO_InitStructure); } int main(void) { IO_Init(); GPIO_SetBits(GPIOB,GPIO_Pin_8|GPIO_Pin_9); while(1) { GPIO_ResetBits(GPIOB,GPIO_Pin_8); //PB8 = 0 GPIO_SetBits(GPIOB,GPIO_Pin_9); //PB9 = 1 delay(0xfffff); GPIO_ResetBits(GPIOB,GPIO_Pin_9); //PB9 = 0 GPIO_SetBits(GPIOB,GPIO_Pin_8); //PB8 = 1 delay(0xfffff); } } 我们通过对寄存器的封装,实现了最简单的库函数。使用STM32编程,库函数远比寄存器方便。所谓的库函数,就是封装寄存器,提供接口给用户调用。如果是需要快速开发的时候,可以直接使用库函数,不必纠结于是怎样实现的,要大胆的拿来就用。但是,学习的时候,还是要考虑下库函数的实现方式,库函数是经过千锤百炼的优秀代码。因此我们花费巨大的精力,自己实现了简单的库函数,就是为了让大家明白,库函数不神秘,如果需要,我们自己就能写出来。以后使用其它的平台,也能根据数据手册,操作寄存器,实现功能,并把代码封装,优化,这才是最好的结果。 项目式开发要避免重复造轮子,要尽快完成目标,而不是把时间浪费在不必要的细节上。库函数与寄存器两种开发方式并不对立,哪个方便用哪个即可。不同类型的库也是,那个方便用哪个即可。 |
|
|
|
只有小组成员才能发言,加入小组>>
3262 浏览 9 评论
2943 浏览 16 评论
3442 浏览 1 评论
8950 浏览 16 评论
4036 浏览 18 评论
1078浏览 3评论
558浏览 2评论
const uint16_t Tab[10]={0}; const uint16_t *p; p = Tab;//报错是怎么回事?
551浏览 2评论
用NUC131单片机UART3作为打印口,但printf没有输出东西是什么原因?
2286浏览 2评论
NUC980DK61YC启动随机性出现Err-DDR是为什么?
1846浏览 2评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-8 19:06 , Processed in 1.369447 second(s), Total 51, Slave 41 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号