学习精英版的学习笔记

2019-07-21 02:25发布

本帖最后由 xiatianyun 于 2018-6-4 09:42 编辑

刚看到有前人发布自己学习中的持续更新贴作为鞭策自己不断学习的方法,我也做个学习笔记,把过程中的点点滴滴写下来。
其实写笔记我一直使用复制粘贴保存在电脑里,但最近我喜欢上了用笔写在笔记本上,觉得更有价值更有效率。
--------------------------
先来谈谈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在目录下的ARMARMCCin里就是编译工具,其他 目录里面提供单片机的include、lib。
为什么还要从ST官网下载库呢?官网的比较新嘛。Keil提供的库我想也是ST提供的,不过可能不会同步更新。
Keil的编辑器不好用,不过keil提供从其他编辑器来操作的可定制菜单,看来Keil还是知道自己的短板的。
现在比较不错的编辑器有gvim/sublime text/vs code,不过这些都是普通编辑器,如果要在开发中实现跳转、自动完成的话需要插件,这些都是学习成本,就看值不值了。
补充:
其实开发STM32除了在windows平台外还有Linux平台和Mac平台也可以,这对于极客来说是个好消息。具体看ST公司提供的开发套件,从我了解的信息看是集成在Eclipse的开发环境。
----------------------------------
学习STM32除了软件外其实很重要的还有硬件,电子工程这类。我学习STM32纯属偶然,我在PLC设计中需要使用一块带Modbus RTU通讯的中继板来做扩展,这块中继板设置通讯奇偶校验时只能使用固定的无校验,这让我很郁闷,系统中其他站都要设置成无校验才行。所以有了我是不是可以修改下的冲动。电子工程没弄过,这个是个拦路虎,我想我以后如果中断学习了可能是因为这个。









友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
该问题目前已经被作者或者管理员关闭, 无法添加新回复
50条回答
xiatianyun
1楼-- · 2019-07-22 16:09
本帖最后由 xiatianyun 于 2018-6-5 21:36 编辑

还有一些重要的文件没有安装到项目中去。
首先是stm32f10x_conf.h头文件,说过是一个包含所有外设头文件的总头文件,那么需要哪些外设自然由程序员决定,所以这个头文件其实固件库里面可能是没有的,反正我在库里面没有发现,编译时就出错了。
这个文件现在学习阶段就只好复制例程了,到其他示例中找到随便看看就复制到User里面来吧。
还有中断处理的两个文件stm32f10x_it.h和.c文件,这两个是中断处理的用户程序,暂时没有只需要空空的函数定义即可,只让编译通过就行。复制示例文件到User中。
正点原子
2楼-- · 2019-07-22 18:42
xiatianyun 发表于 2018-6-5 21:24
Keil编译配置:
1、Output选项:选择Objects目录,定位在/Template/Output目录;
勾选Create HEX File, ...

不错,继续
warship
3楼-- · 2019-07-22 22:54
 精彩回答 2  元偷偷看……
xiatianyun
4楼-- · 2019-07-23 00:00
本帖最后由 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()的实现。也就是如何使用寄存器来设置倍频、分频数等等,需要了解了寄存器后才能明白。查看这些函数,有个直观的感受是里面的代码是直接使用寄存器来设置数据的,不是使用库函数。通观整个过程,如果只是学习的话,根据硬件只需要把待设置成的频率宏去掉注释而把不需要的宏注释掉就可以了,编译时会自动加入需要的时钟设置函数的。



LZ雾都
5楼-- · 2019-07-23 04:21
xiatianyun
6楼-- · 2019-07-23 08:04
本帖最后由 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_开头。






一周热门 更多>