OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 64652|回复: 208

学习正点原子精英STM32版的学习笔记

  [复制链接]

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
发表于 2018-6-3 19:04:34 | 显示全部楼层 |阅读模式
刚看到有前人发布自己学习中的持续更新贴作为鞭策自己不断学习的方法,我也做个学习笔记,把过程中的点点滴滴写下来。
其实写笔记我一直使用复制粘贴保存在电脑里,但最近我喜欢上了用笔写在笔记本上,觉得更有价值更有效率。
--------------------------
先来谈谈STM32的常识。
ARM公司其实不生产芯片,靠什么赚钱呢?靠授权。ARM研发内核然后授权给各个芯片厂家,靠授权费用赚钱,这个是个好注意。毕竟研发和生产在环节上很不同,一家科技公司很难占据产品的各个环节,占据了也就作茧自缚了,就像昔日的诺基亚。
但毕竟内核只是芯片的一部分,内核之外叫做片上外设,意思就是芯片上的相对于内核而言的外部设备,比如GPIO。
芯片厂家根据内核自己设计相关的片上外设成为单片机,如果没有统一的标准那么各个厂家的芯片就不能通用。
意法半导体公司就是其中一家授权生产ARM的公司,当然意法的产品不只ARM芯片。
芯片内核也是升级换代的,我以前知道的ARM其实是比较早的内核产品。精英版的STM32是比较新的内核,叫Cortex-M3,手机上的叫Core-A,A7、A8之类,是很先进的东西。
微控制器芯片Cortex-M3、M4、M7,一个比一个先进。
-------------------------------
事实上使用汇编编写程序已经变得很难了,51时代就有使用C来设计51程序了。学校里面曾看到高年级同学使用手操器来下载程序,那种是在纸上写好汇编程序然后自己手动通过手操器(不知道叫什么合适)一条条下载到试验机里面的,连个电脑软件都没有。想来那种手操器兼具汇编和烧制的功能。我是学PLC的,我以前也是这么学习PLC的。C语言是比较底层的语言,用来编写单片机程序就理所当然了。单片机的C语言采用的编译器和普通的C语言不同,可以把单片机的C语言看作是C的一个子集,并不是所有C的特性均支持。
要想用C来写单片机程序当然要做些底层封装,可以自己封装,事实上芯片公司提供了标准封装,ST公司提供的STM32库函数就是这样的封装。
------------------------------
Keil公司是另外一家公司,是软件公司,提供开发单片机的工具,从51到ARM均有。
ST公司也有自己的开发工具,为什么我们都用Keil呢?业有所专嘛。
Keil 提供的STM32开发工具似乎换了个名字,叫MDK,不过装好后还是叫Keil。
Keil不是免费的,以前很难找到破解的Keil,现在我们使用Keil,所以我们都懂了。
Keil是个IDE工具,除了编辑程序外还提供编译器,并且通过简单选择就配置好了开发链路,不用自己配置,类似与其他专有语言开发工具,比如微软的VS。
换句话说如果开发者要自己配置开发链路的话也是可以的,只要够专业。通过查看,安装好的Keil在目录下的\ARM\ARMCC\bin里就是编译工具,其他 目录里面提供单片机的include、lib。
为什么还要从ST官网下载库呢?官网的比较新嘛。Keil提供的库我想也是ST提供的,不过可能不会同步更新。
Keil的编辑器不好用,不过keil提供从其他编辑器来操作的可定制菜单,看来Keil还是知道自己的短板的。
现在比较不错的编辑器有gvim/sublime text/vs code,不过这些都是普通编辑器,如果要在开发中实现跳转、自动完成的话需要插件,这些都是学习成本,就看值不值了。
补充:
其实开发STM32除了在windows平台外还有Linux平台和Mac平台也可以,这对于极客来说是个好消息。具体看ST公司提供的开发套件,从我了解的信息看是集成在Eclipse的开发环境。
----------------------------------
学习STM32除了软件外其实很重要的还有硬件,电子工程这类。我学习STM32纯属偶然,我在PLC设计中需要使用一块带Modbus RTU通讯的中继板来做扩展,这块中继板设置通讯奇偶校验时只能使用固定的无校验,这让我很郁闷,系统中其他站都要设置成无校验才行。所以有了我是不是可以修改下的冲动。电子工程没弄过,这个是个拦路虎,我想我以后如果中断学习了可能是因为这个。









正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-4 10:27:21 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-4 10:34 编辑

从目前了解的看,STM32的操作其实就是对内部存储器的操作,寄存器是映射到存储器地址空间上的。
但这些存储器地址可不像普通内存空间一样可以按照自己的需要来操作,它需要遵循硬件特性,先操作哪个再操作哪个。
首先需要了解的是存储器地址映射。
STM32自然是32位机,存储器可寻址空间为4GB,即0x0000,0000~0xFFFF,FFFF。
这里有个通常所说的1024问题,其实这是为了在数字上好用16进制来表示才规定的,说是规定不如说是约定俗成。1k=1024,表示成16进制刚好是0x400,如果1k=1000表示成16进制就是0x3E8,那么2k=0x7D0就不如表示成1024时2k=0x800好表示:2k=2*0x400=0x800。看,刚好是400的2倍。
这些地址空间被粗略地分成了8个块,每块512MB,其中第0、1、2块最重要。
block0:是内部Flash,用于储存用户程序代码和其他必须的工厂代码。可是芯片并非全提供这么多的实体Flash给我们用,比如精英版的STM32F103ZET只有512kB可以用来编程就已经是大容量了。第0块大量的是预留空间,其实是为了将来芯片升级不用再重新设计才预留的,预留就是在该芯片上不提供的意思。
block1:是内部SRAM,也就是我们说的内存,易失性。这512M也只有部分可用,精英版的STM32F103ZET是64kB。片上内存总是很贵的。
block2:是以后学习的重灾区了,片上外设映射到此。
片上外设分两种:低速和高速。低速外设用到APB1总线,或者说APB1就是低速外设,映射到存储器地址是:0x4000,000~0x4000,77FF.
高速外设又分两种:APB2和AHB总线。APB2映射地址为:0x4001,000~0x4001,3FFF。AHB地址为:0学4002,000~0x5003,FFFF。看来是AHB总线的地址范围大。
block2的512MB也没有全部提供使用,使用到的空间也不是全都连续。功能不同的总线间有预留空间。
------------------------
为什么是总线映射而不是直接寄存器映射呢?寄存器是设备的控制单元,而设备是挂载在总线上的,无论是外设还是核内设备都如此。总线天然地有地址,这对于有一点点51常识的我来说还是比较好理解的。

可以这么理解:总线上的设备和存储器(Flash/SRAM)是统一编址的。
回复 支持 11 反对 0

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-28 16:45:07 | 显示全部楼层

大声说出来,让别人知道你在做什么,自己才能有学习的动力。
回复 支持 9 反对 0

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-20 22:13:50 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-21 14:30 编辑

最近在学习SysTick定时器,内容还是比较好理解的。
SysTick是内核定时器,有4个相关寄存器,LOAD和VAL是24位寄存器,一直认为STM32的寄存器都是32位的,这下毁三观了。
我在M3权威指南和中文参考里面都没有发现这四个寄存器中CTRL和CALIB的具体定义,只看到CTRL中16、2、1、0位的定义,其他位没有说明,怎么回事儿呀?
再看misc.c中的SysTick定时器初始化函数SysTick_CLKSourceConfig(),传入的是宏常量,关于是配置外部HCLK的还是内部HCLK作为SysTick时钟源,宏定义是:
[mw_shl_code=c,true]#define SysTick_CLKSource_HCLK_Div8 ((uint32_t)0xFFFFFFFB)
#define SysTick_CLKSource_HCLK ((uint32_t)0x00000004)[/mw_shl_code]
SysTick_CLKSourceConfig()里面直接用这个宏来赋值CTRL寄存器,其中如果是内部HCLK做源第2位为1,则使用“或”,这个好理解,就是把CTRL的第2位赋值为传入的参数。如果是外部HCLK则使用“与”:
[mw_shl_code=c,true]  if (SysTick_CLKSource == SysTick_CLKSource_HCLK)  {
    SysTick->CTRL |= SysTick_CLKSource_HCLK;
  }
  else
  {
    SysTick->CTRL &= SysTick_CLKSource_HCLK_Div8;
  }[/mw_shl_code]
外部HCLK值做源为0xFFFFFFFB,低4位B是二进制1011,第2位是0,采用AHB的8分频。第2位&也容易理解。
第0位第1位是1,使能SysTick定时器和定时到产生中断。“与”1就是不用管,不产生影响了。
所以,如果以后要使某位置1则data中的某位为1其他位0,然后或。
如果要使某位置0则data中某位为0其他位为1,然后与。

回复 支持 7 反对 1

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 13:42:48 | 显示全部楼层
warship 发表于 2018-6-10 11:10
楼主写得真好,
照这个思路,
完全可以出书了.

