打开主菜单

谷雨文档中心 β

NBDK-L4:基础实验教程

Jinx讨论 | 贡献2019年1月25日 (五) 14:10的版本 STM32L476 随机发生器简介

目录

1 教程介绍

1.1 工程简介

NBDK-L4开发板基础实验包含如下,在这里给大家简单说明一下每个例程中讲解的内容及关键节点。

实验简介
实验名称 内容简介 功能
01-led实验 驱动LED点亮 GPIO推挽输出
02-马达实验 驱动马达振动 GPIO推挽输出
03-蜂鸣器实验 驱动蜂鸣器发声 GPIO推挽输出
04-按键中断实验 按键控制 GPIO外部中断
05-光敏二极管实验 采集光敏二极管数据 ADC采集
06-DAC实验 引脚输出模拟电平 DAC输出
07-温湿度实验 采集SHT20温湿度数据 I2C接口使用、SHT20使用
08-RGB实验 定时器TIM2 PWM输出
09-红外接收实验 红外接收器获取遥控器按键信息 定时器TIM3捕获模式
10-串口打印实验 串口格式化输出数据 串口使用,printf使用
11
12

1.2 工程目录简介

大家打开任意一个基础例程,都会看到如下的4个目录(Drivers、Inc、MDK-ARM、Src)及clean.bat文件。

其中clean.bat是用于清除工程编译生成的中间文件。例如我们想拷贝一个编译过的工程,工程有200M左右大小,我们点击clean.bat清除一下编译生成的中间文件,则工程大概会缩小到100M左右,此时工程只剩下了库文件、用户文件,以及编译生成的hex文件。

从上图可以看到,四个主目录下分别包含的一些文件,这边给大家简单的介绍一下这边文件大概的功能。

Drivers:

STM32驱动文件目录,也就是大家常说的hal库,里面包含了hal(硬件抽象层)相关的文件。

主要就是有RCC时钟、Flash内存,以及大家常用的外设(例如uart、spi、adc等等)的一些库文件。

Inc:

用户.h头文件,用户文件的头文件一般都放到这边,也可自己另存其他位置,但是需要在keil中添加头文件所在的路径。

添加新路径的方式如下图所示,可以看到../Inc这个路径已经事先添加进去了。

MDK-ARM:

工程目录,主要是两个工程文件“.uvoptx”以及".uvprojx"(keil打开的是这个)。剩下的文件比较重要的是Output目录下编译生成的“.hex”文件。

Src:

用户.c文件,用户自己开发的一些驱动文件(外设驱动等等),以及main文件所在的目录。

1.3 常用文件简介

针对试验工程中的常见文件,我们以开发者的方式来给大家做一个简单的介绍。具体每个文件中的源码的详细说明,大家可以参照每个试验下的源码详解。

常见文件简介列表
文件名 说明
stm32l4xx_hal_conf.h 路径位于Inc目录下,里面主要是一些宏定义,用于选择本工程所使用的库文件。这边选择的库文件,就是目录简介中提到的HAL库。
main.c: main()所在的文件,keil中我们配置了run to main(),也就是说工程从main()函数开始执行,所以main.c就是我们工程的主文件。
stm32l4xx_hal_msp.c msp(main stack pointer)主栈堆指针初始化的文件。我们重定义外设引脚选择的时候,STM32Cube生成的硬件引脚重定义函数默认也位于此文件下,但是为了方便,我们一般将其复制到各自的驱动文件下。
stm32l4xx_it.c 中断配置文件,用于存放工程的中断。STM32Cube生成的中断函数默认位于此文件下,同样为了方便起见,我们一般将各自的中断函数放到各自的驱动文件下。
gyu_util.c 从STM32Cube生成的main文件中独立出来的部分。主要用于处理工程的时钟选择,包含系统时钟、总线时钟以及外设时钟。
stm32l4xx_hal_xx HAL库文件。
gyu_xx 由谷雨物联编写的文件,大部分是外设的驱动文件。

2 01-led实验

第一个实验我们给大家带来的是最简单的外设控制,也就是 IO 口操作,通过这个实验我们可以了解到如何让STM32L476RC的一个 IO 输出高低电平,并以此控制 LED 的点亮和熄灭。  

2.1 STM32L476 IO简介

每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。

经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:

  1. 输入浮空
  2. 输入上拉
  3. 输入下拉
  4. 模拟
  5. 带上拉或下拉的开漏输出
  6. 带上拉或下拉的推挽输出
  7. 带上拉或下拉的复用功能推挽
  8. 带上拉或下拉的复用功能开漏

2.2 硬件设计

选择STM32L4引脚PA15作为LED的控制引脚,PA15高电平时点亮LED。

2.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用Keil打开基础实验 01-led实验工程。
  3. 下载程序,并完成功能测试。

2.4 实验验证

下载完成后,可以看到开发板上的LED灯周期闪烁,点亮及熄灭的周期时间为500ms。

2.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

2.5.1 stm32l4xx_hal_conf.h

此文件位于“01-led实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。

此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO

2.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化LED引脚配置,并且在while()循环中周期点亮、熄灭LED。

31 int main(void)
32 {
33   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
34 	// 重置所有外设、flash界面以及系统时钟
35   HAL_Init();
36 
37 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
38   SystemClock_Config();
39 	
40   // 初始化LED引脚
41   LED_Init();
42 	
43   // 
44   while (1)
45   {
46 		LED_SET(GPIO_PIN_SET);			// 设置LED引脚(PA15)输出高电平,LED点亮
47 		HAL_Delay(500);							// 延时500ms
48 		LED_SET(GPIO_PIN_RESET);		// 设置LED引脚(PA15)输出低电平,LED熄灭
49 		HAL_Delay(500);							// 延时500ms
50   }
51 }

2.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

2.5.4 gyu_led.c

此文件用于配置LED控制引脚,在LED_Init()函数中我们初始化PA15为推挽输出,并且使能GPIOA时钟,初始化PA15默认输出低电平。

31 void LED_Init(void)
32 {
33 	GPIO_InitTypeDef  GPIO_InitStructure;               // 定义引脚参数结构体
34 
35 	__HAL_RCC_GPIOA_CLK_ENABLE();                       // 使能GPIOA时钟
36 
37 	GPIO_InitStructure.Pin= GPIO_PIN_15;                // 引脚编号为15
38 	GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;      // 推挽输出
39 	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW;     // 低频率
40 	GPIO_InitStructure.Pull = GPIO_PULLDOWN;            // 下拉
41 	HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);          // 初始化PA15
42 	
43 	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);    // 设置PA15默认输出低电平
44 }

LED_SET()函数留给大家控制LED灯点亮或者熄灭,参数可选为GPIO_PIN_RESET(低电平)或者GPIO_PIN_SET(高电平)。

54 void LED_SET(GPIO_PinState pinSate)
55 {   
56 	HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, pinSate);   // 设置PA15输出
57 }

3 02-马达实验

振动马达实验通过控制 GPIO 引脚输出高低电平,用于控制马达振动或停止 。

3.1 STM32L476 IO简介

每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。

经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:

  1. 输入浮空
  2. 输入上拉
  3. 输入下拉
  4. 模拟
  5. 带上拉或下拉的开漏输出
  6. 带上拉或下拉的推挽输出
  7. 带上拉或下拉的复用功能推挽
  8. 带上拉或下拉的复用功能开漏

3.2 硬件设计

选择STM32L4引脚PC7作为马达的控制引脚,PC7高电平时马达起振。

3.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用Keil打开基础实验 02-马达实验工程。
  3. 下载程序,并完成功能测试。

3.4 实验验证

下载完成后,按下开发板上按键S1,马达起振,按下S3,马达停止。

3.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

3.5.1 stm32l4xx_hal_conf.h

此文件位于“02-马达实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。

此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO

3.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来是初始化按键,有关按键的部分会在04-按键实验中给大家讲解。

这边我们主要关注的是马达的初始化,其实是和LED实验一样的,就是初始化一下马达控制IO。

36 int main(void)
37 {
38   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 	// 重置所有外设、flash界面以及系统时钟
40   HAL_Init();
41 
42 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43   SystemClock_Config();
44   
45   // 初始化按键引脚
46 	MX_KEY_Init();
47   
48   //注册按钮回调函数
49   KEY_RegisterCb(AppKey_cb);
50   
51 	// 初始化马达
52 	Motor_Init();
53   
54   // 
55   while (1)
56   {
57 		KEY_Poll();   // 按键轮训,监测是否有按键被按下
58   }
59 }

在按键的处理回调函数中,我们可以看到,按键S1(UP)按下后,设置马达引脚高电平,按键S3(DOWN)按下后,设置马达引脚低电平

69 void AppKey_cb(uint8_t key)
70 {
71   // 如果有相应按键被按下,则串口打印调试信息
72   if(key & KEY_UP)
73   {
74     Motor_SET(GPIO_PIN_SET);
75   }
76   if(key & KEY_LEFT)
77   {
78     //
79   }
80   if(key & KEY_DOWN)
81   {
82     Motor_SET(GPIO_PIN_RESET);
83   }
84   if(key & KEY_RIGHT)
85   {
86     //
87   }
88 }

3.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

3.5.4 gyu_motor.c

马达引脚初始化函数,初始化PC7推挽输出低电平。

