目录

【新手向】STM32入门

如何从对STM32一脸懵逼到用STM32做点事情。

STM32 是什么、能干什么、如何工作

STM32可以看作一个“弱化”的、可以驱动硬件执行某些操作的电脑。

狭义上说,STM32并不是你手上的那个电路板/vx_images/5172004191142.png,而是中间那个方形的STM32芯片/vx_images/1972905209568.png。对于STM32芯片来说,除了他以外的部分相当于是生产厂家为了帮助你能更方便地使用STM32而开发的,不过一般不引起混淆的话,我们直接把整块板叫做STM32。

当你拿到一块STM32的时候,你至少需要注意以下部分:

  1. STM32 本身的型号

    STM32F103C6T6,他的命名是分段的:

    • STM32
    • F103
    • C6T6

    至于每一个部分代表什么,可以直接到网上查相关资料。你大概需要知道,型号不同,芯片能操作的引脚数、主频(运行速度)、带有的外设数量(比如计时器等等,后面会讲)、后期的软件设置都会略有不同。对于只需要点个灯,蜂鸣器开个响的新手而言,STM32之间几乎没有什么差别,随便选一个教程多的STM32型号就可以(但是一定要确保自己有写入程序进STM32的能力。最小系统板本身并不能下载程序,具体后面会讲)。

  2. 你的 STM32 上有什么

    对比下面的三个板子:

    /vx_images/2056609197435.png/vx_images/3945309217601.png/vx_images/1185010210270.png

    他们都是STM32,但是看起来差别很大。

    • 第一块板一般称为最小系统板。特点是“啥都没有”,你甚至得自己焊针脚上去。

    • 第二块板是意法半导体(生产STM32的公司)自己做的开发板。特点是保留的引脚数目多,而且预留了与Arduino兼容的引脚。

    • 第三块板是正点原子制作的教育开发板。特点是已经自带了非常多硬件,可以快速实现简单功能,但是扩展性比较差。

    作为新手,如果你只是想要玩一下STM32或者排斥做硬件的话,建议选择板载硬件比较多的第三种。当然,一般而言学校的各种竞赛只会发给你最小系统板。使用这种板需要一些硬件基础,而且需要自己买USB转TTL设备(用于串口连接,后面会提到。不是必备的)和STLink(用于把程序下载到STM32中)。

认识 STM32 的组成部分

仔细看你的STM32(下面用最小系统板STM32F103C6T6举例):

/vx_images/2056609197435.png

  • 两侧的孔就是STM32和外界硬件连接的接口,每个接口的边上有名字。你可能会发现很多脚叫做PXxx,其中X=A/B/C/Dxx表示数字,我们一般用到的驱动外部硬件的脚就是这些。其他还有特殊的口,比如GND``3.3V``5V这样的供电口,或者VB(AT)用于备用电池供电,或者R(eset)用于外部重启。

  • 前面的四个脚用来STLink。你的程序就是从这四个口中的两个(SWO``SWCLK,另外两个是GND5V,用于供电)下载到STM32中的。

  • 中间的就是STM32芯片。

  • 黄色的部分用来切换引导模式(可以先不管)。上面插着两个“帽子”,叫做跳线帽,用来连接两个相邻针脚。

  • 黄色的部分边上有个白色的键Reset,用于STM32的复位(重新启动)。

  • 前面四根脚(SWD)的后面有两个灯PWRPC13,用于显示是否通电和是否正常工作。注意,我们可以通过编写软件人为改变PC13(也就是灯)的行为。

  • 还有一些板载芯片用于稳压,保证STM32正常运行。

一般而言,最小系统板具有的部分,较大的开发板都会有,可以仔细看自己手上的开发板,找找上述部分。注意对于上面第三种STM32,某些针脚已经连接了板载硬件,所以看起来针脚会比较少。

学习利用 STM32 进行开发的大致流程

基于STM32的开发是软硬结合的。

大致上说,你要先知道你想要什么功能,然后了解什么硬件可以实现这种功能,然后查相关硬件的使用方式和使用条件(比如需要供多少伏的电等等,这个过程中你可以直接参考其他人的电路设计),最后才是软件编写。

硬件该学多少才能做项目

你得熟悉你要操作的硬件。比如你要操控蜂鸣器发声,你就应该知道蜂鸣器电路怎么连接,也要知道什么样的频率发出什么样的声音;比如你要通过DHT11读入温湿度数据,你就必须要知道DHT11如何工作。同时,有一些芯片可以大大简化你的电路设计。比如四位数码管理论上应该连接十二根线到STM32上,但是如果你知道芯片74HC59574HC138,你就可以简化到只需要接入六个脚。

STM32CubeMX 和 uVision 是干什么的

STM32CubxMX相当于帮助你可视化地初始化STM32。STM32CubeMX生成的代码没有什么特殊性,你甚至可以完全手写一遍,但是STM32CubxMX生成的效率更高(如果有用过Qt的Designer,可以理解为Designer之于你的代码)。