纯粹的笔记,不排除有抄的味道,不过也是我自己抄的。
我刚学习,点滴记录,我认为是我的学习之道。
回复 支持 6 反对 1

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-5 09:40:01 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-8 14:59 编辑

来看STM32的寄存器是如何用C封装的。首先学习的是GPIO,所以只能看看GPIO的寄存器如何封装的。其他设备的寄存器举一反三。
精英版的STM32F103ZET的GPIO有7组,每组16Pin。也就是GPIOA /GPIOB...GPIOG。
GPIO是片上外设,挂在APB2总线上,也就是高速总线,一个IO外设为什么是高速的呢?我也不清楚,不过IO不要以为只是简单的按键输入和继电器输出,还有可以用到编码器,这都需要高速的。
先来看地址的对应关系:APB2总线的地址从0x4001,0000开始到0x4001,3FFF结束,而GPIOA作为挂在APB2上的设备,地址从0x4001,0800开始,GPIOA一共有7个寄存器,也就是说每组都有7个寄存器,STM32的寄存器都是32位的,2个word4个字节,并且有一点值得关注,就是这7个寄存器都是一个接一个的,地址连续,这让封装有了极好的机会。GPIOA占了4*7=28个字节,那GPIOB应该从0x4001,0800+28=0x4001,081C开始了吧?不是,每组地址之间有很大的空隙,GPIOB地址从0x4001,0C00开始,中间差不多有996个字节用不到,不知道做什么用,不会持家呀。只能一种解释,就是方便书写或者是为了扩展预留。不过从以前的一些零星知识猜测是因为挂在总线上的设备地址受限于硬件设计不可能连续,总有地址间隔,比如说使能位就能够空出很大空间。
这样,GPIOB从0x4001,0C00开始,GPIOC从0x4001,1000开始,GPIOD从0x4001,1400开始...直至GPIOG从0x4001,2000开始。
这些地址对于一块芯片来说总是固定的,不可能改变。
而GPIO的寄存器呢?GPIOA的寄存器说了是连续的,也就是说哪个地址是哪个寄存器是固定的。这样7组GPIO的每组7个寄存器的排列一样。
数字地址总是难以记忆难以理解实际作用的,这样就需要一个别名。这对于高级语言来说不是事。
可以把所有的总线地址、寄存器地址都用别名来表示,来看看ST库里面是如何实现的。
为了简单起见,也为了编写减少出错几率,采用了偏移地址的方法,也就是固定一个基址,其他地址都是相对基址的偏移,这样一环环编写可以减少很多不必要的麻烦也减少了书写错误。
首先定义的是总线地址APB2,APB2是外设地址上的一部分,所以需要定义外设地址:[mw_shl_code=c,true]#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */[/mw_shl_code]
其实也就是block2的基地址。
APB2这样定义:
[mw_shl_code=c,true]#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000)[/mw_shl_code]
就是在外设基地址上加偏移0x10000,这样APB2的地址其实是:0x4001,0000。
明白了这个其他的好理解了。
GPIOA~GPIOG的定义是:
[mw_shl_code=c,true]#define GPIOA_BASE            (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE            (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE            (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE            (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE            (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE            (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE            (APB2PERIPH_BASE + 0x2000)[/mw_shl_code]
--------------------------------
GPIO上的寄存器呢?
这个需要一点点技巧。因为7组的寄存器排列一样且连续,刚好对应C的结构体特性,那就用结构先定义7个寄存器。
[mw_shl_code=c,true]typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;[/mw_shl_code]
结构体里面的寄存器顺序很重要,不能排错,因为这是利用了C的结构体语法特性来定义寄存器的。
定义好寄存器结构后如果使用GPIOA的BSRR寄存器(举例)就这样用:GPIOA_BASE->BSRR。
不过这样做似乎还不行,因为GPIOA_BASE怎么说也只是个数字不是地址,所以要先强制转换为指针。
[mw_shl_code=c,true]#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)[/mw_shl_code]
GPIO_TypeDef就是刚说的寄存器结构,这样GPIOA就是寄存器结构体指针了,可以直接使用GPIOA->BSRR了。
--------------------------
这些定义位于stm32f10x.h头文件中。
还有,寄存器的读写需要32位读写,不能16位读写两次,这个我没有试验,因为不像普通语言说试就试,还有很多东西需要了解。
--------------------------------
(补充)
在学习RCC时钟配置过程中发现寄存器并非只能按照32位来操作,也可以按照字节、半字、字来操作,具体并非每个都可以,需要参考手册。









回复 支持 6 反对 0

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-8 09:48:42 | 显示全部楼层
RCC复位及时钟控制。
学习STM32一开始就点亮LED等就如同学习高级语言一开始总是从“Hello World!"开始一样,不过这似乎避开了一个重要的知识点,就是系统时钟。STM32要想工作离不开时钟,不同于51单片机,STM32有多个时钟,主要是因为它复杂嘛。
时钟源其实就四个:内部时钟源两个HSI和LSI、外部时钟源两个HSE和LSE。PLL叫做锁相倍频器,不是一个物理独立的时钟源,但通常也把PLL作为一个时钟源看待,这样就有了 5个时钟源。
高速和低速时钟当然是时钟频率不同了,精英版的外部高速时钟HSE是8MHz而低速外部时钟LSE是32.768kHz。内部时钟是芯片内自带的。
这么多时钟都是用来服务应用的,有这么几个时钟应用:
首先是独立看门狗IWDG时钟,它只能从内部低速时钟LSI配置。
其次是实时时钟RTC,它可以由三种时钟来配置:内部低速时钟LSI、外部低速时钟LSE、外部高速时钟HSE的128分频。
然后是USB时钟,来自于PLL的预分频。
更多的应用,比如总线更多的和系统时钟有关。
系统时钟可以由三种时钟源配置:HSI、HSE和PLL。
内部高速时钟HSI更多地是作为外部高速时钟HSE的冗余时钟出现,一旦HSE失效就自动由HSI接替。
------------------------------
系统时钟是内核CPU工作的时钟,开发STM32更多地关注外设总线:AHB、APB1、APB2。
外设总线时钟都是由系统时钟经由AHB的预分频得到。
-------------------------------
时钟是个拦路虎,看似复杂,不过我想无外乎就是为了适应不同的需求而设立的,为的是以逸待劳,以不变应万变,作为学习没必要太过纠结这些内容。
不过时钟是如何被配置的呢?
当系统上电后不是一来就执行main(),而是有个初始化过程,这个初始化就是startup文件的内容。
也就是说startup是独立于main的程序,学习阶段把它看作是ST公司提供的不变的东西吧。
库里面的startup 是由汇编设计的,看不懂,主要是进行系统堆栈分配、中断向量分配和时钟配置,然后交由用户的main()。
这些内容其实在51单片机里面也有,不过似乎没有时钟分配。
在没有进行时钟配置前系统执行启动程序是用的什么时钟呢?有内部时钟嘛。
一旦配置好时钟后系统就开始执行用户程序了,记住一点:所有外设如果要工作除了需要配置好时钟外(在main()前就已经就绪了)还要使能。
回复 支持 4 反对 0

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-6 19:53:34 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-8 15:05 编辑

有关STM32固件库里面的一些C语法笔记。
查看固件库,里面有很多在通常的C语言教程中不常看到的语法内容,总结如下(占贴,逐步记录):
__I:其实是个宏定义,在core_cm3.h里面:
[mw_shl_code=c,true]#ifdef __cplusplus
  #define     __I     volatile                /*!< defines 'read only' permissions      */
#else
  #define     __I     volatile const          /*!< defines 'read only' permissions      */