31 void Motor_Init(void)
32 {
33 	GPIO_InitTypeDef  GPIO_InitStructure;               // 定义引脚参数结构体
34 
35 	__HAL_RCC_GPIOC_CLK_ENABLE();                       // 使能GPIOC时钟
36 
37 	GPIO_InitStructure.Pin= GPIO_PIN_7;                 // 引脚编号为7
38 	GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;      // 推挽输出
39 	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW;     // 低频率
40 	GPIO_InitStructure.Pull = GPIO_PULLUP;              // 上拉
41 	HAL_GPIO_Init(GPIOC, &GPIO_InitStructure);          // 初始化PC7
42 	
43 	HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, GPIO_PIN_RESET);    // 设置PC7默认输出低电平
44 }

马达引脚电平设置函数,设置为高电平,马达起振,设置低电平,马达停止。

54 void Motor_SET(GPIO_PinState pinSate)
55 {   
56 	HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, pinSate);   // 设置PC7输出
57 }

4 03-蜂鸣器实验

蜂鸣器实验通过控制 GPIO 引脚输出高低电平,用于控制蜂鸣器发出蜂鸣声或者停止发声 。

4.1 STM32L476 IO简介

每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。

经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:

  1. 输入浮空
  2. 输入上拉
  3. 输入下拉
  4. 模拟
  5. 带上拉或下拉的开漏输出
  6. 带上拉或下拉的推挽输出
  7. 带上拉或下拉的复用功能推挽
  8. 带上拉或下拉的复用功能开漏

4.2 硬件设计

选择STM32L4引脚PB2作为蜂鸣器的控制引脚,PB2高电平时蜂鸣器发出蜂鸣声。

4.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用Keil打开基础实验 03-蜂鸣器实验工程。
  3. 下载程序,并完成功能测试。

4.4 实验验证

下载完成后,按下开发板上按键S1,蜂鸣器发声,按下S3,蜂鸣器停止。

4.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

4.5.1 stm32l4xx_hal_conf.h

此文件位于“03-蜂鸣器实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。

此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO

4.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来是初始化按键,有关按键的部分会在04-按键实验中给大家讲解。

最后我们初始化蜂鸣器引脚,配置蜂鸣器引脚为默认输出低电平。

36 int main(void)
37 {
38   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 	// 重置所有外设、flash界面以及系统时钟
40   HAL_Init();
41 
42 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43   SystemClock_Config();
44   
45   // 初始化按键引脚
46   MX_KEY_Init();
47   
48   //注册按钮回调函数
49   KEY_RegisterCb(AppKey_cb);
50   
51   //初始化蜂鸣器
52   Buzzer_Init();
53   
54   // 
55   while (1)
56   {
57 		KEY_Poll();   // 按键轮训,监测是否有按键被按下
58   }
59 }

在按键的处理回调函数中,我们可以看到,按键S1(UP)按下后,设置蜂鸣器引脚高电平,按键S3(DOWN)按下后,设置蜂鸣器引脚低电平

69 void AppKey_cb(uint8_t key)
70 {
71   // 如果有相应按键被按下,则串口打印调试信息
72   if(key & KEY_UP)
73   {
74     Motor_SET(GPIO_PIN_SET);
75   }
76   if(key & KEY_LEFT)
77   {
78     //
79   }
80   if(key & KEY_DOWN)
81   {
82     Motor_SET(GPIO_PIN_RESET);
83   }
84   if(key & KEY_RIGHT)
85   {
86     //
87   }
88 }

4.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

4.5.4 gyu_buzzer.c

蜂鸣器引脚初始化函数,初始化PB2推挽输出低电平。

31 void Buzzer_Init(void)
32 {
33 	GPIO_InitTypeDef  GPIO_InitStructure;               // 定义引脚参数结构体
34 
35 	__HAL_RCC_GPIOB_CLK_ENABLE();                       // 使能GPIOB时钟
36 
37 	GPIO_InitStructure.Pin= GPIO_PIN_2;                 // 引脚编号为2
38 	GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;      // 推挽输出
39 	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW;     // 低频率
40 	GPIO_InitStructure.Pull = GPIO_PULLUP;              // 上拉
41 	HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);          // 初始化PB2
42 	
43 	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET);    // 设置PB2默认输出低电平
44 }

蜂鸣器引脚电平设置函数,设置为高电平,蜂鸣器发出蜂鸣声,设置低电平,蜂鸣器停止。

54 void Buzzer_SET(GPIO_PinState pinSate)
55 {   
56 	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, pinSate);   // 设置PB2输出
57 }

5 04-按键中断实验

按键中断实验,是通过外部引脚中断来判断是否有按键被按下,按键部分的代码这个在马达以及蜂鸣器实验中其实已经展示过了,这边给大家做一个详细的讲解。我们分别选择PC0、PC1、PC2、PC3这4个引脚作为我们的按键引脚,对应EXTI line0、EXTI line1、EXTI line2、EXTI line3。

5.1 STM32L476 外部中断简介

首先我们看一下外部中断/事件的GPIO映射图。

由上面的映射图可以知道,多个GPIO引脚(GPIOA、GPIOB、GPIOC、GPIOD等等的GPIO_Pin_0)都会触发同一个中断线(EXTI line0)。也就是说,当EXTI0被触发时,我们无法判断他是PA0触发,还是PB0触发,因此大家在设计自己的硬件的时候,需要选择合适的中断引脚。

源码中我们配置外部中断的步骤如下:

1.使能GPIO时钟

2.GPIO初始化,配置GPIO的边沿触发条件

3.设置EXTI线,配置GPIO与EXTI的关系

4.中断向量初始化

5.2 硬件设计

选择STM32L4引脚PC0、PC1、PC2、PC3作为按键的控制引脚。

5.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 将SW1拨到DBG端,SW2拨到MCU。
  3. 使用Keil打开基础实验 04-按键中断实验工程。
  4. 使用Xshell打开Jlink虚拟出的COM口
  5. 下载程序,并完成功能测试。

5.4 实验验证

下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下:

5.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

5.5.1 stm32l4xx_hal_conf.h

此文件位于“04-按键中断实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。

此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_DMA_MODULE_ENABLED      // DMA
112 #define HAL_UART_MODULE_ENABLED     // UART

5.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化了串口部分,目的是打印按键按下的调试信息。

接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。

在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.c中会将按键信息,通过上面说的回调函数,传到应用层(mian.c)中进行处理。

36 int main(void)
37 {
38   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
39 	// 重置所有外设、flash界面以及系统时钟
40   HAL_Init();
41 
42 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
43   SystemClock_Config();
44 	
45 	// 初始化USART1
46 	MX_USART1_UART_Init();
47   
48   // 初始化按键引脚
49 	MX_KEY_Init();
50   
51   //注册按钮回调函数
52   KEY_RegisterCb(AppKey_cb);
53   
54   // 
55   while (1)
56   {
57 		KEY_Poll();   // 按键轮训,监测是否有按键被按下
58   }
59 }

在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。

69 void AppKey_cb(uint8_t key)
70 {
71   // 如果有相应按键被按下,则串口打印调试信息
72   if(key & KEY_UP)
73   {
74     printf("key_up press\r\n");
75   }
76   if(key & KEY_LEFT)
77   {
78     printf("key_left press\r\n");
79   }
80   if(key & KEY_DOWN)
81   {
82     printf("key_down press\r\n");
83   }
84   if(key & KEY_RIGHT)
85   {
86     printf("key_right press\r\n");
87   }
88 }

5.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

5.5.4 gyu_key.c

首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。

 75 void MX_KEY_Init(void)
 76 {
 77   // 定义GPIO结构体
 78   GPIO_InitTypeDef GPIO_InitStruct;
 79 
 80   // 使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3)
 81   __HAL_RCC_GPIOC_CLK_ENABLE();
 82 
 83   // 配置按键引脚
 84   GPIO_InitStruct.Pin = KEY_LEFT_Pin|KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_Pin; // 选择PC0、PC1、PC2、PC3
 85   GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;    // 下降沿中断触发
 86   GPIO_InitStruct.Pull = GPIO_PULLUP;             // 上拉
 87   HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);         // 初始化引脚
 88   
 89   // 配置中断优先级,并且使能中断
 90   {
 91   // 配置PC0的中断,也就是EXTI line0
 92   HAL_NVIC_SetPriority(KEY_LEFT_EXTI_IRQn, 10, 0);
 93   HAL_NVIC_EnableIRQ(KEY_LEFT_EXTI_IRQn);
 94   
 95   // 配置PC1的中断,也就是EXTI line1    
 96   HAL_NVIC_SetPriority(KEY_DOWN_EXTI_IRQn, 10, 0);
 97   HAL_NVIC_EnableIRQ(KEY_DOWN_EXTI_IRQn);
 98   
 99   // 配置PC2的中断,也就是EXTI line2  
100   HAL_NVIC_SetPriority(KEY_RIGHT_EXTI_IRQn, 10, 0);
101   HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn);
102   
103   // 配置PC3的中断,也就是EXTI line3
104   HAL_NVIC_SetPriority(KEY_UP_EXTI_IRQn, 10, 0);
105   HAL_NVIC_EnableIRQ(KEY_UP_EXTI_IRQn);
106   }
107 }

如下,是我们在初始化函数中打开的四个中断线。

43 // EXTI line0 中断函数
44 void EXTI0_IRQHandler(void)
45 {
46   HAL_GPIO_EXTI_IRQHandler(KEY_LEFT_Pin);
47 }
48 
49 // EXTI line1 中断函数
50 void EXTI1_IRQHandler(void)
51 {
52   HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_Pin);
53 }
54 
55 // EXTI line2 中断函数
56 void EXTI2_IRQHandler(void)
57 {
58   HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_Pin);
59 }
60 
61 // EXTI line3 中断函数
62 void EXTI3_IRQHandler(void)
63 {
64   HAL_GPIO_EXTI_IRQHandler(KEY_UP_Pin);
65 }

