GD32开发实战指南(基础篇) 第3章 GPIO流水灯的前世今生

开发环境:
mdk:keil 5.30
开发板:gd32f207i-eval
mcu:gd32f207ik
上一章通过控制gpio的高低电平实现了流水灯,但只是告诉了大家怎么做,如何实现流水灯,本文将深入剖析的gpio流水灯的前生今世,深入研究流水灯的调用逻辑和数据结构。
1 gpio配置概述前面一章大概讲解gpio的配置过程和核心的寄存器,当然啦,关于gpio的寄存器远不止我上一章列出来的,还有很多,具体请参看《gd32f20x_user_manual》中gpio相关的内容吧。
根据前面实现的gpio流水灯,本文将其归纳如下:
要想控制led亮灭,就需要做以上三件事:使能时钟,配置gpio参数,最后循环控制gpio的高低电平就能实现流水灯的效果,gpio的寄存器这里就不说了,更多详细的寄存器描述看官方手册就行,下面先来看看gd32的时钟。
2 gd32的时钟系统2.1 gd32的系统架构gd32的系统架构比51单片机强大很多了。首先我们看看gd32的系统架构图:
gd32f20x系列器件是基于arm® cortex®-m3处理器的32位通用微控制器。 arm® cortex®-m3处理器包括三条ahb总线分别称为i-code总线、 d-code总线和系统总线。
下面我们具体讲解一下图中几个总线的知识:
① icode 总线:该总线将 m3 内核指令总线和闪存指令接口相连,指令的预取在该总线上面完成。
② dcode 总线:该总线将 m3 内核的 dcode 总线与闪存存储器的数据接口相连接,常量加载和调试访问在该总线上面完成。
③ 系统总线:该总线连接 m3 内核的系统总线到总线矩阵,总线矩阵协调内核和 dma 间访问。
④ dma 总线:该总线将 dma 的 ahb 主控接口与总线矩阵相连,总线矩阵协调 cpu 的dcode 和 dma 到 sram,闪存和外设的访问。
⑤ 总线矩阵:总线矩阵协调内核系统总线和 dma 主控总线之间的访问仲裁,仲裁利用轮换算法。
⑥ ahb/apb 桥:这两个桥在 ahb 和 2 个 apb 总线间提供同步连接,apb1 操作速度限于60mhz,apb2 操作速度全速。
对于系统架构的知识,在刚开始学习 gd32 的时候只需要一个大概的了解,大致知道是个什么情况即可。
2.2 gd32时钟架构时钟是整个处理器运行的基础,时钟信号推动处理器内各个部分执行相应的指令。时钟系统就是cpu的脉搏,决定cpu速率,像人的心跳一样 只有有了心跳,人才能做其他的事情,而单片机有了时钟,才能够运行执行指令,才能够做其他的处理 (点灯,串口,adc),时钟的重要性不言而喻。
我们在学习51单片机时,其最小系统必有晶振电路,这块电路就是单片机的时钟来源,晶振的振荡频率直接影响单片机的处理速度。gd32相比51单片机就复杂得多,不仅是外设非常多,就连时钟来源就有四个。但我们实际使用的时候只会用到有限的几个外设,使用任何外设都需要时钟才能启动,但并不是所有的外设都需要系统时钟那么高的频率,为了兼容不同速度的设备,有些高速,有些低速,如果都用高速时钟,势必造成浪费,而且,同一个电路,时钟越快功耗越快,同时抗电磁干扰能力也就越弱,所以较为复杂的mcu都是采用多时钟源的方法来解决这些问题,因此便有了gd32的时钟系统和时钟树。
gd32三个不同的时钟源可以用来驱动系统时钟(ck_sys):
● irc8m晶振时钟(高速内部时钟信号)
● hxtal晶振时钟(高速外部时钟信号)
● pll时钟
gd32有两个二级时钟源:
● 40khz的低速内部irc40k,它可以驱动独立看门狗,还可选择地通过程序选择驱动rtc。 rtc用于从停机/待机模式下自动唤醒系统。
● 32.768khz的低速外部晶振lxtal,可选择它用来驱动rtc。
每个时钟源在不使用时都可以单独被打开或关闭,这样就可以优化系统功耗。
2.3 gd32的时钟系统gd32 芯片为了实现低功耗,设计了一个功能完善但却非常复杂的时钟系统。普通的mcu 一般只要配置好 gpio 的寄存器就可以使用了,但 gd32还有一个步骤,就是开启外设时钟。
在 gd32中,可分为五种时钟源,为 irc8m、hxtal、irc40k、lxtal、pll。从时钟频率来分可以分为高速时钟源和低速时钟源,其中 irc8m, hxtal以及 pll 是高速时钟,irc40k和 lxtal是低速时钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时钟源,其中 hxtal和 lxtal是外部时钟源,其他的是内部时钟源。
下面我们看看 gd32 的 5 个时钟源,我们讲解顺序是按图中红圈标示的顺序:
①irc8m是__高速内部时钟__,rc 振荡器,频率为 8mhz。
②hxtal是__高速外部时钟__,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4mhz~32mhz。我们的开发板接的是 25m 的晶振。当使用有源晶振时,时钟从 osc_in 引脚进入, osc_out 引脚悬空,当选用无源晶振时,时钟从 osc_in 和 osc_out 进入,并且要配谐振电容。当确定 pll 时钟来源的时候, hxtal可以不分频或者 2 分频,这个由时钟配置寄存器 cfg0 的位 17。
③irc40k是__低速内部时钟__,rc 振荡器,频率为 40khz。独立看门狗的时钟源只能是 irc40k,同时 irc40k还可以作为 rtc 的时钟源。
④lxtal是__低速外部时钟__,接频率为 32.768khz 的石英晶体。这个主要是 rtc 的时钟源。
⑤pll 为锁相环倍频输出,其时钟输入源可选择为 irc8m、hxtal。倍频可选择为2~32倍,但是其输出频率最大不得超过 120mhz。
图中我们用 a~e 标示我们要讲解的地方。
a. out是 gd32 的一个时钟输出io,它可以选择一个时钟信号输出, 可以选择为 pll 输出的 2 分频、irc8m、hxtal、或者系统时钟。这个时钟可以用来给外部其他系统提供时钟源。
b. 这里是 rtc 时钟源,从图上可以看出,rtc 的时钟源可以选择 irc40k,以及hxtal的 128 分频。
c. 从图中可以看出 c 处 usb 的时钟是来自 pll 时钟源。 gd32 中有一个全速功能的 usb 模块,其串行接口引擎需要一个频率为 48mhz 的时钟源。该时钟源只能从 pll 输出端获取,可以选择为 1/1.5/2/2.5 分频。
d. d 处就是 gd32 的系统时钟 sysclk,它是供 gd32 中绝大部分部件工作的时钟源。系统时钟可选择为 pll 输出、 irc8m或者 hxtal。系统时钟最大频率为 120mhz,当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。
e. 这里的 e 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最终来源都是 sysclk。sysclk 通过 ahb 分频器分频后送给各模块使用。这些模块包括:
①ahb 总线、内核、内存和 dma 使用的 hclk 时钟。
②通过 8 分频后送给 cortex 的系统定时器时钟,也就是 systick 了。
③直接送给 cortex 的空闲运行时钟 fclk。
④送给 apb1 分频器。apb1 分频器输出一路供 apb1 外设使用(pclk1,最大频率 60mhz),另一路送给定时器(timer)使用。
⑤送给 apb2 分频器。apb2 分频器分频输出一路供apb2外设使用(pclk2,最大频率 120mhz),另一路送给定时器(timer)使用。
其中需要理解的是 apb1 和 apb2 的区别, apb1 上面连接的是低速外设,包括电源接口、备份接口、 can、 usb、 i2c0、 i2c1、 uart1、 uart2 等等, apb2 上面连接的是高速外设包括 uart0、 spi0、 timer0、 adc0、 adc1、所有普通 io 口(pa~pg)、第二功能 io 口等。
不同的总线有不同的频率,不同的外设挂在不同的总线下,为了更适合初学者查阅,笔者把常用的外设与总线的对应关系总结如下:
systeminit()函数中设置的系统时钟大小:
sysclk(系统时钟) =120mhzahb 总线时钟(使用 sysclk) =120mhzapb1 总线时钟(pclk1) =60mhzapb2 总线时钟(pclk2) =120mhzpll 时钟 =120mhz值得注意的是,gd32f207系列有多个pll,具体参看源码。
2.4 gd32的时钟配置剖析既然时钟搞清楚了,接下来回到上一章的配置时钟的代码:
/*enable the led clock*/rcu_periph_clock_enable(rcu_gpiof );rcu_periph_clock_enable就是配置时钟的函数,函数原型如下:
/*! \\brief enable the peripherals clock \\param[in] periph: rcu peripherals, refer to rcu_periph_enum only one parameter can be selected which is shown as below: \\arg rcu_gpiox (x=a,b,c,d,e,f,g,h,i): gpio ports clock \\arg rcu_af : alternate function clock \\arg rcu_crc: crc clock \\arg rcu_dmax (x=0,1): dma clock \\arg rcu_enet: enet clock \\arg rcu_enettx: enettx clock \\arg rcu_enetrx: enetrx clock \\arg rcu_usbfs: usbfs clock \\arg rcu_exmc: exmc clock \\arg rcu_timerx (x=0,1,2,3,4,5,6,7,8,9,10,11,12,13): timer clock \\arg rcu_wwdgt: wwdgt clock \\arg rcu_spix (x=0,1,2): spi clock \\arg rcu_usartx (x=0,1,2,5): usart clock \\arg rcu_uartx (x=3,4,6,7): uart clock \\arg rcu_i2cx (x=0,1,2): i2c clock \\arg rcu_canx (x=0,1): can clock \\arg rcu_pmu: pmu clock \\arg rcu_dac: dac clock \\arg rcu_rtc: rtc clock \\arg rcu_adcx (x=0,1,2): adc clock \\arg rcu_sdio: sdio clock \\arg rcu_bkpi: bkp interface clock \\arg rcu_tli: tli clock \\arg rcu_dci: dci clock \\arg rcu_cau: cau clock \\arg rcu_hau: hau clock \\arg rcu_trng: trng clock \\param[out] none \\retval none*/void rcu_periph_clock_enable(rcu_periph_enum periph){ rcu_reg_val(periph) |= bit(rcu_bit_pos(periph));}整个函数就一个参数,其参数就是具体的外设时钟,整个函数很简单,就是打开具体的外设时钟。
参数periph传入值是通过宏来定义的,这样的好处也是便于移植,如果换了mcu,架构一样,只需要就该底层驱动就行,不需要更改上层应用,这样就提高了开发效率。言归正传,我们传入的rcu_gpioc定义如下。
rcu_gpiof是一个枚举类型。我们继续追溯以上的宏。
/* constants definitions *//* define the peripheral clock enable bit position and its register index offset */#define rcu_regidx_bit(regidx, bitpos) (((uint32_t)(regidx) <> 6)))#define rcu_bit_pos(val) ((uint32_t)(val) & 0x1fu)#define apb2en_reg_offset 0x18u /*!< apb2 enable register offset */#define bit(x) ((uint32_t)((uint32_t)0x01u<<(x)))/* rcu definitions */#define rcu rcu_base#define rcu_base (ahb1_bus_base + 0x00009000u) /*!< rcu base address */#define ahb1_bus_base ((uint32_t)0x40018000u) /*!< ahb1 base address */以上宏定义就是整个时钟初始化相关的宏定义了,将其带入函数中。rcu的基地址就是0x40018000+0x9000。可以从gd32参考手册中获取。
ahb1总线的基地址是0x40018000。
rcu偏移是0x9000。
rcu_reg_val(rcu_gpiof)最终的结果是0x40021018。
宏定义bit就是获取gpio具体的使能位。
bit(rcu_bit_pos(rcu_gpiof))最终的结果就是0x64。
最终的函数替换后如下:
0x40021018 |= 0x64;
都是宏定义直接替换就行,还是比较简单。
这里需要注意rcu_regidx_bit宏定义。
#define rcu_regidx_bit(regidx, bitpos) (((uint32_t)(regidx) <> 6)))#define rcu_bit_pos(val) ((uint32_t)(val) & 0x1fu)rcu的apb2使能寄存器如下:
这里配置gpiof的时钟,需要将第7位置1,因此转换成10进制就是64,和代码就匹配起来了。
gd32的固件库和stm32的固件库还是有一些差别的,但是不管如何,最终都是配置的寄存器,只是stm32通过结构体对外设进行了封装,gd32是通过宏定义直接替换,偏向于直接操作寄存器。
3 gd32的地址映射我们先看看51 单片机中是怎么做的,51 单片机开发中会引用一个 reg51.h 的头文件,51单片机是通过以下方式将名字和寄存器联系起来的:
sfr p0 =0x80;
sfr 也是一种扩充数据类型,占用一个内存单元,值域为 0~255。利用它可以访问 51 单片机内部的所有特殊功能寄存器。如用 sfr p1 = 0x90 这一句定义 p1 为 p1 端口在片内的寄存器。然后我们往地址为 0x80 的寄存器设值的方法是: p0=value;通过改变value的值来控制单片机。
所谓地址映射,就是将芯片上的存储器甚至 i/o 等资源与地址建立一一对应的关系。如果某地址对应着某寄存器,我们就可以运用 c 语言的指针来寻址并修改这个地址上的内容,从而实现修改该寄存器的内容。cortex-m的地址映射也是类似的。cortex-m有 32 根地址线,所以它的寻址空间大小为 2 32 bit=4 gb。arm 公司设计时,预先把这 4 gb 的寻址空间大致地分配好了。它把从 0x40000000 至 0x5fffffff( 512 mb)的地址分配给片上外设。通过把片上外设的寄存器映射到这个地址区,就可以简单地以访问内存的方式,访问这些外设的寄存器,从而控制外设的工作。这样,片上外设可以使用 c 语言来操作。
gd32f10x.h 这个文件中重要的内容就是把 gd32 的所有寄存器进行地址映射。如同51 单片机的 头文件一样,gd32f10x.h 像一个大表格,我们在使用的时候就是通过宏定义进行类似查表的操作,但是这样操作会很麻烦,而且32位的mcu寄存器很多,非常不方便。于是就有了现在的固件库。
在这里我们以流水灯中的 gpiof为例进行剖析,如果是其他的 io 端口,则改成相应的地址即可。在这个文件中一系列宏实现了地址映射。
#define apb2_bus_base ((uint32_t)0x40010000u) /*!< apb2 base address */#define gpio_base (apb2_bus_base + 0x00000800u) /*!< gpio base address */这几个宏定义是从文件中的几个部分抽离出来的,具体的内容读者可参考gd32f10x.h 源码。
宏apb2_bus_base指向的地址为 0x40010000。这个 apb2_bus_base宏是什么地址呢?gd32 不同的外设是挂载在不同的总线上的。gd32 芯片有 ahb 总线、apb2总线和 apb1 总线,挂载在这些总线上的外设有特定的地址范围。其中像 gpio、串口 1、adc 及部分定时器是挂载在称为 apb2 的总线上,挂载到apb2 总 线上的外设地址空间是从0x40010000 至 0x40017fff地址。这里的第一个地址,也就是 0x40010000,称为 apb2_bus_base(apb2 总线外设基地址)。
而 apb2 总线基地址相对于外设基地址的偏移量为 0x10000 个地址,即为 apb2 相对外设基地址的偏移地址。
最后到了宏 gpio_base,宏展开为 apb2_bus_base加上偏移量 0x1400得到了 gpio端口的寄存器组的基地址。
在gd32f20x_gpio.h 文件,我们还可以发现有关各个 gpio 基地址的宏。
/* gpiox(x=a,b,c,d,e,f,g,h,i) definitions */#define gpioa (gpio_base + 0x00000000u) /*!< gpioa bsae address */#define gpiob (gpio_base + 0x00000400u) /*!< gpiob bsae address */#define gpioc (gpio_base + 0x00000800u) /*!< gpioc bsae address */#define gpiod (gpio_base + 0x00000c00u) /*!< gpiod bsae address */#define gpioe (gpio_base + 0x00001000u) /*!< gpioe bsae address */#define gpiof (gpio_base + 0x00001400u) /*!< gpiof bsae address */#define gpiog (gpio_base + 0x00001800u) /*!< gpiog bsae address */#define gpioh (gpio_base + 0x00006c00u) /*!< gpioh bsae address */#define gpioi (gpio_base + 0x00007000u) /*!< gpioi bsae address */除了 gpiof寄存器组的地址,还有 gpioa、gpiob等地址,并且这些地址是不一样的。前面提到,每组 gpio 都对应着独立的一组寄存器,查看 gd32 的数据手册。
注意到这个说明中有一个偏移地址:0x1400,这里的偏移地址是相对哪个地址的偏移呢?下面进行举例说明。
4 固件库对寄存器的封装gd的工程师用结构体的形式封装了寄存器组,在gd32f20x_gpio.h文件定义的。
/* gpiox(x=a,b,c,d,e,f,g,h,i) definitions */#define gpioa (gpio_base + 0x00000000u) /*!< gpioa bsae address */#define gpiob (gpio_base + 0x00000400u) /*!< gpiob bsae address */#define gpioc (gpio_base + 0x00000800u) /*!< gpioc bsae address */#define gpiod (gpio_base + 0x00000c00u) /*!< gpiod bsae address */#define gpioe (gpio_base + 0x00001000u) /*!< gpioe bsae address */#define gpiof (gpio_base + 0x00001400u) /*!< gpiof bsae address */#define gpiog (gpio_base + 0x00001800u) /*!< gpiog bsae address */#define gpioh (gpio_base + 0x00006c00u) /*!< gpioh bsae address */#define gpioi (gpio_base + 0x00007000u) /*!< gpioi bsae address */有了这些宏,我们就可以定位到具体的寄存器地址,gd32f10x_gpio.h 文件中定义了以下类型的宏定义。
/* gpio registers definitions */#define gpio_ctl0(gpiox) reg32((gpiox) + 0x00000000u) /*!< gpio port control register 0 */#define gpio_ctl1(gpiox) reg32((gpiox) + 0x00000004u) /*!< gpio port control register 1 */#define gpio_istat(gpiox) reg32((gpiox) + 0x00000008u) /*!< gpio port input status register */#define gpio_octl(gpiox) reg32((gpiox) + 0x0000000cu) /*!< gpio port output control register */#define gpio_bop(gpiox) reg32((gpiox) + 0x00000010u) /*!< gpio port bit operation register */#define gpio_bc(gpiox) reg32((gpiox) + 0x00000014u) /*!< gpio bit clear register */#define gpio_lock(gpiox) reg32((gpiox) + 0x00000018u) /*!< gpio port configuration lock register */这里定义了 7 个宏定义,两个宏之间是4 个字节地址的偏移量。
0x010偏移量正是 gpiox_bop寄存器相对于所在寄存器组的偏移地址。
通过类似的方式,我们就可以给具体的寄存器写上适当的参数以控制 gd32 了。
这样我们就可以通过库函数实现了gpio的初始化了。
/*! \\brief gpio parameter initialization \\param[in] gpio_periph: gpiox(x = a,b,c,d,e,f,g,h,i) \\param[in] mode: gpio pin mode only one parameter can be selected which is shown as below: \\arg gpio_mode_ain: analog input mode \\arg gpio_mode_in_floating: floating input mode \\arg gpio_mode_ipd: pull-down input mode \\arg gpio_mode_ipu: pull-up input mode \\arg gpio_mode_out_od: gpio output with open-drain \\arg gpio_mode_out_pp: gpio output with push-pull \\arg gpio_mode_af_od: afio output with open-drain \\arg gpio_mode_af_pp: afio output with push-pull \\param[in] speed: gpio output max speed value only one parameter can be selected which is shown as below: \\arg gpio_ospeed_10mhz: output max speed 10mhz \\arg gpio_ospeed_2mhz: output max speed 2mhz \\arg gpio_ospeed_50mhz: output max speed 50mhz \\param[in] pin: gpio pin one or more parameters can be selected which are shown as below: \\arg gpio_pin_x(x=0..15), gpio_pin_all \\param[out] none \\retval none*/void gpio_init(uint32_t gpio_periph, uint32_t mode, uint32_t speed, uint32_t pin){ uint16_t i; uint32_t temp_mode = 0u; uint32_t reg = 0u; /* gpio mode configuration */ temp_mode = (uint32_t)(mode & ((uint32_t)0x0fu)); /* gpio speed configuration */ if(((uint32_t)0x00u) != ((uint32_t)mode & ((uint32_t)0x10u))) { /* output mode max speed: 10mhz, 2mhz, 50mhz */ temp_mode |= (uint32_t)speed; } /* configure the eight low port pins with gpio_ctl0 */ for(i = 0u; i < 8u; i++) { if((1u << i) & pin) { reg = gpio_ctl0(gpio_periph); /* clear the specified pin mode bits */ reg &= ~gpio_mode_mask(i); /* set the specified pin mode bits */ reg |= gpio_mode_set(i, temp_mode); /* set ipd or ipu */ if(gpio_mode_ipd == mode) { /* reset the corresponding octl bit */ gpio_bc(gpio_periph) = (uint32_t)((1u << i) & pin); } else { /* set the corresponding octl bit */ if(gpio_mode_ipu == mode) { gpio_bop(gpio_periph) = (uint32_t)((1u << i) & pin); } } /* set gpio_ctl0 register */ gpio_ctl0(gpio_periph) = reg; } } /* configure the eight high port pins with gpio_ctl1 */ for(i = 8u; i < 16u; i++) { if((1u << i) & pin) { reg = gpio_ctl1(gpio_periph); /* clear the specified pin mode bits */ reg &= ~gpio_mode_mask(i - 8u); /* set the specified pin mode bits */ reg |= gpio_mode_set(i - 8u, temp_mode); /* set ipd or ipu */ if(gpio_mode_ipd == mode) { /* reset the corresponding octl bit */ gpio_bc(gpio_periph) = (uint32_t)((1u << i) & pin); } else { /* set the corresponding octl bit */ if(gpio_mode_ipu == mode) { gpio_bop(gpio_periph) = (uint32_t)((1u << i) & pin); } } /* set gpio_ctl1 register */ gpio_ctl1(gpio_periph) = reg; } }}然后再main函数中调用gpio\\_init\\(\\)函数接口对gpio初始化了。通过对时钟和gpio的分析,我想大家已经对固件的逻辑有了一定的认识,从本质上讲,都是在配置寄存器,只是地址和值不同罢了,而固件库就是对寄存器配置的封装,便于开发者调用。
值得注意的是,gd32的固件库并没有使用结构体来对寄存器组进行封装,全程用的宏定义,这点和stm32有很大的不同。

基于GPRS和GPS技术相结合实现车载系统终端的设计
一文读懂2018年大数据的增长趋势 十张图告诉你
基于TL431的低压差直流稳压电源设计
5G芯片采用7nm,紫光展锐能做到吗?
半导体光电及激光智能制造技术会议在苏州圆满落幕
GD32开发实战指南(基础篇) 第3章 GPIO流水灯的前世今生
企业组织成功进行数字化创新的关键要点和措施
EPP接口协议的原理及实现PC与单片机系统间高速传输的电路设计
交错并联反激式准单级光伏并网微逆变器(安科瑞 王琪)
无刷直流电机霍尔传感器的作用
IC Insights:半导体资本年支出将首次超过千亿美元,存储占比53%
某水下机器人供电系统干扰改善方案浅析
浅析液压伺服系统工作原理及电气故障诊断
液晶显示器色彩数
Linux汇编启动relocate重定向分析
LCD驱动芯片VK1056B/C概述及特点
异步电机的改进型电压模型磁链观测器介绍
小米6最新消息:小米6价格被曝光:这次真的不是1999元,雷军很烦恼!
彩电维修口诀
美国高通公司宣布为电视和机顶盒推出全新超高清处理器