#endif
#define     __O     volatile                  /*!< defines 'write only' permissions     */
#define     __IO    volatile                  /*!< defines 'read / write' permissions   */
[/mw_shl_code]
除了__I以外还有__O、__IO等等,都是volatile的别名,意思是要求编译器不作优化而直接读取变量的真实地址上的内容。
怎么优化呢?复杂,好比说iTempA = iTempB * 2,下一步如果使用到iTempA就会一直认为iTempB * 2的值就是iTempA的值,而实际上可能在其他地方iTempA被其他程序改变,所以如果使用到iTempA的话就不能认为是iTempB * 2而应该从iTempA的实际地址读取。
这只是类比,STM32是关于硬件的,iTempA如果被volatile修饰就应该读取它的真实地址内容不应该读取缓冲区内容。用书上的话就是“易失性”。
从宏定义可以看出来,无论是__O还是__IO都是volatile,并无区别,这个只是给程序员看的,没有语法上的约束。唯一不同的是__I,是const,常量。
不过具体怎么用呢?目前不得而知。
-------------------------
库里面有一些很奇特的数据类型,比如uint32_t,uint16_t,虽然猜能够猜到是什么,不过具体是什么呢?
使用Keil很容易找到定义处,在c:\Keil_v5\ARM\ARMCC\include\stdint.h里面定义。(可是我的项目中却没有#include这个头文件呀!!看来Keil调用编译器的时候自动包含的,Keil自带的include不是没用啊。)
stdint.h里面定义了许多这样的数据类型,大多是C原生数据类型的typedef,理解为别名好了。
[mw_shl_code=c,true]    /* exact-width signed integer types */
typedef   signed          char int8_t;
typedef   signed short     int int16_t;
typedef   signed           int int32_t;
typedef   signed       __INT64 int64_t;

    /* exact-width unsigned integer types */
typedef unsigned          char uint8_t;
typedef unsigned short     int uint16_t;
typedef unsigned           int uint32_t;
typedef unsigned       __INT64 uint64_t;
[/mw_shl_code]
关于硬件的数据类型可能不会自动识别为int、float之类,这些都是需要使用者自己来识别来定义的,所以硬件的数据大多只和数据是存储在16bit还是32bit空间有关。
8位数据用C来定义就是signed char 或unsigned char ,而16位就是signed short int或unsigned short int,类似地,常用的寄存器是32位,就是unsigned int。
还是为了防止以后出现类型错误,减少编写出错几率。
-------------------------------------
在stm32f10x.h中还定义了几个更精简的类型:vu32/vu16/vu8这类。
[mw_shl_code=c,true]typedef __IO uint32_t  vu32;
typedef __IO uint16_t vu16;
typedef __IO uint8_t  vu8;
[/mw_shl_code]
这是在uint32_t的基础上加上了__IO修饰,定义了这么多,记忆是个问题。我现在的理解是:如果有__IO等修饰,那么就是从硬件读或写数据,不能用简单赋值。
----------------------------------------
关于字、字节。
我一直在做PLC项目,西门子PLC对于字和字节的定义是16位和8位的,用惯了PLC再来学习C用些对字有迷惑的地方,原来C中的字是32位的,16位是半字,字节还是8位,哈哈。



回复 支持 3 反对 0

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-22 18:22:24 | 显示全部楼层
xiatianyun 发表于 2018-6-21 23:44
嗯,有道理,就像for从0开始总是

装载的时间周期非常短,
只是数个指令周期而已,基本在us就完成了。
但装载定时器周期时,
无论是ms量级还是us量级的,
都是统一要求减一的,
所以认为:减一是考虑到装载时间的想法是不合适的。
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 1 反对 1

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-21 23:17:46 | 显示全部楼层
个人认为:
基本上所有定时器装载初值的时候都是要-1的,
这并不是因为装载需要时间,
而是因为计数器是过0才会溢出,
所以假设要计10个脉冲, 装载值为9即可
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 2 反对 0

使用道具 举报

0

主题

11

帖子

0

精华

初级会员

Rank: 2

积分
52
金钱
52
注册时间
2017-8-13
在线时间
8 小时
发表于 2018-6-8 16:37:23 | 显示全部楼层
回复 支持 0 反对 1

使用道具 举报

2

主题

168

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
336
金钱
336
注册时间
2018-5-14
在线时间
69 小时
发表于 2018-6-3 19:06:16 | 显示全部楼层
支持,保持下去
回复 支持 反对

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-4 21:44:41 | 显示全部楼层
笔记记得不错,
注意保持,
我们会常来看看哦.
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 1 反对 0

使用道具 举报

530

主题

11万

帖子

34

精华

管理员

Rank: 12Rank: 12Rank: 12

积分
165352
金钱
165352
注册时间
2010-12-1
在线时间
2108 小时
发表于 2018-6-5 01:12:03 | 显示全部楼层
不错,谢谢分享,继续努力。
回复 支持 1 反对 0

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-5 17:13:12 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-5 18:29 编辑

继续。
如果论坛编辑器支持markdown就好了。
知道了一点C对STM32的封装后基本可以对库里面的很多语法上的东西有个大体了解了,当然纯粹是基于语法层面的。
还是尽快安装好Keil工具上手吧,学得再多不如动一下手。
如何下载安装破解就略过了。
Keil不光针对STM32F103,也不太可能专为一家公司开发,所以还是需要费点劲来配置的。
首先是得到官方库,从ST官网下载,我下载了多次结果没有成功过,算了,还是用现成精英提供的吧。
固件库在A盘\STM32参考资料\STM32F1xx固件库里面,隐藏得较深。参考资料和固件库有点归类不搭边,很容易错过,知道名称的话使用搜索工具很容易得到。
固件库的名字叫做STM32F10x_StdPeriph_Lib_V3.5.0,包含Libraries既是,其他的是一些例程工具,暂时不明白作用。
跟着视频做好模板就可以了,不过似乎可以按照自己的想法来做模板也是可以的。
-----------------------------
自己做模板:
其实需要用到的库一开始不用全部包含,但这个需要知道哪个文件是做什么的,对于初学的我来说这不现实。
复制Libraries到自己的模板目录里面,不带中文。现在模板目录为e:\Stm32Course\Template。
现在生产以下目录:Libraries、Project、User这个是主要目录,Project主要是Keil项目的目录,里面是uvprojx文件也就是Keil项目文件。
用Keil新建项目时定位在Project目录除了生成项目文件外还自动生成Listings和Objects目录,这两个目录用来存放编译过程自动产生的过程文件,也就是汇编、目标之类的文件,按照教程应该把这些文件放到模板根目录下的Listing和Output目录,我想这个根据自己的喜好决定了。我还是按教程来吧。
其中Output里面有下载到芯片的Hex文件,这个需要在Keil里面选择才能生成。
User目录放置程序。Doc是文档,说明之类。
-----------------------------
现在对Keil进行配置,这是关键。
Group和目录其实可以对应起来,也可以理解为实体目录里面是些乱七八糟的资源,Group里面才是项目需要的组合。
Group里面新建几个:STARTUP、CMSIS、FWLB、USER、DOC。和目录不对应了。
STARTUP是启动文件,只放置startup_stm32f10x_hd.s文件。
CMSIS是内核文件,放置core_cm3.c、system_stm32f10x.c和stm32f10x.h文件。
FWLB是外设文件,也就是外设驱动,把固件库里面的src目录全放进来。inc目录是头文件,已经在src相关文件中包含了,不用重复放置。
---------------------------
附加说明:
其实不用这么麻烦,一开始对于Keil里面的Group不知道做啥用的,也不知道这几个Group和目录有什么区别。
我的理解是我们这么做恰恰把新建项目的顺序颠倒了,我们先有目录然后在Group,正常顺序应该在Keil里面Group,由Keil自动生成目录及文件。
现阶段的学习即不用对库文件进行修改也不会,所以Startup、Cmsis、Fwlb这些目录纯粹为了查看的目的而存在。其实可以在有了实体目录即库文件后只在Group里面新建一个USER来放置设计的程序就可以了。
关键的关键是需要对Keil进行设置,需要设置IncludePath。
------------------------------------------------------------------------


回复 支持 1 反对 0

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-5 21:24:10 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-5 21:26 编辑

Keil编译配置:
1、Output选项:选择Objects目录,定位在/Template/Output目录;
勾选Create HEX File,这个是下载到芯片中中的映像文件。
2、Listing选项:选择Listing目录,定位在/Template/Listing目录;
我想Listing中的文件关系到tag跳转定位,所以如果项目不进行一次编译是不能进行符号、函数的跳转的。
3、C/C++选项:这个比较关键。首先是需要预定义两个宏:STM32F10X_HD宏,关系到把项目编译成哪种内存容量的程序,如果是高容量就写这个。
USE_STDPERIPH_DRIVER宏,这个用在stm32f10x.h头文件中,如果定义了这个宏则会自动包含stm32f10x_conf.h这个头文件。这个头文件其实是关于外设头文件的总包含头文件,有了这个头文件就不用把外设头文件一个个include了。这个包含链是这样的:stm32f10x.h--->stm32f10x_conf.h--->很多个外设头文件,也就是外设目录/inc目录下的头文件。所以程序中只需要#include <stm32f10x.h>这一个头文件即可。
宏定义可以在C头文件中定义,这里改在Keil中定义,会自动在以后编译时被作为参数一起编译。好处当然是方便进行芯片更换。
之后是比较麻烦的IncludePath 设置了。
需要添加四个路径包含:Libraries/CMISIS/CM3/DeviceSupport/ST/STM32F10x  +  Libraries/CMISIS/CM3/CoreSupport    +    Libraries/STM32F10x_StdPeriph_Driver/inc  +   /User
这些全都是实体目录,和Group无关。第一个路径上有启动文件和stm32f10x.h头文件和system_stm32f10x.c文件和.h文件。
第二个路径上有2个内核文件core_cm3.c及.h文件。
第三个路径上有外设头文件。第四个路径上就是自己的程序文件。
如果不设置IncludePath或者设置不正确,那么编译时就不能正确找到项目中用到的所有文件导致编译出错。
其实这是因为所有文件中的#include 包含预处理这是写了简单的头文件名,不含路径,编译程序认为是在项目同一个目录里面,但实际上并不是所有文件都处于同一个目录,所以需要指明到哪里搜寻所需的文件。当然,如果不包含一些路径也可以编译,编译程序会自动到Keil安装目录下的include目录找,里面的文件可能不会是我们需要的或者说可能是旧的。


---------------------
如果不调试,差不多就可以了。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-5 21:35:38 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-5 21:36 编辑

还有一些重要的文件没有安装到项目中去。
首先是stm32f10x_conf.h头文件,说过是一个包含所有外设头文件的总头文件,那么需要哪些外设自然由程序员决定,所以这个头文件其实固件库里面可能是没有的,反正我在库里面没有发现,编译时就出错了。
这个文件现在学习阶段就只好复制例程了,到其他示例中找到随便看看就复制到User里面来吧。
还有中断处理的两个文件stm32f10x_it.h和.c文件,这两个是中断处理的用户程序,暂时没有只需要空空的函数定义即可,只让编译通过就行。复制示例文件到User中。
回复 支持 反对

使用道具 举报

530

主题

11万

帖子

34

精华

管理员

Rank: 12Rank: 12Rank: 12

积分
165352
金钱
165352
注册时间
2010-12-1
在线时间
2108 小时
发表于 2018-6-6 01:28:37 | 显示全部楼层
xiatianyun 发表于 2018-6-5 21:24
Keil编译配置:
1、Output选项:选择Objects目录,定位在/Template/Output目录;
勾选Create HEX File, ...

不错,继续
我是开源电子网www.openedv.com站长,有关站务问题请与我联系。
正点原子STM32开发板购买店铺http://openedv.taobao.com
正点原子官方微信公众平台,点击这里关注“正点原子”
回复 支持 1 反对 0

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-6 20:29:23 | 显示全部楼层
写得不错,
蛮会总结的.
坚持, 继续!!
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-8 10:11:02 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-8 10:37 编辑

来看看库里面的时钟配置程序。
startup 里面有个调用SystemInit()函数的过程:
[mw_shl_code=asm,true]; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP
[/mw_shl_code]
这个SystemInit在哪里定义呢?在system_stm32f10x.c中定义。
SystemInit()快结束时调用了SetSysClock()函数,时钟配置就由该函数具体实施。
------------------------------------------------------
SetSysClock()也在system_stm32f10x.c中定义。
其实该函数很短,它根据不同的宏定义来调用不同的SetSysClockToxx()函数,比如SetSysClockTo71()就是当有宏定义SYSCLK_FREQ_72MHz时就调用该函数来具体配置时钟的。这些不同的SetSysClockToxx()调用是有优先级的,频率低的大于频率高的,而直接采用HSE具有最高优先级。
来看看这些宏定义和调用函数之间的关系:
首先是定义宏:
[mw_shl_code=c,true]#if defined (STM32F10X_LD_VL) || (defined STM32F10X_MD_VL) || (defined STM32F10X_HD_VL)
/* #define SYSCLK_FREQ_HSE    HSE_VALUE */
#define SYSCLK_FREQ_24MHz  24000000
#else
/* #define SYSCLK_FREQ_HSE    HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz  24000000 */
/* #define SYSCLK_FREQ_36MHz  36000000 */
/* #define SYSCLK_FREQ_48MHz  48000000 */
/* #define SYSCLK_FREQ_56MHz  56000000 */
#define SYSCLK_FREQ_72MHz  72000000
#endif
[/mw_shl_code]
看来需要根据项目需求来,不能同时使用多个宏,不用的宏需要注释掉。
然后根据定义的宏来声明所需的函数:
[mw_shl_code=c,true]#ifdef SYSCLK_FREQ_HSE
  static void SetSysClockToHSE(void);
#elif defined SYSCLK_FREQ_24MHz
  static void SetSysClockTo24(void);
#elif defined SYSCLK_FREQ_36MHz
  static void SetSysClockTo36(void);
#elif defined SYSCLK_FREQ_48MHz
  static void SetSysClockTo48(void);
#elif defined SYSCLK_FREQ_56MHz
  static void SetSysClockTo56(void);
#elif defined SYSCLK_FREQ_72MHz
  static void SetSysClockTo72(void);
#endif
[/mw_shl_code]
如果定义了多个宏会屏蔽掉低优先级的函数。在SetSysClock()里面再根据宏来调用用时钟配置函数:
[mw_shl_code=c,true]static void SetSysClock(void)
{
#ifdef SYSCLK_FREQ_HSE
  SetSysClockToHSE();
#elif defined SYSCLK_FREQ_24MHz
  SetSysClockTo24();
#elif defined SYSCLK_FREQ_36MHz
  SetSysClockTo36();
#elif defined SYSCLK_FREQ_48MHz
  SetSysClockTo48();
#elif defined SYSCLK_FREQ_56MHz
  SetSysClockTo56();
#elif defined SYSCLK_FREQ_72MHz
  SetSysClockTo72();
#endif

/* If none of the define above is enabled, the HSI is used as System clock
    source (default after reset) */
}
[/mw_shl_code]
如果没有宏定义,或者说让SystemInit()函数为空函数,则系统不会配置时钟而直接采用内部高速时钟HSI作为系统时钟使用。
system_stm32f10x.c里面其他的代码就是关于这些SetSysClockToxx()的实现。也就是如何使用寄存器来设置倍频、分频数等等,需要了解了寄存器后才能明白。查看这些函数,有个直观的感受是里面的代码是直接使用寄存器来设置数据的,不是使用库函数。通观整个过程,如果只是学习的话,根据硬件只需要把待设置成的频率宏去掉注释而把不需要的宏注释掉就可以了,编译时会自动加入需要的时钟设置函数的。



回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-8 18:02:11 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-9 23:08 编辑

配置好时钟后就开始执行用户程序了,在用户程序里面要使用外设首先必须让外设挂载的总线上的该设备进行时钟使能,让时钟驱动外设工作。
这是通过寄存器设置来实现的。
RCC一共有10个寄存器,互联型更多。启动时配置时钟当然要是通过这些寄存器来设置的,不过进入main后使能外设时钟就只用到了三个寄存器:RCC_AHBENR、RCC_APB2ENR、RCC_APB1ENR。寄存器同样是封装在结构体里面的:RCC_TypeDef,里面的寄存器名称去掉了RCC_前缀。既然是使用库函数,我们也就不必太关心这些寄存器封装是如何使用的了,只需关心库提供了哪些函数去操作这些寄存器实现所需的功能了。
RCC库函数在stm32f10x_rcc.h和.c中实现。
RCC地址宏定义,在stm32f10x.h中定义:
[mw_shl_code=c,true]#define PERIPH_BASE           ((uint32_t)0x40000000) /*!< Peripheral base address in the alias region */
#define AHBPERIPH_BASE        (PERIPH_BASE + 0x20000)
#define RCC_BASE              (AHBPERIPH_BASE + 0x1000)#define RCC                 ((RCC_TypeDef *) RCC_BASE)
#define RCC                 ((RCC_TypeDef *) RCC_BASE)

[/mw_shl_code]
RCC寄存器原来是在AHB总线地址空间上的,这个AHB总线有些不平常。AHB、APB1、APB2总线上外设使能的函数分别是:
[mw_shl_code=c,true]void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);[/mw_shl_code]
要使某个外设时钟使能,其实就是使RCC_AHBENR、RCC_APB2ENR、RCC_APB1ENR三个寄存器中对应位置1,那么这是如何在函数中实现的呢?首先是定义了这三个寄存器各个位代表的外设的宏,也就是当置1时寄存器的值。以APB2ENR寄存器各个位代表的外设为例说明:
[mw_shl_code=c,true]#define RCC_APB2Periph_AFIO              ((uint32_t)0x00000001)
#define RCC_APB2Periph_GPIOA             ((uint32_t)0x00000004)
#define RCC_APB2Periph_GPIOB             ((uint32_t)0x00000008)
#define RCC_APB2Periph_GPIOC             ((uint32_t)0x00000010)
#define RCC_APB2Periph_GPIOD             ((uint32_t)0x00000020)
#define RCC_APB2Periph_GPIOE             ((uint32_t)0x00000040)
#define RCC_APB2Periph_GPIOF             ((uint32_t)0x00000080)
#define RCC_APB2Periph_GPIOG             ((uint32_t)0x00000100)
#define RCC_APB2Periph_ADC1              ((uint32_t)0x00000200)
#define RCC_APB2Periph_ADC2              ((uint32_t)0x00000400)
#define RCC_APB2Periph_TIM1              ((uint32_t)0x00000800)
#define RCC_APB2Periph_SPI1              ((uint32_t)0x00001000)
#define RCC_APB2Periph_TIM8              ((uint32_t)0x00002000)
#define RCC_APB2Periph_USART1            ((uint32_t)0x00004000)
#define RCC_APB2Periph_ADC3              ((uint32_t)0x00008000)
#define RCC_APB2Periph_TIM15             ((uint32_t)0x00010000)
#define RCC_APB2Periph_TIM16             ((uint32_t)0x00020000)
#define RCC_APB2Periph_TIM17             ((uint32_t)0x00040000)
#define RCC_APB2Periph_TIM9              ((uint32_t)0x00080000)
#define RCC_APB2Periph_TIM10             ((uint32_t)0x00100000)
#define RCC_APB2Periph_TIM11             ((uint32_t)0x00200000)
[/mw_shl_code]均是当外设位为1而其他位为0时寄存器的值。stm32中文参考手册V10.0中关于APB2ENR寄存器只有低16位有定义,高16位保留,可是,库函数里面却有高16位的几个定义,这是为什么?
而如果需要使能就使APB2ENR寄存器“或”上该值,关闭则是“与”上该值的取反值。
[mw_shl_code=c,true]void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
{
  /* Check the parameters */
  assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
  assert_param(IS_FUNCTIONAL_STATE(NewState));
  if (NewState != DISABLE)
  {
    RCC->APB2ENR |= RCC_APB2Periph;
  }
  else
  {
    RCC->APB2ENR &= ~RCC_APB2Periph;
  }
}
[/mw_shl_code]
------------------------------------------------
断言assert_param():
看似是一个函数,其实是宏,在stm32f10x_conf.h中定义:
[mw_shl_code=c,true]#ifdef  USE_FULL_ASSERT
  #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

  void assert_failed(uint8_t* file, uint32_t line);