uVision是你编写代码使用的IDE。这个IDE除了编写代码,编译代码以外还集成了下载代码到STM32中的功能。

HAL库和寄存器的区别在哪里

如果你仔细查看HAL库,你会发现HAL库相当于是寄存器套壳。HAL库只是屏蔽了你和底层的关系,使得你的代码更具有可移植性和可读性而已。所以本质上说,HAL库和寄存器没有什么区别,只是一个比较好读,一个比较直接(效率也会相应地稍高一些)而已。

我个人建议是入门先学HAL库。当然,如果有别的单片机基础,直接学寄存器也没问题。

自学帮助

怎么看懂别人写的教程

你可以完全按照教程所说的做。有一些教程会在章节头放非常详细地寄存器操作方式(即使这份教程是HAL库教程)。这是好的,对于对硬件的理解很有帮助,但是对于初学者来说,可能看懂并不容易,这时候可以先跳过这一部分,先看具体代码并且学会如何使用它。等到后面对STM32理解比较深了再回过头来看这些部分。

注意,尤其在初学阶段,建议只看一份教程,而且这份教程最好和你手中的STM32完全配套。如果不配套,有可能出现简单的(但是初学者并不能解决的)兼容性问题。

怎么看懂 STM32CubeMX 为我生成的代码

打开uVision,可以看到我们的main.c所在的文件夹中还有stm32f1xx_it.cstm32f1xx_hal_msp.c。这三个文件就是我们一般更改所在的位置。你可以这么理解:运行时代码放在main.c,中断代码放在stm32f1xx_it.c,而初始化设置放在stm32f1xx_hal_msp.c中。

STM32CubeMX为我们生成的代码很多。要追踪自动生成的代码,我们可以直接从int main();入手。举个例子,我的main函数中有这些函数:

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */

  /* USER CODE BEGIN 2 */

  /* USER CODE END 2 */

  /* Infinite loop */

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

我们点进HAL_Init();,可以看到:

HAL_StatusTypeDef HAL_Init(void)
{
  /* Configure Flash prefetch */
#if (PREFETCH_ENABLE != 0)
#if defined(STM32F101x6) || defined(STM32F101xB) || defined(STM32F101xE) || defined(STM32F101xG) || \
    defined(STM32F102x6) || defined(STM32F102xB) || \
    defined(STM32F103x6) || defined(STM32F103xB) || defined(STM32F103xE) || defined(STM32F103xG) || \
    defined(STM32F105xC) || defined(STM32F107xC)

  /* Prefetch buffer is not available on value line devices */
  __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif
#endif /* PREFETCH_ENABLE */

  /* Set Interrupt Group Priority */
  HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

  /* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
  HAL_InitTick(TICK_INT_PRIORITY);

  /* Init the low level hardware */
  HAL_MspInit();

  /* Return function status */
  return HAL_OK;
}

像这样刨根问底地点进每一个生成的函数,你会发现STM32CubeMX为你生成的代码和你在STM32CubeMX中的图形化设置内容完全一致。比如,如果我使用了TIM3,那么我的main函数当中就会出现:

// int main(void);
/* Initialize all configured peripherals */
  MX_TIM3_Init();

点进去,就会有:

// stm32f1xx_it.c
static void MX_TIM3_Init(void)
{

  /* USER CODE BEGIN TIM3_Init 0 */

  /* USER CODE END TIM3_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM3_Init 1 */

  /* USER CODE END TIM3_Init 1 */
  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 159;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 49;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM3_Init 2 */

  /* USER CODE END TIM3_Init 2 */

}

在STM32CubeMX中,我的TIM3设置形如:

/vx_images/5404452141146.png

是不是完全一致?

如何判断哪里有问题

如果你操作的硬件不能执行,你可以尝试让硬件先完成简单的任务。比如说,如果你不能让四位数码管显示四个数字,那你就先尝试数码管完全点亮;如果你的按键互相冲突,你就取消所有按键,再一个一个按键尝试查错。如果连最简单的任务都不能执行,那就一定存在代码问题或者硬件接触问题。

这时候,你可以先考虑硬件的问题,比如有没有虚焊、电线有没有断开等等。以四位数码管为例,用多用电表测试你的单片机引脚,如果引脚有正常的输出,但是四位数码管没有显示数字,那就是四位数码管内部损坏;相反,如果你的单片机引脚都没有任何反应,要么是软件有问题,要么是单片机本身损坏,可以尝试换另一个STM32,或者检查代码。

STM32 内部细节

中断——IRQ

中断顾名思义就是CPU暂停处理当前任务而去处理一个更加紧急的任务。中断从能否屏蔽来看分为可屏蔽中断和不可屏蔽中断,从中断目的来看可以分为陷阱中断、故障中断和终止中断。

在单片机中,我们常用的中断有计时器中断、RTC秒中断等等。