我们配置好上面所说的引脚外部中断后,一旦有按键被按下,则会触发中断,最终会跑到如下的HAL_GPIO_EXTI_Callback()函数中。我们在这个函数中,判断一下是哪一个中断线触发的中断,并且记录一下按键信息,以及触发的时间(记录触发的时间,是为了进行按键消抖,防止误操作)。

119 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
120 {
121   // 如果是UP键被触发,记录按键任务为KEY_UP,并记录当前时钟
122   if(GPIO_Pin == KEY_UP_Pin)
123   {
124     key_check_press.key_event = KEY_UP;
125     key_check_press.start_tick = HAL_GetTick();
126   }
127   // 如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟
128   if(GPIO_Pin == KEY_LEFT_Pin)
129   {
130     key_check_press.key_event = KEY_LEFT;
131     key_check_press.start_tick = HAL_GetTick();
132   }
133   // 如果是DOWN键被触发,记录按键任务为KEY_DOWN,并记录当前时钟
134   if(GPIO_Pin == KEY_DOWN_Pin)
135   {
136     key_check_press.key_event = KEY_DOWN;
137     key_check_press.start_tick = HAL_GetTick();
138   }
139   // 如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟
140   if(GPIO_Pin == KEY_RIGHT_Pin)
141   {
142     key_check_press.key_event = KEY_RIGHT;
143     key_check_press.start_tick = HAL_GetTick();
144   }
145 }

按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。

171 void KEY_Poll(void)
172 {
173   uint8_t key_event = 0;
174   
175   // 如果有按键任务
176   if(key_check_press.key_event)
177   {
178     // 获取当前时钟 减去 记录的按键触发时钟,如果大于消抖延时,则继续向下判断
179     if(HAL_GetTick() - key_check_press.start_tick >= KEY_DELAY_TICK )
180     {
181       // 如果按键任务记录为KEY_UP
182       if(key_check_press.key_event & KEY_UP)
183       {
184         // 获取当前KEY_UP引脚电平,如果是低电平,则认为UP按键被按下
185         if(HAL_GPIO_ReadPin(KEY_UP_GPIO_Port,KEY_UP_Pin) == GPIO_PIN_RESET)
186         {
187           key_event |= KEY_UP;  // 记录app按键任务
188         }
189         key_check_press.key_event ^= KEY_UP;  // 删除按键中断任务
190       }
191       
192       // 如果按键任务记录为KEY_LEFT
193       if(key_check_press.key_event & KEY_LEFT)
194       {
195         // 获取当前KEY_LEFT引脚电平,如果是低电平,则认为LEFT按键被按下
196         if(HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port,KEY_LEFT_Pin) == GPIO_PIN_RESET)
197         {
198           key_event |= KEY_LEFT;  // 记录app按键任务
199         }
200         key_check_press.key_event ^= KEY_LEFT;  // 删除按键中断任务
201       }
202       
203       // 如果按键任务记录为KEY_DOWN
204       if(key_check_press.key_event & KEY_DOWN)
205       {
206         // 获取当前KEY_DOWN引脚电平,如果是低电平,则认为DOWN按键被按下
207         if(HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port,KEY_DOWN_Pin) == GPIO_PIN_RESET)
208         {
209           key_event |= KEY_DOWN;  // 记录app按键任务
210         }
211         key_check_press.key_event ^= KEY_DOWN;  // 删除按键中断任务
212       }
213       
214       // 如果按键任务记录为KEY_RIGHT
215       if(key_check_press.key_event & KEY_RIGHT)
216       {
217         // 获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下
218         if(HAL_GPIO_ReadPin(KEY_RIGHT_GPIO_Port,KEY_RIGHT_Pin) == GPIO_PIN_RESET)
219         {
220           key_event |= KEY_RIGHT;   // 记录app按键任务
221         }
222         key_check_press.key_event ^= KEY_RIGHT;   // 删除按键中断任务
223       }
224     }
225   }
226   //如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数
227   if(key_event && pFkey_cb)
228   {
229     pFkey_cb(key_event);
230   }
231 }

留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。

155 void KEY_RegisterCb(key_cb cb)
156 {
157   if(cb != 0)
158   {
159     pFkey_cb = cb;
160   }
161 }

6 05-光敏二极管实验

光敏二极管在不同的光照强度下,它的out引脚输出的电压不同。所以此实验,我们利用STM32L4的ADC功能,去采集光敏二极管的引脚输出电压,以此能够获取到当前环境的光照情况。

6.1 STM32L476 ADC简介

STM32L476一共有3个ADC(ADC1、ADC2、ADC3),3个ADC都可以进行独立的工作,每个ADC都支持12位精度的采样。

每个ADC最多有19个多路复用的通道,每个通道的AD转换都是可以执行单一、连续扫描或者不连续扫描的。

ADC的参考电压取决于VREF+引脚的输入电压,在开发板上,我们接好miniUSB供电,此引脚电压范围在3.2V~3.3V之间,代码中我们默认取3.3V为参考电压。

ADC引脚的输入电压范围:VREF- ≤ VIN ≤ VREF+ ,在开发板上,就是大于0V,小于3.3V。

在这个例程中,我们将以ADC1的通道16(也就是PB1引脚)来采集光敏二极管的输出电压。

6.2 硬件设计

选择STM32L4引脚PB1作为光敏二极管的ADC采集引脚。

6.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 将SW1拨到DBG端,SW2拨到MCU。
  4. 使用Keil打开基础实验 05-光敏二极管实验工程。
  5. 使用Xshell打开Jlink虚拟出的COM口
  6. 下载程序,并完成功能测试。

6.4 实验验证

下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的ADC数据。 红色字体部分,是正常的室内光照强度时采集的电压;绿色字体部分,是打开手机手电筒照射光敏二极管时采集的电压。可以看到明显的电压差值。

6.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

6.5.1 stm32l4xx_hal_conf.h

此文件位于“05-光敏二极管实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的ADC功能,所以我们宏定义中打开ADC相关的。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_UART_MODULE_ENABLED     // UART
112 #define HAL_DMA_MODULE_ENABLED      // DMA  
113 #define HAL_ADC_MODULE_ENABLED      // ADC

6.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化了串口部分,目的是打印ADC采集电压值。

接下来是ADC引脚初始化。

在最后的while()循环中,我们调用ADC值采集函数,每个500ms采集一次,并且将采集值转换成电压值,格式化打印到串口显示。

33 int main(void)
34 {
35   uint16_t ad_value = 0;
36 
37   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
38 	// 重置所有外设、flash界面以及系统时钟
39   HAL_Init();
40 
41 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
42   SystemClock_Config();
43 	
44   // 初始化串口USART1
45   MX_USART1_UART_Init();
46 
47   // 初始化ADC1
48   MX_ADC1_Init();
49   
50   // 
51   while (1)
52   {
53     HAL_Delay(500);
54     ad_value = HAL_ADC_Read();    // 获取ADC采集值
55     printf("Adc_value = %.2f\r\n",ad_value*3.3/4095);     // 转换ADC采集为真实电压,并且打印到串口显示
56   }
57 }

6.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

6.5.4 gyu_adc.c

ADC初始化函数,配置12位采样分辨率,配置2.5倍ADC时钟周期的采样频率。

选择ADC通道16(CH16)作为此次的ADC引脚(也就是PB1)。

40 void MX_ADC1_Init(void)
41 {
42   // 定义常规ADC通道结构体
43   ADC_ChannelConfTypeDef sConfig;
44 
45   // 初始化ADC
46   hadc1.Instance = ADC1;                              // ADC寄存器地址,定义为ADC1
47   hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;   // 没有预分频器的ADC异步时钟
48   hadc1.Init.Resolution = ADC_RESOLUTION_12B;         // 12位采样分辨率
49   // 初始化ADC,如果失败,则进入错误处理程序
50   if (HAL_ADC_Init(&hadc1) != HAL_OK)
51   {
52     _Error_Handler(__FILE__, __LINE__);
53   }
54 
55   // 配置ADC通道
56   sConfig.Channel = ADC_CHANNEL_16;                 // 配置为ADC通道16
57   sConfig.Rank = ADC_REGULAR_RANK_1;                // 指定ADC规格组中的排名
58   sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5;  // 采样时间配置为2.5个ADC时钟周期
59   sConfig.SingleDiff = ADC_SINGLE_ENDED;            // ADC通道结束设置为单端 
60   sConfig.OffsetNumber = ADC_OFFSET_NONE;           // 禁用ADC偏移
61   sConfig.Offset = 0;                               // 定义从原始量中减去的偏移量
62   if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)  // 配置ADC通道
63   {
64     _Error_Handler(__FILE__, __LINE__);   // 如果出错,进入错误处理
65   }
66 
67 }

重新定义ADC的硬件引脚,可以看到,配置PB1为ADC引脚。

77 void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
78 {
79   GPIO_InitTypeDef GPIO_InitStruct;
80   if(adcHandle->Instance==ADC1)
81   {
82     // 使能GPIOB引脚时钟(选择的ADC引脚为PB1)
83     __HAL_RCC_GPIOB_CLK_ENABLE();
84 
85     // 使能ADC时钟
86     __HAL_RCC_ADC_CLK_ENABLE();
87   
88     // 初始化ADC引脚配置
89     GPIO_InitStruct.Pin = GPIO_PIN_1;                     // 选择引脚编号1
90     GPIO_InitStruct.Mode = GPIO_MODE_ANALOG_ADC_CONTROL;  // 配置为ADC引脚
91     GPIO_InitStruct.Pull = GPIO_NOPULL;                   // 无上下拉
92     HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);               // 初始化PB1引脚
93   }
94 }