#else
  #define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */
[/mw_shl_code]
断言用于参数检查,如果说传入的参数符合要求那么宏值为(void)0,否则执行assert_failed()函数,这个函数没有实现。如果需要可以实现assert_failed()函数,指出哪个文件的哪行出了错误。
断言一般用在开发阶段,必须定义USE_FULL_ASSERT宏,否则只是得到(void)0,对于C来说就相当于空语句。开发调试结束取消USE_FULL_ASSERT宏。这里需要注意:宏定义不仅仅用于预编译阶段,它甚至可以用于运行阶段。比如在运行阶段才能决定断言里面的值倒地是true还是false,从而决定断言值是(void)0还是执行assert_failed()函数。断言和错误捕捉显然不同。
---------------------------------
来看看都做了什么检查: assert_param(IS_FUNCTIONAL_STATE(NewState));
[mw_shl_code=c,true]#define IS_FUNCTIONAL_STATE(STATE) (((STATE) == DISABLE) || ((STATE) == ENABLE))
[/mw_shl_code]很清楚了,如果说参数STATE为DISABLE或ENABLE则宏值为true,否则为false。
那么 assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph))呢?[mw_shl_code=c,true]#define IS_RCC_APB2_PERIPH(PERIPH) ((((PERIPH) & 0xFFC00002) == 0x00) && ((PERIPH) != 0x00))
[/mw_shl_code]
这个比较诡异!!
---------------------------------------
总结,void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);中的RCC_APB2Periph参数必须是预定义的宏才能保证是有效值。
比如:RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
如果需要同时使能多个同总线上的外设就采用“或”.
如何记忆这个函数:RCC_APB2PeriphClockCmd就是“关于RCC时钟的”挂载在APB2总线上的外设时钟使能函数,参数开头也必须是RCC_APB2Periph_开头。