如果你试过Qt的signal-slot机制,那么你可以大致理解为:我的中断函数就是slot,而单片机负责在相应的signal出现时调用你的slot。

需要注意的是,你不应该在任何中断中写太多耗时操作,尤其不应该添加Delay(原因自行思考)。

串口——USART和UART

串口一般用于在单片机和上位机或者别的单片机之间互相通信。常用的通信协议有USARTUART。区别在于:USART可以同时读写,而UART只能在同一时刻读或者写。

时钟树——RCC

当你第一次面对时钟树的时候:

/vx_images/4626443101150.png

你可以简单地理解为:左边一列是时钟源(其实时钟源还包括中间的PLLCLK),相当于把某些特定频率“灌入”单片机中,而中间的部分就是对于这些频率进行修改(比如倍频或者倍降频),右边“灌水”的终点就是利用这些频率的设备。

PLLCLK相对特殊,可以看作一个特殊的时钟源,用于将四个基本时钟源进行倍频。

仔细看右边一列,我们可以看到哪些设备使用了时钟:

  • to RTC:走时系统
  • to ADC 1, 2:模拟输入频率
  • xCLK:和CPU、总线、SysTickAPB2外设有关

当你希望单片机的运行速度快一些或者慢一些,或者单独调整某些外设的速度,就会来修改RCC系统。一般地,对于刚入门的新手而言,你不是很需要调整RCC,只要保证这个部分在STM32CubeMX中不报错即可。你在看教程的时候,也不需要完全按照教程的RCC来配置(但是要确定运行结果一致,比如你在STM32CubeMX中设置了8M的Systick频率,如果得到1us在代码中的体现是/8,16M配置下你就应该改为/16)。

电压计——ADC

单片机的某些引脚可以复用(就是除了这个功能之外还有别的功能可以选。注意,不是所有引脚都可以复用为ADC)为ADC,此时单片机可以读取这个引脚和单片机GND引脚的电压差。在默认情况下,电压差应该在0~3.3V之间。当然,你也可以采用更改参考电压的方式修改这个区间。

假设你的输入就是默认的0~3.3V,单片机会把你的电压均匀地映射到0~4096上。也就是说,要得到原有电压,你应该采用$V = \frac{3.3}{4096}S$来得到。

计时器——TIM

单片机中会有若干个计时器,每个计时器可以独立设置独立启用。如何设置计时器和如何利用计时器都可以通过查询相关资料学习到。

特殊计时器——SysTick

你可以将SysTick直接看做一个特殊TIM。具体设置方式可以查阅资料。

可断电走时系统——RTC

你的单片机上应该有个口叫做VB(at)。这个引脚用于输入来自纽扣电源的3.3V输入,只给单片机内的RTC模块和某些寄存器供电,保证单片机在VCC断电后还能继续走时并保证某些寄存器内容不丢失。

顺带一提,你的VB外围电路应当这么设置:

/vx_images/1169809129576.png

STM32 的硬件操控

硬件和软件的关系是什么

非常多人在一开始用STM32的时候都会纠结:我应该把外设接到哪个口?软件里又应该如何设置?他们的误解可能是:教程里蜂鸣器接到了PA1,那是不是以后我的蜂鸣器只能接到PA1?或者教程里软件写了PA1,我是不是只能写PA1才能操控蜂鸣器?

可以这么想,单片机并不知道你的外设是什么,他只会根据你的软件编写执行相应行为。相应地,外设自己也不知道他接入了哪个口。也就是说,只要双方约定接入同一个能满足你需求的口,你的单片机就能通过这个引脚操控这个硬件。比如说,你要用单片机驱动蜂鸣器,你只需要选择一个支持GPIO_Output的引脚就可以(如果你要PWN调制,你也可以选择支持TIMx_Output的引脚),比如PA1PB9;如果你要通过TIM输出一个PWN波,你就不能选择一个不能复用为TIMx_Output的引脚。当你选择了一个具体的引脚比如PC2,你的硬件就应该接入PC2,同时软件上操作PC2

特殊的输出方式——PWN 调制输出

你的手机或者电脑屏幕可能正在使用PWN调制。

PWN调制就是用只能输出固定高电平和固定低电平的设备,模拟输出一个不固定电平。具体实现的方法可以查询相关资料。

尝试驱动一些基本单元

当你有教程的时候,驱动基本单元就是一件很简单的事情,

如何实现复杂的外设控制

但是当你的基本单元变多了,你就应该考虑控制与控制之间会不会互相干扰。比如你的STM32既要点亮四位数码管,又要读入DHT11的输入,你就应该考虑点亮四位数码管的中断(如果你采用中断来显示数码管)会不会导致DHT11输入的误差。或者说,如果你的某个外设需要Delay(5000),你的按键可能也只能五秒读取一次。这时候,你就要合理地改变Delay的使用位置和时间,或者干脆就把需要Delay的部分写到主循环中,把不需要Delay的部分写到计时器中断里。