用于获取ADC采集值的函数,经过一系列的API函数调用,我们获取到最终的adcValue。

104 uint16_t HAL_ADC_Read(void)
105 {
106   uint16_t ad_value = 0;
107   
108   // 启动ADC转换
109   HAL_ADC_Start(&hadc1);  
110   
111   // 等待ADC转换完成
112   HAL_ADC_PollForConversion(&hadc1, 50);
113   
114   // 检查是否已经完成转换  
115   if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC)) 
116   {
117     // 获取采集到的ADC值
118     ad_value = HAL_ADC_GetValue(&hadc1);
119   }
120   
121   return ad_value;  // 返回ADC采集值
122 }

7 06-DAC实验

此实验我们将会配置DAC1的通道1,作为模拟电压的输出引脚,并且配置一个ADC引脚,采集DAC输出的电压,并将电压值格式化打印到串口显示。

7.1 STM32L476 DAC简介

DAC模块是一个12位电压输出数模转换器。 DAC可配置为8位或12位模式,并可与DMA控制器配合使用。在12位模式下,数据可以左对齐或右对齐。 DAC有两个输出通道,每个通道都有自己的转换器。在双DAC通道模式下,当两个通道组合在一起进行同步更新操作时,可以单独或同时完成转换。

DAC的主要特性如下:

•两个DAC转换器:每个转换器一个输出通道

•12位模式下的左或右数据对齐

•同步更新功能

•噪声波和三角波生成

•双DAC通道,用于独立或同步转换

•每个通道的DMA功能,包括DMA欠载错误检测

•转换的外部触发器

•DAC输出通道缓冲/非缓冲模式

•缓冲偏移校准

•每个DAC输出可以与DAC_OUTx输出引脚断开

•DAC输出连接到片上外设

•采样和保持模式,用于在停止模式下进行低功耗操作

•输入电压参考,VREF +

7.2 硬件设计

选择STM32L4引脚PA4作为模拟输出(DAC)引脚,配置PA7作为ADC引脚。

此实验测试的时候,由于选择的PA4及PA7引脚被显示屏占用,所以我们需要拔下显示屏,然后使用杜邦线短接PA4及PA7引脚。

7.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 将SW1拨到DBG端,SW2拨到MCU。
  4. 使用Keil打开基础实验 06-DAC实验工程。
  5. 使用Xshell打开Jlink虚拟出的COM口
  6. 下载程序,并完成功能测试。

7.4 实验验证

下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的ADC数据。

我们默认配置DAC引脚输出2000(也就是1.6V)电压,可以看到ADC采集到的数据为1.56V,这个是ADC采集的偏移量导致的。

7.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

7.5.1 stm32l4xx_hal_conf.h

此文件位于“06-DAC实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的DAC功能,所以我们宏定义中打开DAC相关的。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_UART_MODULE_ENABLED     // UART
112 #define HAL_DMA_MODULE_ENABLED      // DMA  
113 #define HAL_ADC_MODULE_ENABLED      // ADC
114 #define HAL_DAC_MODULE_ENABLED      // DAC

7.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化了串口部分,目的是打印采集到DAC输出电压。

接下来分别初始化了ADC以及DAC引脚配置。

在最后的while()循环中,我们设置DAC引脚的输出电压(默认设置输出1.6V),然后我们调用ADC值采集函数采集这个引脚电压,并且每隔500ms将采集值转换成电压值,格式化打印到串口显示。

33 int main(void)
34 {
35   uint16_t ad_value = 0;
36 
37   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
38 	// 重置所有外设、flash界面以及系统时钟
39   HAL_Init();
40 
41 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
42   SystemClock_Config();
43 	
44   // 初始化串口USART1
45   MX_USART1_UART_Init();
46 
47   // 初始化ADC1
48   MX_ADC1_Init();
49 
50   // 初始化DAC1
51   MX_DAC1_Init();
52   
53   // 
54   while (1)
55   {
56     HAL_Delay(500);
57     
58     HAL_DAC_Set(2000);    // 设置2000DAC输出值(1.6V)
59     
60     ad_value = HAL_ADC_Read();        // 获取ADC采集值
61     printf("adc = %d\r\n",ad_value);  // 打印ADC采集值
62     printf("Adc_value = %.2fV\r\n",ad_value*3.3/4095);   // 转换ADC采集为真实电压,并且打印到串口显示
63   }
64 }

7.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

7.5.4 gyu_dac.c

DAC初始化函数,配置DAC1通道1(也就是PA4引脚)为DAC输出引脚。

37 void MX_DAC1_Init(void)
38 {
39   hdac1.Instance = DAC1;                // 配置为DAC1
40   if (HAL_DAC_Init(&hdac1) != HAL_OK)   // 初始化DAC1
41   {
42     _Error_Handler(__FILE__, __LINE__);
43   }
44   
45   // DAC通道配置结构体定义
46   DAC_ChannelConfTypeDef sConfig;
47   sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE;  //DAC模式
48   sConfig.DAC_Trigger=DAC_TRIGGER_NONE;                   // 不使用触发功能
49   sConfig.DAC_OutputBuffer=DAC_OUTPUTBUFFER_DISABLE;      // DAC1输出缓冲关闭
50 	sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE;  //不连接到片内外设
51   
52   // 初始化通道CH1配置
53   if (HAL_DAC_ConfigChannel(&hdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK)
54   {
55     _Error_Handler(__FILE__, __LINE__);
56   }
57   
58   HAL_DAC_Start(&hdac1,DAC_CHANNEL_1);  //开启DAC通道1
59 }

重新定义DAC的硬件引脚,可以看到,配置PA4为DAC引脚。

70 void HAL_DAC_MspInit(DAC_HandleTypeDef* hdac)
71 {
72   GPIO_InitTypeDef GPIO_InitStruct;
73   if(hdac->Instance==DAC1)                  // 判断是否是DAC1
74   {
75     __HAL_RCC_DAC1_CLK_ENABLE();            // 使能DAC时钟
76     
77     GPIO_InitStruct.Pin = GPIO_PIN_4;       // 选择PA4
78     GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;// 配置模拟模式
79     GPIO_InitStruct.Pull = GPIO_NOPULL;     // 无上下拉
80     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化PA4引脚配置
81   }
82 }

设置DAC引脚输出电压,value为单元值,不是真正的电压值大小。 模拟电压值(范围:0~4095,对应0~3.3V)。

92 void HAL_DAC_Set(uint16_t value)
93 {
94   HAL_DAC_SetValue(&hdac1,DAC_CHANNEL_1,DAC_ALIGN_12B_R,value); // 配置CH1 12位右对齐模拟输出
95 }

8 07-温湿度实验

此实验给大家展示的是利用STM32L476的I2C外设功能,去获取sht20温湿度传感器采集的温湿度数据。并且将获取到的数据转换成真实的温湿度数据,格式化打印到串口显示。

8.1 STM32L476 I2C简介

I2C(内部集成电路)总线接口处理STM32L4和串行I2C总线之间的通信。 它提供多主机功能,并控制所有I2C总线特定的排序,协议,仲裁和定时。 它支持标准模式(Sm),快速模式(Fm)和快速模式加(Fm +)。兼容SMBus(系统管理总线)和PMBus(电源管理总线)。

I2C主要功能:

•I2C总线规范rev03兼容性:

- 从模式和主模式

- 多主机功能

- 标准模式(最高100 kHz)

- 快速模式(最高400 kHz)

- 快速模式加(最高1 MHz)

- 7位和10位寻址模式

- 多个7位从机地址(2个地址,1个带可配置掩码)

- 所有7位地址确认模式

- 一般电话

- 可编程设置和保持时间

- 易于使用的事件管理

- 可选的时钟拉伸

- 软件重置

•具有DMA功能的1字节缓冲区

•可编程模拟和数字噪声滤波器

8.2 硬件设计

选择STM32L4引脚PB13作为I2C SCL引脚,PB14作为I2C SDA引脚。

8.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 将SW1拨到DBG端,SW2拨到MCU。
  4. 使用Keil打开基础实验 07-温湿度实验工程。
  5. 使用Xshell打开Jlink虚拟出的COM口
  6. 下载程序,并完成功能测试。

8.4 实验验证

下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的温湿度数据。

在采集的过程中,我们将手指按在SHT20上,可以看到温度和湿度都在上升,例如温度,由开始的20.4°C上升到最终的26.7°C。

8.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

8.5.1 stm32l4xx_hal_conf.h

此文件位于“07-温湿度实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的I2C功能,所以我们宏定义中打开I2C相关的。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_UART_MODULE_ENABLED     // UART
112 #define HAL_DMA_MODULE_ENABLED      // DMA  
113 #define HAL_ADC_MODULE_ENABLED      // ADC
114 #define HAL_DAC_MODULE_ENABLED      // DAC
115 #define HAL_I2C_MODULE_ENABLED      // I2C

8.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化了串口部分,目的是打印采集到的温湿度数据。

接下来初始化I2C引脚。

在while()循环中,我们每隔500ms采集一次温湿度的值,并且将采集的温湿度值转化成真实值,格式化打印到串口显示。

33 int main(void)
34 {
35   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
36 	// 重置所有外设、flash界面以及系统时钟
37   HAL_Init();
38 
39 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
40   SystemClock_Config();
41 	
42   // 初始化串口USART1
43   MX_USART1_UART_Init();
44   
45   // 初始化I2C2
46   MX_I2C2_Init();
47   
48   // 
49   while (1)
50   {
51     HAL_Delay(500);
52     printf("Temp = %.1f\r\n",SHT20_Convert(SHT20_ReadTemp(),1));
53     printf("RH   = %.1f%%\r\n",SHT20_Convert(SHT20_ReadRH(),0));    
54   }
55 }

8.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

8.5.4 gyu_i2c.c

在讲解i2c代码之前,我们先给大家讲解一下参数Timing,这个值是通过计算得来的,在STM32芯片手册的P1238页有计算公式说明,我们这边偷懒,利用STM32CUBE里面的配置功能,对应SHT20的I2C参数要求。

我们配置I2C时钟为100KHz,Rise Time 300ns,Fall Time 100ns。最终得出Timing值为0x10D05E82。

初始化I2C引脚,选择的I2C2。

36 void MX_I2C2_Init(void)
37 {
38   hi2c2.Instance = I2C2;                // I2C寄存器基础地址,定义为I2C2的
39   hi2c2.Init.Timing = 0x10D05E82;       // 指定I2C_TIMINGR寄存器值,此值必须在I2C初始化之前配置
40   
41   if (HAL_I2C_Init(&hi2c2) != HAL_OK)   // 初始化I2C2
42   {
43     _Error_Handler(__FILE__, __LINE__); // 如果初始化失败,则进入错误处理
44   }
45 }

定义I2C2功能引脚,选择PB13为SCL引脚,PB14为SDA引脚。并且使能GPIOB以及I2C2的时钟。

55 void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
56 {
57   // 使能GPIOB引脚时钟,因为选择的I2C引脚均在PB上
58   __HAL_RCC_GPIOB_CLK_ENABLE();
59   
60   // 定义GPIO结构
61   GPIO_InitTypeDef GPIO_InitStruct;
62   
63   // 判断I2C是否选择的是I2C2
64   if(i2cHandle->Instance==I2C2)
65   {
66     // I2C2引脚配置
67     GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_14;      // 选择PB13为SCL引脚,PB14为SDA引脚
68     GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;             // 外设功能为开漏模式
69     GPIO_InitStruct.Pull = GPIO_PULLUP;                 // 上拉
70     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;  // 高速模式
71     GPIO_InitStruct.Alternate = GPIO_AF4_I2C2;          // 外设引脚选择I2C2
72     HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);             // 初始化IO配置
73 
74     // 使能I2C2时钟
75     __HAL_RCC_I2C2_CLK_ENABLE(); 
76   }
77 }