回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 10:02:47 | 显示全部楼层
C语言中的#define.
宏定义的用法有两种,一种是宏常量,比如定义各种寄存器地址,就是用一个比较好记忆的符号来代替一个难以记忆的数字。
另一种是宏替换,这种说法可能不太听说,我们通常不加区分地说“宏”。比如断言assert_param()就属于宏替换。
宏替换的作用和函数很像,不过宏替换不像函数那样需要额外的内存开销。
所谓替换就是用一个字符串替代宏名,这是在预编译阶段发生的。
记住:不是用宏值替换,宏值是发生在运行阶段。
既然是字符串替换,那么就可以在替换的字符串里面有普通函数,就像assert_param()替换里面的assert_failed()函数一样。
所以,assert_param()是宏替换而assert_failed()是普通函数。
所以,assert_param检测到非法参数输入时如果assert_failed()没有其他处理程序是不会终止的。
--------------------------------
#defined相关的预处理还有#if/#ifndef/#else/#endif等等。




回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 10:49:33 | 显示全部楼层
C语言中的bool。
很久没有使用C了,当接触到C中居然没有bool类型时我感到很愕然。
其实也好理解,C是偏重于底层硬件的语言,内存中一次读写的最小单位是一个字节,所以就没有bit这种所谓的bool型了。
其实其他语言中的bool也是对一个内存单元中数据的解释,就像int、float一样。
C中的true或false 有两种情况,一种是逻辑判断,比如iTemp == 0xff,结果是个逻辑值,要么是true要么是false。一种是用数值来代替逻辑值,这就比较特殊了。
C中规定,非零值都是true 而零值是false,这里又引入了什么是零值,暂且不讨论。
我们可以简单看作0是false而大于0是true ,这个有时是行得通的。当然我们自己要明白这只是C中逻辑值的一种。
具体项目,可以用宏定义true和false。
#define TRUE 1
#define FALSE 0
回复 支持 1 反对 0

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-10 11:10:48 | 显示全部楼层
楼主写得真好,
照这个思路,
完全可以出书了.
不过,
我强烈怀疑这不是楼主的学习笔记,
应该是对STM32有相当的了解,
然后再来整理笔记.
不过无论如何,
这是一篇非常好的东西.
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 18:35:16 | 显示全部楼层
现在该学习GPIO了,其实早就应该先学习这部分的内容了,不过我认为有些知识需要准备才迟迟没有写笔记。
首先自然是硬件内容了,也是我最薄弱的环节。
STM32F103ZET芯片共计有7组GPIO,每组16个引脚。是不是每款芯片都是成组(16Pin)提供呢?不知道,记得51的IO引脚似乎有些不是。
这些GPIO引脚都是作为IO用吗?可以作为其他用途,也就是复用,就像键盘上的按键有复用功能一样。
比如可以作串口使用、可以接触摸屏等等。
还是先来看GPIO。
每个引脚都可以有输入功能也可以做输出用,要么IN要么OUT,这个看怎么配置了。
Input功能有4种:模拟输入、浮空输入、上拉输入、下拉输入。
模拟输入暂时不讨论。浮空输入就是引脚内部不接任何器件直接接外部电路,但是直接接按钮吗?不是,需要电压的,所以一般外部电路需要设计有上拉或下拉电阻。
上拉和下拉输入可以和浮空输入合起来学习,因为在浮空输入外接上下拉电阻时起到的作用和设置成上下拉输入时类似。
上拉时通过电阻接VDD,VDD就是外部电路的电源+,这个根据不同的场合有不同的电压。可以这么认为,上拉就是为了配合外部电路的VDD设计的。
设置成上拉时,当没有信号时,也就是外部浮空既不1也不0时,端口通过上拉电阻接到VDD上使端口为1,这个就是端口默认电平。换句话说端口不能检测到1信号。
那端口能够检测到哪个信号呢?0信号。当端口出现了低电平时可以被检测到。
好奇特的端口,不是可以检测1和0吗?事实上PLC也是这么的。
同理,下拉输入时端口通过下拉电阻接到VSS上去,默认是0,当端口出现1时被检测到。
总结下:上拉用于检测0信号而下拉用于检测1信号。
如果设置成上拉输入或下拉输入(不同于浮空通过外部上下拉电阻),这个上拉下拉也叫“弱上拉或弱下拉”。怎么个弱法?可能是电阻阻值吧。
总之,GPIO的输入还是比较简单的。
这里需要了解下什么是1电平什么是0电平,什么是TTL电平什么是CMOS电平。
----------------------------------------------------
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 18:53:19 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-11 10:03 编辑

GPIO的输出有四种,不过大体有两种:推挽输出和开漏输出,再此基础上又有两种:原生和复用。即推挽、开漏、复用推挽和复用开漏输出。
具体需要看以后的学习。
输出配置时端口其实内部接有两个MOS管,N-MOS和P-MOS,N-MOS在基极(不知道是否这么叫)为低电平时导通而P-MOS在基极为高电平时导通。
来看开漏输出。
配置成开漏输出时只有接到VSS的N-MOS管起作用,P-MOS管没有作用。
当端口输出1时由于N-MOS管截至这样端口既不0也不1,呈现高组态。有用么?这个需要外接上拉电阻让端口输出1才有用。也就是输出1时才能正常输出1。也是为了电平匹配。
当端口输出0时N-MOS管导通接到VSS也就是低电平,这样端口输出0。
配置成推挽输出时两个MOS管均起作用。
输出1时P管导通接VDD而输出0时N管像开漏一样导通接VSS。这样无论如何都可以在端口上得到1和0.
而且推挽不需要外接上下拉电阻。所以还是推挽比较好理解。
--------------------------------------
GPIO每组有7个寄存器,CRL和CRH用于端口配置,IDR是端口输入值寄存器,ODR是输出数据寄存器,这个都比较好理解。
BSRR是用于端口置1置0的寄存器,而BRR只用于清除置0。有就是说BRR做了BSRR高16位的事情。使用中可以把BSRR低16位看作置1寄存器而BRR低16位看作置0寄存器,舍弃BSRR高16位置0的功能,这样就只需要对低16位进行操作了,比较统一。
LCKR是端口配置锁定寄存器,用于锁住端口配置信息,具体怎么用不清楚。