I2C发送数据的函数。

89 uint8_t HAL_I2C_Send(uint8_t addr , uint8_t *pData, uint16_t len)
90 {
91   // 判断是否存在数据,不存在返回HAL_ERROR
92   if(len == 0 || pData == 0)    
93   {
94     return HAL_ERROR;
95   }
96   
97   // 发送数据,并返回发送状态
98   return HAL_I2C_Master_Transmit(&hi2c2,addr,pData,len,100);
99 }

I2C接收数据的函数。

111 uint8_t HAL_I2C_Read(uint8_t addr, uint8_t *pData, uint16_t len)
112 {
113   // 判断是否存在数据,不存在返回HAL_ERROR
114   if(len == 0 || pData == 0)
115   {
116     return HAL_ERROR;
117   }
118   
119   // 接收数据,并返回接收状态
120   return HAL_I2C_Master_Receive(&hi2c2,addr,pData,len,100);
121 }

8.5.5 gyu_sht20.c

读取SHT20温度的函数,最终返回是温度是采集值,不是真实的温度值。

52 uint16_t SHT20_ReadTemp(void)
53 {
54   uint16_t temp = 0;
55 
56   // 发送“读取温度指令”
57   uint8_t  cmd = SHT20_MEASURE_TEMP_CMD;
58   HAL_I2C_Send(SHT20_WRITE_ADDR,&cmd,1);
59   
60   // 获取温度采集值,3位数据分别为:Data(MSB)、Data(LSB)、CheckSum
61   uint8_t pDATA[3] = {0,0,0};
62   HAL_I2C_Read(SHT20_READ_ADDR,pDATA,3);
63   
64   // 计算出真实的采集值,保留14bit(MSB 8bit、LSB 高6bit)
65   temp = pDATA[0];
66   temp <<= 8;
67   temp += (pDATA[1] & 0xfc);
68   
69   // 返回温度采集值
70   return temp;
71 }

读取SHT20湿度的函数,最终返回是湿度是采集值,不是真实的湿度值。

 81 uint16_t SHT20_ReadRH(void)
 82 {
 83   uint16_t rh = 0;
 84   
 85   // 发送“读取湿度指令”
 86   uint8_t  cmd = SHT20_MEASURE_RH_CMD;
 87   HAL_I2C_Send(SHT20_WRITE_ADDR,&cmd,1);
 88   
 89   // 获取湿度采集值,3位数据分别为:Data(MSB)、Data(LSB)、CheckSum
 90   uint8_t pDATA[3] = {0,0,0};
 91   HAL_I2C_Read(SHT20_READ_ADDR,pDATA,3);
 92  
 93   // 计算出真实的采集值,保留12bit(MSB 8bit、LSB 高4bit)
 94   rh = pDATA[0];
 95   rh <<= 8;
 96   rh += (pDATA[1] & 0xf0);
 97   
 98   // 返回湿度采集值
 99   return rh;
100 }

SHT20软件复位函数,工程中没有使用此函数。

void SHT20_SoftReset(void)
{
  // 发送SHT20软件复位指令
  uint8_t  cmd = SHT20_MEASURE_RH_CMD;
  HAL_I2C_Send(SHT20_WRITE_ADDR,&cmd,1);
}

温湿度转换函数,用于将采集到的温湿度采集值,转化成真实的温湿度值。

127 float SHT20_Convert(uint16_t value,uint8_t isTemp)
128 {
129   float tmp = 0.0;
130   // 判断本次需要转换的值是温度还是湿度
131   if(isTemp)
132   {
133     tmp = -46.85 + (175.72* value)/(1 << 16);     // 温度值转换,公式:T = -46.85 + 175.72*(S/2^16)
134   }
135   else
136   {
137     tmp = -6 + (125.0 *value)/(1<<16);            // 湿度值转换,公式:RH = -6.00 + 125.00*(S/2^16)
138   }
139   return tmp;
140 }

9 08-RGB实验

10 09-红外接收实验

红外接收实验,是利用开发板上的红外接收传感器,去获取遥控器按下的信号,红外传感器获取到这个信号后,会转成一段PWM波形从它的DATA引脚输出。此时我们利用STM32的定时器捕获功能,就可以获取到这个PWM波形所携带的信息,以此判断遥控器按下的是哪个按键。

10.1 STM32L476 定时器捕获简介

以下部分为TIM2 / TIM3 / TIM4 / TIM5这四个定时器的介绍。

1.通用定时器简介:

通用定时器由一个由可编程预分频器驱动的16位或32位自动重载计数器组成。它们可用于各种目的,包括测量输入信号的脉冲长度(输入捕获)或生成输出波形(输出比较和PWM)。

使用定时器预分频器和RCC时钟控制器预分频器,可以将脉冲长度和波形周期从几微秒调制到几毫秒。定时器完全独立,不共享任何资源。

2.通用定时器功能

•16位(TIM3,TIM4)或32位(TIM2和TIM5)上,下,上/下自动重载计数器。

•16位可编程预分频器,用于分频(也“在运行中”)计数器时钟

频率由1到65535之间的任何因子组成。

•最多4个独立频道:

- 输入捕获

- 输出比较

- PWM生成(边缘和中心对齐模式)

- 单脉冲模式输出

•同步电路,用外部信号控制定时器并互连

几个计时器。

•以下事件的中断/ DMA生成:

- 更新:计数器溢出/下溢,计数器初始化(通过软件或

内部/外部触发器)

- 触发事件(计数器启动,停止,初始化或通过内部/外部触发计数)

- 输入捕获

- 输出比较

•支持增量(正交)编码器和霍尔传感器电路进行定位

目的

•外部时钟或逐周期电流管理的触发输入

3.通用定时器捕获模式

在输入捕捉模式下,捕捉/比较寄存器(TIMx_CCRx)用于在相应ICx信号检测到转换后锁存计数器的值。 发生捕获时,会设置相应的CCXIF标志(TIMx_SR寄存器),如果使能了中断或DMA请求,则可以发送它们。 如果在CCxIF标志已经为高电平时发生捕获,则设置过捕获标志CCxOF(TIMx_SR寄存器)。 CCxIF可以通过软件将其写入0或读取存储在TIMx_CCRx寄存器中的捕获数据来清除。 将其写入0时,CCxOF将被清除。

10.1.1 遥控器协议说明

遥控器使用的协议,被称为NEC码,NEC的编码方式如下所示,这个部分大家需要理解清楚,方便后续代码的阅读。

NEC码高低电平位定义如下:

一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平),一个逻辑 1 传输需要 2.25ms(560us脉冲+1680us 低电平)。

NEC数据格式为:

引导码、用户地址码、用户地址反码、数据码、数据反码。

引导码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,用户地址码、用户地址反码、数据码、数据反码均是8 位数据格式。  

当按键被持续按下时,每隔108ms重新发送一次此数据,所以我们可以利用计时超过108ms的方式,来计算按键持续按下的次数(代码中是判断的110ms)。

10.2 硬件设计

选择STM32L4引脚PC6用来捕获红外传感器HS0038的DATA引脚输出的PWM波。

10.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 将SW1拨到DBG端,SW2拨到MCU。
  4. 使用Keil打开基础实验 09-红外线接收实验工程。
  5. 使用Xshell打开Jlink虚拟出的COM口
  6. 下载程序,并完成功能测试。

10.4 实验验证

下载完成后,我们按下遥控器上的任意按键,可以看到LCD上将显示如下,irBtnVal代表的是键值,irBtnCnt代表是按键被按下的次数,irBtnInfo代表按键的图标或者定义。

10.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

10.5.1 stm32l4xx_hal_conf.h

此文件位于“09-红外线接收实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的I2C功能,所以我们宏定义中打开I2C相关的。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_UART_MODULE_ENABLED     // UART
112 #define HAL_DMA_MODULE_ENABLED      // DMA  
113 #define HAL_ADC_MODULE_ENABLED      // ADC
114 #define HAL_DAC_MODULE_ENABLED      // DAC
115 #define HAL_I2C_MODULE_ENABLED      // I2C

10.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化LCD的SPI控制引脚,LCD背光引脚,并且初始化LCD的图形控制界面。

然后我们在LCD上打印固定的遥控器按键显示格式,也就是"irBtnVal"、"irBtnCnt"、"irBtnInfo"这几个字符串。

接下来我们初始化我们此实验的重点功能,也就是TIM3定时器。

在while()循环中,我们轮询遥控器的按键信息,一旦有遥控器按下,则在LCD的对应位置,打印按键的信息。

 34 int main(void)
 35 {
 36   irInfo_t irkey = {0,0};
 37   
 38   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
 39 	// 重置所有外设、flash界面以及系统时钟
 40   HAL_Init();
 41 
 42 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
 43   SystemClock_Config();
 44   
 45   // LCD SPI初始化
 46   LCD_GPIO_Init();  // LCD IO控制引脚(例如背光)
 47   MX_SPI1_Init();   // LCD SPI控制引脚
 48   
 49   // 图形界面初始化
 50   GUI_Init();       // GUI界面初始化
 51   GUI_Clear();      // 清屏
 52   
 53   GUI_SetColor(GUI_Crimson);            // 红色字体
 54   GUI_DispStringAt("irBtnVal:",24,24);  // 打印字符串"irBtnVal:"到位置X->24,Y->24
 55   GUI_DispStringAt("irBtnCnt:",24,72);  // 打印字符串"irBtnCnt:"到位置X->24,Y->96
 56   GUI_DispStringAt("irBtnInfo:",24,120);  // 打印字符串"irBtnCnt:"到位置X->24,Y->120
 57 
 58   // 初始化TIM3
 59   MX_TIM3_Init();
 60   
 61   // 
 62   while(1)
 63   {
 64     irkey = IRBNT_POLL();     // 轮训获取IR按键信息
 65     
 66     if(irkey.irBtnVal)        // 如果按键信息存在
 67     {
 68       GUI_DispHexAt(irkey.irBtnVal,144,24,2);     // 打印按键值
 69       GUI_DispHexAt(irkey.irBtnCnt,144,72,2);     // 打印按键计数
 70       
 71       // 打印按键图标或名称
 72       GUI_GotoXY(144,120);      // 指的光标位置
 73       GUI_ClearArea();          // 清除指定位置数据
 74       switch(irkey.irBtnVal)    // 判断按键值,打印相应图标或名称
 75       {
 76         case REMOTE_BTN_SWITCH: GUI_DispString("'switch'"); break;
 77         case REMOTE_BTN_MENU: GUI_DispString("'menu'"); break;
 78         case REMOTE_BTN_MUTE: GUI_DispString("'mute'"); break;
 79         case REMOTE_BTN_MODE: GUI_DispString("'mode'"); break;
 80         case REMOTE_BTN_PLUS: GUI_DispString("'+'"); break;
 81         case REMOTE_BTN_RETURN: GUI_DispString("'return'"); break;
 82         case REMOTE_BTN_REWIND: GUI_DispString("'|<<'"); break;
 83         case REMOTE_BTN_PAUSE: GUI_DispString("'>||'"); break;
 84         case REMOTE_BTN_FASTFORWARD: GUI_DispString("'>>|'"); break;
 85         case REMOTE_BTN_0: GUI_DispString("'0'"); break;
 86         case REMOTE_BTN_LESS: GUI_DispString("'-'"); break;
 87         case REMOTE_BTN_OK: GUI_DispString("'OK'"); break;
 88         case REMOTE_BTN_1: GUI_DispString("'1'"); break;
 89         case REMOTE_BTN_2: GUI_DispString("'2'"); break;
 90         case REMOTE_BTN_3: GUI_DispString("'3'"); break;
 91         case REMOTE_BTN_4: GUI_DispString("'4'"); break;
 92         case REMOTE_BTN_5: GUI_DispString("'5'"); break;
 93         case REMOTE_BTN_6: GUI_DispString("'6'"); break;
 94         case REMOTE_BTN_7: GUI_DispString("'7'"); break;
 95         case REMOTE_BTN_8: GUI_DispString("'8'"); break;
 96         case REMOTE_BTN_9: GUI_DispString("'9'"); break;
 97       }
 98     }
 99   }
100 }

10.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

10.5.4 gyu_irc

初始化定时器TIM3,首先配置TIM3的时钟为1MHz(也就是1us),我们设置它向上自动装载,并且设置自动装载值为10000,通过计算可以知道装满一次需要10ms。

然后我们需要设置输入捕获的参数,我们配置上升沿下降沿都捕获,并且设置8个时钟周期的滤波(防止误识别)。

最后使能TIM3的中断,并且开始捕获TIM3的通道1(也就是PC6引脚)。

59 void MX_TIM3_Init(void)
60 {
61   htim3.Instance = TIM3;                            // 通用定时器3
62   htim3.Init.Prescaler = 80-1;                      // TIM3 80预分频器(APB2总线),80MHz / 80 = 1MHz(1us)
63   htim3.Init.CounterMode = TIM_COUNTERMODE_UP;      // 向上计数器
64   htim3.Init.Period = 10000;                        // 自动装载值设为10000,装满一次 10000 * 1us = 10ms
65   htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;// 不分频
66   htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 自动加载使能
67   if (HAL_TIM_IC_Init(&htim3) != HAL_OK)            // 初始化TIM3,出错则进入错误处理函数
68   {
69     _Error_Handler(__FILE__, __LINE__);
70   }
71   
72   // 初始化TIM3输入捕获参数
73   TIM_IC_InitTypeDef sConfigIC;                     // 定义输入捕获结构体(IC:Input capture)
74   sConfigIC.ICPolarity = TIM_ICPOLARITY_BOTHEDGE;   // 上升沿下降沿都捕获
75   sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 配置为TI1
76   sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;           // 不分频
77   sConfigIC.ICFilter = 0x03;                        // IC1F=0011 8个定时器时钟周期滤波
78   // 初始化TIM3 CH1通道,出错则进入错误处理函数
79   if (HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1) != HAL_OK)
80   {
81     _Error_Handler(__FILE__, __LINE__);
82   }
83   
84   HAL_TIM_Base_Start_IT(&htim3);                // 使能更新中断(也就是TIM_IT_UPDATE)
85   HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);   // 开始捕获TIM3 CH1
86 }

使能GPIOC以及TIM3的时钟,并且配置PC6为TIM3的通道1。

 96 void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
 97 {
 98   GPIO_InitTypeDef GPIO_Initure;
 99   __HAL_RCC_TIM3_CLK_ENABLE();            // 使能TIM3时钟
100   __HAL_RCC_GPIOC_CLK_ENABLE();           // 开启GPIOC时钟
101   
102   GPIO_Initure.Pin = GPIO_PIN_6;          // PC6
103   GPIO_Initure.Mode = GPIO_MODE_AF_PP;    // 推挽输出
104   GPIO_Initure.Pull = GPIO_PULLUP;        // 上拉
105   GPIO_Initure.Speed = GPIO_SPEED_HIGH;   // 高速模式
106   GPIO_Initure.Alternate = GPIO_AF2_TIM3; // PC6配置为TIM3通道1
107   HAL_GPIO_Init(GPIOC, &GPIO_Initure);
108   
109   HAL_NVIC_SetPriority(TIM3_IRQn, 10, 0); // 设置TIM3中断优先级
110   HAL_NVIC_EnableIRQ(TIM3_IRQn);          // 使能TIM3中断
111 }

定时器周期中断回调函数,TIM3的自动装载值装满一次,进入一次此回调(本工程配置的参数是10ms进入一次)。

我们判断是否已经接收到引导码(根据引导码标志位判断),一旦接收到引导码,我们认为已经开始了一次NEC数据的接收。

如果是接收到引导码之后,第一次进入此函数,那么我们使能记录遥控器按键值的标志位,也就是代表接收到了一次NEC数据(不管数据对错)。

如果进入的次数少于11次,则继续增加计数,当计数值等于11时(也就是从接收到引导码已经过去至少110ms时),我们认为一次NEC的数据获取已经完成,此时清除周期回调的计数值,并且删除引导码标志位(下次再进到这个函数时,只有新的引导码数据到来,才会进行新的数据处理)。

122 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
123 {
124   if(htim->Instance == TIM3)
125   {
126     if(irStatus & IR_STATUS_BootCode)   // 如果接收到引导码
127     {
128       irStatus &= ~IR_STATUS_Rising;    // 删除上升沿标记(以防止本次出错,确保下次采集流程正确)
129       
130       if(tim3Cnt == 0)                  // 如果是第一次进入(计数为0)  
131       {
132         irStatus |= IR_STATUS_BtnInfo;  // 记录已经获取到IR按键信号(遥控器按键值)
133       }
134       if((tim3Cnt & 0X0F) < 11)         // 进入回调少于11次
135       {
136         tim3Cnt++;                      // 计数值自加
137       }
138       else                              // 超过11次,代表一次采集超时(不论是否成功)
139       {
140         irStatus &= ~IR_STATUS_BootCode;// 删除引导码标记
141         tim3Cnt = 0;                    // 清除计数值
142       }
143     }
144   }
145 }