回复 支持 反对

使用道具 举报

10

主题

67

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
430
金钱
430
注册时间
2017-5-16
在线时间
64 小时
发表于 2018-6-10 19:35:16 | 显示全部楼层
好评!又长知识了
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 20:39:17 | 显示全部楼层
C对GPIO的封装。
每组GPIO都要7个寄存器,用结构进行封装:
[mw_shl_code=c,true]typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
[/mw_shl_code]
7组GPIO宏定义:
[mw_shl_code=c,true]#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
[/mw_shl_code]
这样可以直接用GPIOA~GPIOG来引用这7组GPIO。
-------------------------------------
7个寄存器的功能是关于引脚、输出速度、和模式的配置,使用结构体来定义这三个功能:
[mw_shl_code=c,true]typedef struct
{
  uint16_t GPIO_Pin;             /*!< Specifies the GPIO pins to be configured.
                                      This parameter can be any value of @ref GPIO_pins_define */

  GPIOSpeed_TypeDef GPIO_Speed;  /*!< Specifies the speed for the selected pins.
                                      This parameter can be a value of @ref GPIOSpeed_TypeDef */

  GPIOMode_TypeDef GPIO_Mode;    /*!< Specifies the operating mode for the selected pins.
                                      This parameter can be a value of @ref GPIOMode_TypeDef */
}GPIO_InitTypeDef;
[/mw_shl_code]
其中,输出速度是枚举类型:
[mw_shl_code=c,true]typedef enum
{
  GPIO_Speed_10MHz = 1,
  GPIO_Speed_2MHz,
  GPIO_Speed_50MHz
}GPIOSpeed_TypeDef;
[/mw_shl_code]
而模式也是枚举类型:
[mw_shl_code=c,true]typedef enum{ GPIO_Mode_AIN = 0x0,
  GPIO_Mode_IN_FLOATING = 0x04,
  GPIO_Mode_IPD = 0x28,
  GPIO_Mode_IPU = 0x48,
  GPIO_Mode_Out_OD = 0x14,
  GPIO_Mode_Out_PP = 0x10,
  GPIO_Mode_AF_OD = 0x1C,
  GPIO_Mode_AF_PP = 0x18
}GPIOMode_TypeDef;
[/mw_shl_code]
注意,枚举类型是整型,数值从开始第一个枚举值+1,所以可以从手册得知GPIO_Speed_2MHz的值是二进制10也就是2,而50MHz的值是二进制11就是3。模式没有类似的以此类推的值,分别指出模式值。
不过,模式值似乎和寄存器中的定义有点不一样,通过分析得知输入输出还和低两位本来是速度的配置位第0位有关,所以可能会在程序中进行分解。
----------------------------------------
值得注意的还有关于上拉下拉输入的配置数据在寄存器里面都是10,模式枚举里面是不同的值,看来是在程序里面有区别的。
查阅参考手册和百度,手册上有关于端口位配置表在106页,上拉还是下拉输入通过PxODR寄存器来区分,当ODR相应位配置为0时代表输入是下拉,当ODR相应位配置为1是代表输入是上拉。
什么意思?ODR是输出数据寄存器,是直接的输出寄存器,把端口配置成输入还要配置ODR?确实如此。
可能是一种妥协,一种硬件妥协,既然输入用不到输出而输入又需要区分上下拉,就用ODR来嘛。这只是猜想,其实是芯片硬件设计使然,只要理解就可以了。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-10 21:11:54 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-10 21:35 编辑

端口引脚GPIO_Pin在GPIO_InitTypeDef这个初始化数据结构体中被定义为uint16_t,不过引脚也是有宏定义的:
[mw_shl_code=c,true]#define GPIO_Pin_0                 ((uint16_t)0x0001)  /*!< Pin 0 selected */
#define GPIO_Pin_1                 ((uint16_t)0x0002)  /*!< Pin 1 selected */
#define GPIO_Pin_2                 ((uint16_t)0x0004)  /*!< Pin 2 selected */
#define GPIO_Pin_3                 ((uint16_t)0x0008)  /*!< Pin 3 selected */
#define GPIO_Pin_4                 ((uint16_t)0x0010)  /*!< Pin 4 selected */
#define GPIO_Pin_5                 ((uint16_t)0x0020)  /*!< Pin 5 selected */
#define GPIO_Pin_6                 ((uint16_t)0x0040)  /*!< Pin 6 selected */
#define GPIO_Pin_7                 ((uint16_t)0x0080)  /*!< Pin 7 selected */
#define GPIO_Pin_8                 ((uint16_t)0x0100)  /*!< Pin 8 selected */
#define GPIO_Pin_9                 ((uint16_t)0x0200)  /*!< Pin 9 selected */
#define GPIO_Pin_10                ((uint16_t)0x0400)  /*!< Pin 10 selected */
#define GPIO_Pin_11                ((uint16_t)0x0800)  /*!< Pin 11 selected */
#define GPIO_Pin_12                ((uint16_t)0x1000)  /*!< Pin 12 selected */
#define GPIO_Pin_13                ((uint16_t)0x2000)  /*!< Pin 13 selected */
#define GPIO_Pin_14                ((uint16_t)0x4000)  /*!< Pin 14 selected */
#define GPIO_Pin_15                ((uint16_t)0x8000)  /*!< Pin 15 selected */
#define GPIO_Pin_All               ((uint16_t)0xFFFF)  /*!< All pins selected */
[/mw_shl_code]
这些宏的宏值对应于IDR和ODR寄存器中引脚的地址值,也对应于BSRR的第16位关于引脚的地址值和BRR低16位引脚地址值。但不对应于CRL和CRH中引脚的地址值,操作函数必须进行相应的处理才行。函数参数对于引脚数值合法性检测怎么做呢?
[mw_shl_code=c,true]#define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != (uint16_t)0x00))
[/mw_shl_code]
其实就是不作检查。因为可以一次设置多个引脚,值从0x0001到0xffff都可以。(GPIO_Pinx可以看作引脚地址,只要相应的操作函数中实现了一次调用配置多个引脚的功能就可以,事实上库函数关于GPIO的配置也的确是如此的。参考GPIO_Init()函数)
--------------------------------------
怎样对端口进行配置呢?
首先是使能端口时钟,这个用到了前面的知识。
[mw_shl_code=c,true]
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE);         //使能PB,PE端口时钟
[/mw_shl_code]
然后对初始化结构体赋值:
[mw_shl_code=c,true] GPIO_InitTypeDef  GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//引脚5
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
[/mw_shl_code]
然后调用初始化函数对相应端口进行配置:
[mw_shl_code=c,true] GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB.5
[/mw_shl_code]
如果仅仅是组别和引脚不同就接着初始化:

[mw_shl_code=c,true] GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//引脚5
GPIO_Init(GPIOE, &GPIO_InitStructure);//推挽输出 ,GPIOE.5口速度为50MHz[/mw_shl_code]
如此,完成了GPIO相应端口配置。
可以进行端口读写了。

回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-11 10:08:47 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-12 22:40 编辑

有了这些知识再查看原理图就可以设计自己的板载资源库了,现在可以实现LED和按键和蜂鸣器资源。------------
6月12日。
今天心情不好。不过还是写点吧。

完成了板载key、led、beep的资源设计程序,有所感悟。
还是先写点关于头文件的东西吧。
如果一个文件一个文件地include,难免会出现重复包含的,这样就导致重定义的错误,如何解决?
#ifndef  __KEY_H
#definde __KEY_H
.....头文件实际内容。
....
#endif
查看库函数,几乎都是这样的头文件。
----------------------
头文件中不要include 过多其他文件,都不知道要用哪个,其实就在主文件中#include "stm32f10x.h"和其他自己的头文件,在自己设计的头文件中也只包含这个头文件(指的是库中的头文件,不算自己设计的。)
其实自己设计的头文件中都可以不包含stm32f10x.h,不过单独编译时会提示找不到相关符号,如果统一编译没问题。
回复 支持 反对

使用道具 举报

6

主题

153

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1323
金钱
1323
注册时间
2016-12-27
在线时间
156 小时
发表于 2018-6-11 16:12:20 | 显示全部楼层
谢谢分享,已经收藏,作为温故而知新
回复 支持 反对

使用道具 举报

8

主题

206

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1010
金钱
1010
注册时间
2016-4-6
在线时间
234 小时
发表于 2018-6-11 18:30:05 | 显示全部楼层
很好的学习笔记,已收藏
回复 支持 反对

使用道具 举报

0

主题

6

帖子

0

精华

新手上路

积分
46
金钱
46
注册时间
2018-2-28
在线时间
10 小时
发表于 2018-6-13 16:42:00 | 显示全部楼层
谢谢分享
回复 支持 反对

使用道具 举报

32

主题

138

帖子

0

精华

金牌会员

Rank: 6Rank: 6

积分
1755
金钱
1755
注册时间
2018-4-28
在线时间
241 小时
发表于 2018-6-19 09:39:22 | 显示全部楼层
有没有做 多功能电度表,我想买个原码可测量三相电流、电压、有功功率、无功功率QQ422866299
回复 支持 反对