处理TIM3 CH1(PC6引脚)捕获的数据,这边要记得初始化中我们使能了上升沿下降沿都捕获数据。

当有边沿捕获到来,我们判断此时PC6引脚的电平。

如果此时是高电平,则代表刚刚的是上升沿,此时我们使能上升沿标志位,并且清空定时器计数值。

如果此时是低电平,且上升沿标识位被置位,则我们根据获取到的计数值(也就是上一次高电平的持续时间)来判断本段PWM波代表的含义。

155 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
156 {
157   // 判断是否为TIM3 CH1捕获产生的回调
158   if((htim->Instance == TIM3) && (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1))
159   {
160     if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6))  // 获取PC6引脚电平,如果是高电平,则代表是上升沿捕获
161     {
162       __HAL_TIM_SET_COUNTER(&htim3, 0);     // 清除TIM3定时器计数值
163       irStatus |= IR_STATUS_Rising;         // 标记上升沿捕获
164     }
165     else  //如果是低电平,则代表是下降沿触发
166     {
167       tim3Val = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1);   // 读取TIM3 CH1定时器计数值
168       
169       if(irStatus & IR_STATUS_Rising)       // 存在上升沿标记,我们比较定时器计数值
170       {
171         if(tim3Val > 260 && tim3Val < 860)  // 高电平持续560us代表bit 0,范围560us±300us
172         {
173           irRecData <<= 1;  // 左移一位
174           irRecData |= 0;   // bit位赋值0
175         }
176         else if(tim3Val > 1380 && tim3Val < 1980)	// 高电平持续1680us代表bit 1,范围1680us±300us
177         {
178           irRecData <<= 1;  // 左移一位
179           irRecData |= 1;   // bit位赋值1
180         }
181         else if(tim3Val > 2200 && tim3Val < 2800)	// 高电平持续2500us代表本次按键结束,范围2500us±300us
182         {
183           irCnt++;          // 按键次数新增1
184           tim3Cnt = 0;      // 清除计数值
185         }
186         else if(tim3Val > 4200 && tim3Val < 4800)	// 高电平持续4500us代表新的按键,范围4500us±300us
187         {
188           irStatus |= IR_STATUS_BootCode;   // 标记引导码
189           irCnt = 0;                        // 有新的按键到来,清除按键计数
190         }
191       }
192       
193       irStatus &= ~IR_STATUS_Rising;        // 清除上升沿标记
194     }
195   }
196 }

轮询当前的按键信息。如果按键值标识存在,则代表有按键被按下,接着判断地址码以及数据数据正确,并将最终的按键数据添加到irinfo中留给应用层调用。

206 irInfo_t IRBNT_POLL(void)
207 {
208   irInfo_t irinfo = {0,0};  // 定义按键信息结构体
209   uint8_t bcode, dcode;     // 定义引导码正反编码
210   uint8_t bvalue, dvalue;   // 定义按键值正反编码
211   
212   if(irStatus & IR_STATUS_BtnInfo)    // 如果获取到按键值
213   {
214     bcode = irRecData >> 24;          // 地址码
215     dcode = (irRecData >> 16) & 0xff;	// 地址码反编码
216 
217     if((bcode == (uint8_t)~dcode) && bcode == REMOTE_DEVICE_ID) // 判断地址码是否正确
218     {
219       bvalue = irRecData >> 8;        // 按键值
220       dvalue = irRecData;             // 按键值反编码
221       
222       if(bvalue == (uint8_t)~dvalue)  // 判断按键值是否正确
223       {
224         irinfo.irBtnVal = bvalue;     // 将按键值赋给irinfo.irBtnVal
225       }
226     }
227     
228     irinfo.irBtnCnt = irCnt;          // 将按键此处赋给irinfo.irBtnCnt
229   }
230 
231   return irinfo;  // 返回按键信息
232 }

11 10-串口打印实验

串口打印实验,给大家展示的是如何配置STM32L476一个有效的硬件串口功能,并且顺带给大家介绍了如何去配置一个格式化打印函数printf()。

11.1 STM32L476 UART简介

USART主要功能:

•全双工异步通信

•NRZ标准格式(标记/空格)

•可配置的过采样方法16或8,以提供速度和速度之间的灵活性

时钟容差

•通用可编程发送和接收波特率高达10 Mbit / s时

时钟频率为80 MHz,过采样为8

•双时钟域允许:

- USART功能和从停止模式唤醒

- 独立于PCLK重新编程的便捷波特率编程

•自动波特率检测

•可编程数据字长(7,8或9位)

•可编程数据顺序,具有MSB优先或LSB优先移位

•可配置的停止位(1或2个停止位)

•同步模式和时钟输出,用于同步通信

•单线半双工通信

•使用DMA进行持续通信

•使用集中式DMA将接收/发送的字节缓冲在保留的SRAM中

•发送器和接收器的独立使能位

•独立的信号极性控制,用于发送和接收

•可交换Tx / Rx引脚配置

•调制解调器和RS-485收发器的硬件流控制

•通信控制/错误检测标志

•奇偶校验控制:

- 传输奇偶校验位

- 检查接收数据字节的奇偶校验

•带有标志的14个中断源

•多处理器通信

如果地址不匹配,USART进入静音模式。

•从静音模式唤醒(通过空闲线路检测或地址标记检测)

11.2 硬件设计

选择STM32L4引脚PA9和PA10作为串口,当我们将拨码开关SW1拨到USB一端时,此串口通过CH340芯片转成USB接口,用于向电脑上打印一些调试信息。

11.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 将SW1拨到USB端,SW2拨到MCU。
  4. 使用Keil打开基础实验 10-串口打印实验工程。
  5. 使用Xshell打开miniUSB虚拟出的COM口
  6. 下载程序,并完成功能测试。

11.4 实验验证

下载完成后,我们打开miniUSB虚拟出的COM口,可以看到串口周期性的打印计数值。

11.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

11.5.1 stm32l4xx_hal_conf.h

此文件位于“09-红外线接收实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的串口功能,所以我们宏定义中打开UART相关的。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_DMA_MODULE_ENABLED      // DMA
112 #define HAL_UART_MODULE_ENABLED     // UART

11.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化串口UASRT1。

在while()循环中,我们每隔100ms通过格式化输出"TimeCount = xx:xx:xx"。

34 int main(void)
35 {
36   uint32_t hour = 0;
37   uint32_t minute = 0;
38   uint32_t second = 0;
39 
40   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
41 	// 重置所有外设、flash界面以及系统时钟
42   HAL_Init();
43 
44 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
45   SystemClock_Config();
46 	
47   // 初始化串口USART1
48   MX_USART1_UART_Init();
49   
50   // 
51   while (1)
52   {
53     // 模拟时钟计时,这边的1s实际只是100ms
54     HAL_Delay(100);   // 100ms延时
55     printf("TimeCount = %02d:%02d:%02d\r\n",hour,minute,second);    // 格式化输出"TimeCount = xx:xx:xx"
56 
57     // 时分秒计数
58     second++;
59     if(second == 60)
60     {
61       second = 0;
62       minute++;
63     }
64     if(minute == 60)
65     {
66       minute = 0;
67       hour++;
68     }
69     if(hour == 24)
70     {
71       hour = 0;
72     }
73   }
74 }

11.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

11.5.4 gyu_usartc

串口初始化函数,配置串口协议:波特率115200,数据位8位,停止位1位,无校验位,无流控制。

37 void MX_USART1_UART_Init(void)
38 {
39   // 配置串口参数
40   huart1.Instance = USART1;                     // UART寄存器基础地址,定义为USART1的
41   huart1.Init.BaudRate = 115200;                // 串口波特率为115200
42   huart1.Init.WordLength = UART_WORDLENGTH_8B;  // 串口数据位为8位
43   huart1.Init.StopBits = UART_STOPBITS_1;       // 串口停止位为1位
44   huart1.Init.Parity = UART_PARITY_NONE;        // 串口无校验位
45   huart1.Init.Mode = UART_MODE_TX_RX;           // 串口模式,TX和RX作用
46   huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;  // 串口无流控制
47   huart1.Init.OverSampling = UART_OVERSAMPLING_16;              // 16位过采样
48   huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;     // 1位过采样禁能
49   huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; // 没有串口高级功能初始化
50   
51   // 串口初始化
52   if (HAL_UART_Init(&huart1) != HAL_OK)
53   {
54     _Error_Handler(__FILE__, __LINE__);         // 如果初始化失败,进入错误处理任务
55   }
56 
57 }

配置串口硬件,使能GPIOA以及USART1的时钟,配置PA9和PA10为串口的TX及RX引脚。

67 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
68 {
69   // 定义GPIO结构体
70   GPIO_InitTypeDef GPIO_InitStruct;
71   
72   // 判断选择的是否为USART1
73   if(uartHandle->Instance==USART1)
74   {
75     // 使能GPIOA引脚时钟(因为选择的TX和RX分别为PA9和PA10)
76     __HAL_RCC_GPIOA_CLK_ENABLE();
77     
78     // 使能USART1时钟
79     __HAL_RCC_USART1_CLK_ENABLE();
80   
81     // GPIO配置
82     GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;     // 选择USART1的TX和RX引脚(TX:PA9,RX:PA10)
83     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;           // 推挽输出
84     GPIO_InitStruct.Pull = GPIO_PULLUP;               // 上拉
85     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;// 引脚频率5-80MHz
86     GPIO_InitStruct.Alternate = GPIO_AF7_USART1;      // 配置为USART1
87     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);           // 初始化引脚
88   }
89 }

配置fputc()函数,用于格式化打印,当我们进行了如下代码配置,就可以调用printf()函数去格式化打印调试信息。

 99 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
100 
101 PUTCHAR_PROTOTYPE
102 {
103   // 配置格式化输出到串口USART1
104   HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
105  
106   return ch;
107 }

12 11-串口中断实验

13 12-串口DMA实验

14 13-TFT显示屏实验

15 14-二维码显示实验

16 15-RNG随机发生器实验

RNG实验给大家展示一下随机数的生成,在这个实验中,除了main.c以及gyu_rng.c两个关键文件外,我们还需要注意一下gyu_util.c中系统时钟配置,具体的原因,请大家查看下面的工程介绍。

16.1 STM32L476 随机发生器简介

随机发生器定义:

RNG是一个真正的随机数发生器,它基于模拟噪声源连续提供32位熵样本。它可以被应用程序用作活动熵源,以构建符合NIST的确定性随机比特生成器(DRBG)。 RNG真随机数发生器已根据德国AIS-31标准进行了验证。

熵:热力学中表征物质状态的参量之一,用符号S表示,其物理意义是体系混乱程度的度量。

真随机数发生器生成条件:

•RNG时钟rng_clk = 48 MHz

•AHB时钟rng_hclk = 60 MHz

由于真随机数发生器生成条件的要求,本次实验的gyu_util.c文件中有关系统时钟的配置,和之前的实验有所不同,请大家仔细查看一下该文件。

16.2 硬件设计

使用STM32L476内部RNG随机数发生器。

16.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 将SW1拨到USB端,SW2拨到MCU。
  4. 使用Keil打开基础实验 10-串口打印实验工程。
  5. 使用Xshell打开miniUSB虚拟出的COM口
  6. 下载程序,并完成功能测试。

16.4 实验验证

下载完成后,我们打开miniUSB虚拟出的COM口,可以看到串口周期性的打印计数值。

16.5 源码详解

本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。

16.5.1 stm32l4xx_hal_conf.h

此文件位于“09-红外线接收实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的串口功能,所以我们宏定义中打开UART相关的。

103 // 使能的宏
104 #define HAL_MODULE_ENABLED          // 芯片
105 #define HAL_FLASH_MODULE_ENABLED    // Flash
106 #define HAL_PWR_MODULE_ENABLED      // 电源
107 #define HAL_RCC_MODULE_ENABLED      // 时钟
108 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
109 
110 #define HAL_GPIO_MODULE_ENABLED     // GPIO
111 #define HAL_DMA_MODULE_ENABLED      // DMA
112 #define HAL_UART_MODULE_ENABLED     // UART

16.5.2 main.c

main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。

接下来我们初始化串口UASRT1。

在while()循环中,我们每隔100ms通过格式化输出"TimeCount = xx:xx:xx"。

34 int main(void)
35 {
36   uint32_t hour = 0;
37   uint32_t minute = 0;
38   uint32_t second = 0;
39 
40   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
41 	// 重置所有外设、flash界面以及系统时钟
42   HAL_Init();
43 
44 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
45   SystemClock_Config();
46 	
47   // 初始化串口USART1
48   MX_USART1_UART_Init();
49   
50   // 
51   while (1)
52   {
53     // 模拟时钟计时,这边的1s实际只是100ms
54     HAL_Delay(100);   // 100ms延时
55     printf("TimeCount = %02d:%02d:%02d\r\n",hour,minute,second);    // 格式化输出"TimeCount = xx:xx:xx"
56 
57     // 时分秒计数
58     second++;
59     if(second == 60)
60     {
61       second = 0;
62       minute++;
63     }
64     if(minute == 60)
65     {
66       minute = 0;
67       hour++;
68     }
69     if(hour == 24)
70     {
71       hour = 0;
72     }
73   }
74 }

16.5.3 gyu_util.c

时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。

主要包含了三个部分的初始化配置。

1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。

2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。

3.外设时钟配置,选择外设时钟来源。

为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。

基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。

 49 void SystemClock_Config(void)
 50 {
 51   RCC_OscInitTypeDef RCC_OscInitStruct;     // 定义RCC内部/外部振荡器结构体
 52   RCC_ClkInitTypeDef RCC_ClkInitStruct;     // 定义RCC系统,AHB和APB总线时钟配置结构体
 53   RCC_PeriphCLKInitTypeDef PeriphClkInit;   // 定义RCC扩展时钟结构体
 54   
 55   // 配置LSE驱动器功能为低驱动能力
 56   __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);
 57 
 58   // 初始化CPU,AHB和APB总线时钟
 59   RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE
 60                               |RCC_OSCILLATORTYPE_LSE;        // 设置需要配置的振荡器为HSI、HSE、LSE
 61   // 配置HSE
 62   RCC_OscInitStruct.HSEState = RCC_HSE_ON;                    // 激活HSE时钟(开发板外部为8MHz)
 63   // 配置LSE
 64   RCC_OscInitStruct.LSEState = RCC_LSE_ON;                    // 激活LSE时钟(32.768KHz,低驱动)
 65   // 配置HSI
 66   RCC_OscInitStruct.HSIState = RCC_HSI_ON;                    // 激活HSI时钟
 67   RCC_OscInitStruct.HSICalibrationValue = 16;                 // 配置HSI为16MHz     
 68   // 配置PLL
 69   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 70   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 71   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 72   RCC_OscInitStruct.PLL.PLLN = 20;                            // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz
 73   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,160/7 = 22.857143MHz
 74   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz
 75   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,160/2 = 80MHz
 76   // RCC时钟配置,出错则进入错误处理函数
 77   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CPU,AHB和APB总线时钟
 83   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 84                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 85   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,80MHz
 86   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,80/1 = 80MHz
 87   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,80/1 = 80MHz
 88   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,80/1 = 80MHz
 89   // RCC时钟配置,出错则进入错误处理函数
 90   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)
 91    {
 92     _Error_Handler(__FILE__, __LINE__);
 93   }
 94 
 95   // 初始化外设时钟
 96   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
 97                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
 98                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC;  // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC
 99   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,80MHz
100   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,80MHz
101   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
102   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,80MHz
103   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
104   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义
105   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
106   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
107   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
108   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
109   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
111   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
112   // 外设时钟配置,出错则进入错误处理函数
113   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
114   {
115     _Error_Handler(__FILE__, __LINE__);
116   }
117 
118   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
119   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置系统定时器中断时间,配置为HCLK的千分频
125   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
126 
127   // 配置系统定时器,配置为HCLK
128   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
129 
130   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
131   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
132 }

16.5.4 gyu_usartc

串口初始化函数,配置串口协议:波特率115200,数据位8位,停止位1位,无校验位,无流控制。

37 void MX_USART1_UART_Init(void)
38 {
39   // 配置串口参数
40   huart1.Instance = USART1;                     // UART寄存器基础地址,定义为USART1的
41   huart1.Init.BaudRate = 115200;                // 串口波特率为115200
42   huart1.Init.WordLength = UART_WORDLENGTH_8B;  // 串口数据位为8位
43   huart1.Init.StopBits = UART_STOPBITS_1;       // 串口停止位为1位
44   huart1.Init.Parity = UART_PARITY_NONE;        // 串口无校验位
45   huart1.Init.Mode = UART_MODE_TX_RX;           // 串口模式,TX和RX作用
46   huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;  // 串口无流控制
47   huart1.Init.OverSampling = UART_OVERSAMPLING_16;              // 16位过采样
48   huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;     // 1位过采样禁能
49   huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; // 没有串口高级功能初始化
50   
51   // 串口初始化
52   if (HAL_UART_Init(&huart1) != HAL_OK)
53   {
54     _Error_Handler(__FILE__, __LINE__);         // 如果初始化失败,进入错误处理任务
55   }
56 
57 }

配置串口硬件,使能GPIOA以及USART1的时钟,配置PA9和PA10为串口的TX及RX引脚。

67 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
68 {
69   // 定义GPIO结构体
70   GPIO_InitTypeDef GPIO_InitStruct;
71   
72   // 判断选择的是否为USART1
73   if(uartHandle->Instance==USART1)
74   {
75     // 使能GPIOA引脚时钟(因为选择的TX和RX分别为PA9和PA10)
76     __HAL_RCC_GPIOA_CLK_ENABLE();
77     
78     // 使能USART1时钟
79     __HAL_RCC_USART1_CLK_ENABLE();
80   
81     // GPIO配置
82     GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;     // 选择USART1的TX和RX引脚(TX:PA9,RX:PA10)
83     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;           // 推挽输出
84     GPIO_InitStruct.Pull = GPIO_PULLUP;               // 上拉
85     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;// 引脚频率5-80MHz
86     GPIO_InitStruct.Alternate = GPIO_AF7_USART1;      // 配置为USART1
87     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);           // 初始化引脚
88   }
89 }

配置fputc()函数,用于格式化打印,当我们进行了如下代码配置,就可以调用printf()函数去格式化打印调试信息。

 99 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
100 
101 PUTCHAR_PROTOTYPE
102 {
103   // 配置格式化输出到串口USART1
104   HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
105  
106   return ch;
107 }

17 16-RTC实时时钟实验