使用道具 举报

6

主题

126

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
207
金钱
207
注册时间
2018-4-18
在线时间
10 小时
发表于 2018-6-19 16:28:34 | 显示全部楼层
支持支持
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-21 14:50:24 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-21 16:14 编辑

配置好SysTick的时钟源后来看看SysTick是如何工作的。
在CTRL寄存器第0位置1使能后SysTick开始工作。
从RELOAD寄存器转载计数值到VAL寄存器中,这是个24bit寄存器。每个时钟周期VAL值减一,当VAL值减到0时定时结束。
如果配置了CTRL的第1位为1则此时产生中断,如果没有配置则下个周期又会重转载RELOAD值到VAL中来继续。
周而复始,连绵不绝。
需要知道几个问题:
1、每周期是如何确定的?
2、RELOAD值是如何确定的?
3、如果中断,如何避免中断执行时间不会干扰定时时间?
------------------------
连看了两遍视频才看明白。执行延时其实有两种方法,一种是中断一种是查询。
中断看来是官方推荐的方法,因为在cor_cm3.h中有SysTick_Config()函数,该函数的功能是设置SysTick时钟源、设置中断间隔的时钟数、设置中断、使能SysTick。其后在中断函数SysTick_Handler()中执行中断。我注意到SysTick_Config设置RELOAD值时需要把需要定时的脉冲数-1,这是因为装载初值也是需要时间的,至于-1到底精确吗就是相对的了,这要看装载执行时间(装载是系统自动完成的,可以忽略时间,不过-1还是比较精确的。)
如果采用查询,就像原子官方提供的程序delay_ms()一样,在没有定时到时一直查询,直至定时到才能执行后面的程序。
采用中断似乎要耗费宝贵的中断资源,而采用查询又似乎全面等待。我还是认可采用中断。
中断还没有学呢。
如果系统中需要很多不同的定时器如何处理呢?这个比较容易,就像PLC中的一样,如果精度要求不高就固定一个单位SysTick,比如1ms或1us,这个已经比较高精度了。此后如果需要1s定时就在中断里面累积1000个1ms或1000*1000个1us。还是1ms吧,根据实际需求来。
如果需要另一个12s定时呢?不用在中断里面再另外累积12000个1ms了,直接采用刚才的1s定时再累积12次吧。记得在完成1s定时并又累积12s定时变量后初始化1s变量即可。以此类推其他定时器。
---------------------
如果系统不需要如此高频率的定时呢?比如我只需要1h定时,是否可以减少中断次数呢?这要看定时累积的时钟数量是否突破24bit的unsigned RELOAD值,如果没有突破自然直接设置RELOAD一个大的值减少中断次数,如果已经超过了还是需要二次累积的,这要增加中断次数。

回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-21 17:14:06 | 显示全部楼层
涉及到时钟自然逃不过还要复习下,不过似乎没有找到太多的有关SysTick在时钟一章的介绍。
SysClock经过AHB预分频后成为AHB总线时钟,外设大多由此引入时钟。如果AHB预分频为1,则AHB时钟等同于SysClock时钟。
在这里,SysTick不是外设但它的时钟源也由引入。可以是AHB的8分频也可以是AHB,其实是叫FCLK。
精英版的SysTick配置为HSE,即外部高速时钟,是72MHz。这个在进入main前就已经配好了。
所以SysTick时钟=AHB=SysClock=72MHz  或  SysTick时钟=AHB/8=SysClock/8=72MHz/8=9MHz.
精英提供的delay_Init()函数配置SysTick是用SysTick_CLKSource_HCLK_Div8,也即SysTikc时钟源配置为AHB的8分频,这样SysTick时钟频率为9MHz.
一个周期为:1/9MHz=1.111E-7s。换句话说,可能的误差是1.111...E-7s,大概0.1us.
如果是72MHz则更小。
显然不可能定时几个时钟周期,这也太小了,如果采用中断就不要做其他事情了。当然,STM32提供这样的功能。
来看看如果需要定时1ms需要经过多少时钟周期?十字相乘法。
1000ms--------------9E6 Hz (9MHz)
1     ms--------------9E6/1000 =9000次。
如果是AHB则72E6/1000 = 72000次。
---------------------------------------------------------
如果不考虑装载时间,在大时间尺度上会造成误差,所以还是-1次吧。




回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-21 17:23:25 | 显示全部楼层
我试着用中断做了自己的定时器,当然没有学到中断,暂时使用系统库函数SysTick_Config(),直接一次性设置了SysTick时钟源、装载值、中断、使能。
单位定时时间为1ms,由于系统提供的时钟源宏为SysTick_CTRL_CLKSOURCE_Msk 为CTRL第2位置1,是AHB 72MHz,所以装载值为72000UL。
回复 支持 反对

使用道具 举报

39

主题

212

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
303
金钱
303
注册时间
2016-4-21
在线时间
129 小时
发表于 2018-6-21 17:46:06 | 显示全部楼层
666666666
回复 支持 反对

使用道具 举报

39

主题

212

帖子

0

精华

中级会员

Rank: 3Rank: 3

积分
303
金钱
303
注册时间
2016-4-21
在线时间
129 小时
发表于 2018-6-21 17:50:44 | 显示全部楼层
难得啊。楼主
本人交流群:136045527,欢迎各位大佬和萌新
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-21 22:52:13 | 显示全部楼层
哦,我明白了,之所以采用查询是因为这是delay,延时啊。
如果是定时,当然采用中断比较好。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-21 23:44:50 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-21 23:46 编辑
warship 发表于 2018-6-21 23:17
个人认为:
基本上所有定时器装载初值的时候都是要-1的,
这并不是因为装载需要时间,

嗯,有道理,就像for从0开始总是<i才能正确计数一样。不过装载仍然会消耗系统周期的。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-21 23:48:36 | 显示全部楼层
基本上调试成功。
不过,延时还是感觉不爽,老是有操作不顺滑的感觉。
教程里面的按键防止抖动的逻辑也有问题,有时间重新设计。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-23 19:40:59 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-23 20:01 编辑

谢谢 warship 的指点!
今天得空分析整理了这段时间对key/SysTick的学习,来个综合训练。我把SysTick做成系统共享的定时器脉冲生成器,用来产生1ms脉冲信号,需要用到定时器时使用计数方式累积1ms脉冲直至达到预置时间脉冲数时定时到达。
这种定时方式的误差在1ms之内,正负可以达到0.5ms。我认为在大多数工业场合已经比较精确了,可以用于大多数普通应用。如果需要高精度定时当然要使用系统提供的其他类型的定时中断。
首先定义SysTick的装载值RELOAD:
[mw_shl_code=c,true]// 500us装载值。
// 500us装载值是1ms装载值的一半。
// 72000000/2000=36000.
#define RELOAD_500US    SystemCoreClock/2000U
[/mw_shl_code]
然后是设置SysTick:
[mw_shl_code=c,true]// 如果采用1ms脉冲方式,则以500us设置SysTick.
void delay_Init(void)
{
    // SysTick配置,等待完成。
    if (SysTick_Config(RELOAD_500US))
    {
        while(1);
    }
}
[/mw_shl_code]
定义配合SysTick使用的定时计数结构,其中bPlus_ms是1ms脉冲信号,而uTimer_ms是我另一种定时方式使用的变量,这里无用。
[mw_shl_code=c,true]// 延时器,配合SysTick使用。
static struct {
    u32 uTimer_ms;  // ms延时器,用于查询延时。
    bool bPlus_ms;  // ms脉冲信号,0.5msON,0.5msOFF.
}Timer;
[/mw_shl_code]
设计SysTick中断服务函数,这个函数在库文件中提供了一个空函数,可以直接在库文件中修改,我采用注释掉库函数然后在delay.c中定义。
[mw_shl_code=c,true]// SysTick定时器中断
void SysTick_Handler(void)
{
    // 500us定时中断到,使Timer.bPlus_ms翻转一次。
    Timer.bPlus_ms = !Timer.bPlus_ms;
}
[/mw_shl_code]这样,当初始化SysTick后Timer.bPlus_ms会自动生成1ms等宽脉冲信号。
-------------------------------------------------
接着使用这个脉冲来定时:
定义计数型定时器使用到的数据结构类型,用来定义static的定时器。
[mw_shl_code=c,true]// 脉冲型定时器结构
typedef struct {
    u32  uEt; // 定时计数当前值。
    bool bTemp; //脉冲暂存信号。
    bool bQ;    //定时时间到标识。
}TimerType;
[/mw_shl_code]
之后可以用来声明定时器,比如定义两个定时器:
[mw_shl_code=c,true]// 定时两个定时器。
static TimerType Timer1, Timer2;
[/mw_shl_code]
设计操作定时器的函数,比如需要延时ON定时器:
[mw_shl_code=c,true]// 延时接通定时器
// 当bEnb为TRUE时开始定时,定时单位为1ms。
// 当bEnb为FALSE时复位定时器。
// 当定时到达后如果没有复位定时器则定时器当前计数值uEt保持不变。
bool timeON(bool bEnb, u32 uPt, TimerType *timer)
{
    if(!bEnb){
        timer->uEt = 0U;
        timer->bTemp = FALSE;
        timer->bQ = FALSE;
        return FALSE;
    }
    else{
        if((timer->uEt < uPt) && (Timer.bPlus_ms) && (!timer->bTemp))
            timer->uEt = timer->uEt + 1;
        timer->bTemp = Timer.bPlus_ms;

        if((timer->uEt >= uPt)){
            timer->bQ = TRUE;
            return TRUE;
        }
        else{
            timer->bQ = FALSE;
            return FALSE;
        }
    }
}[/mw_shl_code]比如使定时器Timer1定时15s:
[mw_shl_code=c,true]// 定义定时器。
static TimerType Timer1;
bool bT1_Enb;
....
timeON(bT1_Enb, 15000U, &Timer1);
//通过控制bT1_Enb来使Timer1开始定时及复位。[/mw_shl_code]注意,定时值不超过32位无符号整数范围,也即4294967s又295ms。





回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-23 20:06:19 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-23 20:11 编辑

示例:
使用timeON延时闭合定时器实现LED0的500ms亮1000ms灭,如此循环。
[mw_shl_code=c,true]/*********************************************************
  功能: 内核SysTick试验
  描述: 精英版STM32的SysTick定时器试验。
  设计: azjiao
  版本: 0.1
  日期: 2018年06月21日
*********************************************************/

/* Includes ------------------------------------------------------------------*/
#include <stdio.h>
#include "stm32f10x.h"
#include "template.h"
#include "key.h"
#include "led.h"
#include "beep.h"
#include "delay.h"
void assert_failed(uint8_t* file, uint32_t line)
{
    //printf("Param Error! in file name: xxx, in line %d\n",line);
    //while(1);
}

// 定义两个定时器。
static TimerType Timer1, Timer2;

int main(void)
{
    //bool bSwitch = TRUE;
    bool bT1_Enb = FALSE, bT2_Enb = FALSE;  //定时器使能。
    bool bLED0Round = FALSE; // LED0 定时亮灭开关。

    //板载Key初始化。
    key_Init();

    //板载Led初始化。
    led_Init();

    //板载beep初始化。
    beep_Init();

    // SysTick初始化,开始定时。
    delay_Init();

    /* Infinite loop */
    while (1)
    {
        bool bIsKey0;
        bool bIsKey1;
        bool bIsWKUP;

        bIsKey0 = key0_Scan(TRUE);
        bIsKey1 = key1_Scan(TRUE);
        bIsWKUP = WKUP_Scan(FALSE);
        if(bIsWKUP)
            bLED0Round = !bLED0Round;

        if (bLED0Round){
            LED1_ON;
        }
        else
            LED1_OFF;

        //BEEP发声、禁声。
        if( bIsKey0 || bIsKey1 )
        {
            BEEP_ON;
        }
        else
        {
            BEEP_OFF;
        }

        timeON(bT1_Enb, 500U, &Timer1);
        timeON(bT2_Enb, 1000U, &Timer2);

        if(bLED0Round){
            if(!Timer1.bQ)
            {
                bT1_Enb = TRUE; //Timer1开始定时。
                LED0_ON;
            }
            if(Timer1.bQ && !Timer2.bQ){
                bT2_Enb = TRUE;
                LED0_OFF;
            }
            // 当Timer2定时到时复位两个定时器。
            if(Timer2.bQ){
                bT1_Enb = FALSE;
                bT2_Enb = FALSE;
            }
        }
        else {
            bT1_Enb = FALSE;
            bT2_Enb = FALSE;
            LED0_OFF;
        }
    }
}
[/mw_shl_code]
这段程序定时器单独运行,不影响其他操作,不过key没有做防抖动处理,感觉需要优化。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-23 20:58:55 | 显示全部楼层
按键防抖动处理:
我在教程基础上进行了一些优化,key1/key2/wkup可以独立同时操作,互不影响,而且防抖动更合理了。以WKUP处理为例(三个按键独立检测):
[mw_shl_code=c,true]//WKUP 扫描程序
//bSusKey==TRUE:连续按下可以扫描到连续多个值。
//bSusKey==FALSE:连续按下只扫描到一个按键值。
bool WKUP_Scan( bool bSusKey )
{
        //按键上升沿检测暂存信号。
    static bool bsKey_uPTmp = FALSE;
    //防抖动检测定时器。
    static TimerType timer;
    //经过防抖处理后检测到按键压下。
    bool bKeyPress;
    //返回值。
    bool bRet = FALSE;

        assert_param(Is_BOOL(bSusKey));

    // 防抖处理。
    bKeyPress = timeON(WKUP_CODE, 150U, &timer);

    if(bKeyPress) //如果按键按下。
    {
        if( !bsKey_uPTmp ) //检测到是上升沿
        {
            bRet = TRUE;
            if ( !bSusKey )  //按键状态暂存.
                bsKey_uPTmp = TRUE;
        }
    }
    else  //如果按键没有按下
        {
        bsKey_uPTmp = FALSE;
        }

    return bRet;
}
[/mw_shl_code]
防抖动的代码是:[mw_shl_code=c,true]// 防抖处理。
bKeyPress = timeON(WKUP_CODE, 150U, &timer);[/mw_shl_code]
当WKUP_CODE按下时返回键值,键值非0。如果没有按下则返回0。
如果按键持续按下超过150ms则认为按键有效,如果在150ms内发生按键释放则认为按键抖动,按下无效。
经过实测,150ms除了可以防止抖动外还能让按下比较顺滑。如果防抖定时比较长则按键需要按下较长时间,体验不好。

回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-24 11:46:00 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-26 09:37 编辑

学习中遇到了各种C语言的一些语法需要学习,很长时间没有使用C语言了,这些是遇到的坑。
1、keil C居然不支持在for中声明循环变量:
for(int i = 0; i < 100; i++){}
必须先声明i:
int i;
for(i = 0; i<100; i++){}
2、如果中途局部块只有一句,则不能用使用;结尾。
if(...)
   i++ --->写成i++;就错误了。
else
   data=...;
或者可以用{}包围,如果只有一句则显得比较花。
-------------------------------------
后续:
第2个问题其实不存在,是我的错误导致的。原因是这条单独的语句是宏,我在宏定义中错误地添加了一个结束符';'。
回复 支持 反对

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-24 12:18:08 | 显示全部楼层
xiatianyun 发表于 2018-6-24 11:46
学习中遇到了各种C语言的一些语法需要学习,很长时间没有使用C语言了,这些是遇到的坑。
1、keil C居然不 ...

你说的第二种情况,
好像不存在哦,
if(...) i++; 没有问题
另外,如果只有一句,完全不需要{}的呀。
难道不同的KEIL版本有这么大的差异么?
   
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-24 15:43:23 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-24 15:46 编辑
warship 发表于 2018-6-24 12:18
你说的第二种情况,
好像不存在哦,
if(...) i++; 没有问题

我说的是if...else...中if后如果只有一句时的情况。[mw_shl_code=c,true] if (bLED0Round)
            LED1_ON;
        
        else
            LED1_OFF;[/mw_shl_code]
..\User\SysTick_Test.c(60): error:  #127: expected a statement
改为如下没问题了:[mw_shl_code=c,true]if (bLED0Round)
            LED1_ON
        
        else
            LED1_OFF;[/mw_shl_code]
比较老的C版本确实如此。
回复 支持 反对

使用道具 举报

31

主题

1951

帖子

3

精华

论坛元老

Rank: 8Rank: 8

积分
4418
金钱
4418
注册时间
2018-5-11
在线时间
923 小时
发表于 2018-6-24 16:17:30 | 显示全部楼层
xiatianyun 发表于 2018-6-24 15:43
我说的是if...else...中if后如果只有一句时的情况。[mw_shl_code=c,true] if (bLED0Round)
             ...

我也刚刚试了一下:
        if(type==1)
                type=0;
        else
                type=1;
编译没有任何问题,难道有配置选项可以设置,我们的编译环境不同?
我的开源链接 https://github.com/ShuifaHe/STM32.git  请关注,点赞支持哦。
回复 支持 反对

使用道具 举报

29

主题

338

帖子

1

精华

金牌会员

Rank: 6Rank: 6

积分
1181
金钱
1181
注册时间
2018-4-13
在线时间
170 小时
 楼主| 发表于 2018-6-24 16:26:52 | 显示全部楼层
本帖最后由 xiatianyun 于 2018-6-24 16:40 编辑

刚才试了下,for循环中如果需要类似for(int i=0; i < 100; i++)的定义循环变量i,需要C99支持,只需勾选c/c++选项页中的 c99 mode即可。
-------------------------
C99还允许在使用变量前在程序中的任何地方声明变量,如果不选C99 mode,则需要在第一条执行语句前面就声明。这个也是我遇到的第一个坑。

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2024-11-24 06:56

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表