NBDK-L4:基础实验教程

来自谷雨文档中心
跳转至: 导航搜索

目录

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-串口DMA 串口通过DMA的方式接收和发送数据 串口使用,DMA方式
实验13-TFT显示屏 TFT彩屏打印英文、数据、中文、图片 TFT彩屏使用,SPI接口使用
实验14-二维码显示 TFT彩屏打印二维码 TFT彩屏使用,二维码生成
实验15-RNG随机发生器 配置RNG,打印随机数 RNG随机发生器使用
实验16-RTC实时时钟 配置RTC,打印实时时钟 实时时钟使用
实验17-定时器中断 配置定时器中断功能 定时器TIM2中断使用
实验18-独立看门狗 配置独立看门狗,展示看门狗复位 独立看门狗使用
实验19-窗口看门狗 配置窗口看门狗,展示看门狗复位 窗口看门狗使用
实验20-外部Flash 外部flash芯片W25Q80读写数据 SPI接口使用,W25Q80使用
实验21-内部Flash 内部未被程序占用的flash空闲内存临时存储数据 内部flash读写数据
实验22-串口升级(bootloader) 串口升级的bootloader程序,位于flash bank1 IAP使用(bootloader)
实验23-串口升级(app) 串口升级的app程序,位于flash bank2 IAP使用(app)
实验24-低功耗待机 配置STM32L4为低功耗待机模式 待机模式使用
实验25-低功耗停止 配置STM32L4为低功耗停止模式 停止模式使用
实验26-低功耗串口 配置STM32L4为低功耗停止模式下串口的使用 停止模式及串口使用

1.2 工程目录简介

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

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

NBDK-TAB-MPath.png

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

Drivers:

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

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

Inc:

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

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

NBDK-KEIL-C++.png
NBDK-KEIL-Paths.png

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就是我们工程的主文件。
NBDK-KEIL-RunToMain.png
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。

NBDK-SCH-LED.png

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高电平时马达起振。

NBDK-SCH-MOTOR.png

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

请参照实验01中的介绍。

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

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高电平时蜂鸣器发出蜂鸣声。

NBDK-SCH-BUZZER.png

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

请参照实验01中的介绍。

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

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映射图。

NBDK-DS-EXTI.png

由上面的映射图可以知道,多个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作为按键的控制引脚。

NBDK-SCH-BUTTON.png

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口分别打印如下:

NBDK-XSHELL-BTN.png

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

请参照实验01中的介绍。

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

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采集引脚。

NBDK-SCH-ADCLIGHT.png

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数据。 红色字体部分,是正常的室内光照强度时采集的电压;绿色字体部分,是打开手机手电筒照射光敏二极管时采集的电压。可以看到明显的电压差值。

NBDK-XSHELL-ADCLIGHT.png

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

请参照实验01中的介绍。

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

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引脚。

NBDK-SCH-DAC.png

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采集的偏移量导致的。

NBDK-XSHELL-DAC.png

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

请参照实验01中的介绍。

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

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引脚。

NBDK-SCH-SHT20.png

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。

NBDK-XSHELL-SHT20.png

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

请参照实验01中的介绍。

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

8.5.4 gyu_i2c.c

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

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

NBDK-DS-SHT20.png
NBDK-CUBE-SHT20.png

初始化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软件复位函数,工程中没有使用此函数。

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

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

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 低电平)。

NBDK-INTER-NEC1.jpg

NEC数据格式为:

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

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

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

NBDK-INTER-NEC.jpg

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_RED);            // 红色字体
 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

请参照实验01中的介绍。

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

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接口,用于向电脑上打印一些调试信息。

NBDK-SCH-UART-USB.png

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口,可以看到串口周期性的打印计数值。

NBDK-XSHELL-UARTPRINTF.png

11.5 源码详解

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

11.5.1 stm32l4xx_hal_conf.h

此文件位于“实验10-串口打印\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

请参照实验01中的介绍。

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

11.5.4 gyu_usart.c

串口初始化函数,配置串口协议:波特率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-串口中断

上一章中,已经给大家简单的展示了如何去配置一个串口,并且格式化输出数据。这一章给大家带来串口中断实验,顾名思义,此实验是使用中断的方式去接收和打印串口数据。

12.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进入静音模式。

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

12.2 硬件设计

选择STM32L4引脚PA9和PA10作为串口,当我们将拨码开关SW1拨到USB一端时,此串口通过CH340芯片转成USB接口,用于同PC端做一些串口数据通信。

NBDK-SCH-UART-USB.png

12.3 实验准备

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

12.4 实验验证

下载完成后,我们打开miniUSB虚拟出的COM口,按下复位按键,会打印“Uart ISR demo”。我们随意输入一些数据,可以看到STM32串口打印出相同的数据。

NBDK-XSHELL-UASRTISR.png

12.5 源码详解

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

12.5.1 stm32l4xx_hal_conf.h

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

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

在这边大家也许会有疑问,我们这个实验不是串口中断吗,为什么要打开DMA宏定义,这个其实是stm32l4xx_hal_uart.c这个库文件的问题,在这个库文件中,它并有像我们用户使用串口那样去区分是使用串口中断还是DMA,所以只要我们使用到串口的宏定义,在不修改串口库文件的情况下,就必须打开DMA的宏定义,否则编译出错。

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

12.5.2 main.c

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

接下来我们初始化串口,并且打印欢迎语"Uart ISR demo"。

在while()循环中,我们判断STM32L4是否接收到串口助手发送的串口数据,如果接收到数据,则回显打印到串口助手。

38 int main(void)
39 {
40   uint16_t msgLen = 0;
41   
42   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
43 	// 重置所有外设、flash界面以及系统时钟
44   HAL_Init();
45 
46 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
47   SystemClock_Config();
48 	
49   // 初始化串口USART1
50   MX_USART1_UART_Init();
51   // 初始化USART1中断isrCfg结构
52   HAL_UARTISR_Init();
53   
54   printf("Uart ISR demo");    // 展示printf函数
55   
56   // 
57   while (1)
58   {
59     // 轮训是否存在串口数据
60     if(HAL_UART_Poll())
61     {
62       msgLen = HAL_UART_RxBufLen();   // 读取当前接收缓存区中有效的数据长度
63       // 超过100字节长度的部分不读取
64       if(msgLen > APP_BUF_LEN)
65       {
66         msgLen = APP_BUF_LEN;
67       }
68       msgLen = HAL_UART_Read(app_buf,msgLen); // 读取缓冲区数据
69       HAL_UART_Write(app_buf,msgLen);         // 通过串口TX打印显示
70     }
71   }
72 }

12.5.3 gyu_util.c

请参照实验01中的介绍。

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

12.5.4 gyu_usart_isr.c

此文件中的函数,都是留给用户使用的(留给用户的串口API接口函数),函数名比较明朗,这边简单说一下各个函数的功能。

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

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

串口初始化函数,初始化串口时钟及串口引脚所在的GPIOA时钟,配置串口引脚硬件功能,使能串口中断。

 76 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
 77 {
 78   // 定义GPIO结构体
 79   GPIO_InitTypeDef GPIO_InitStruct;
 80   
 81   // 判断选择的是否为USART1
 82   if(uartHandle->Instance==USART1)
 83   {
 84     // 使能GPIOA引脚时钟(因为选择的TX和RX分别为PA9和PA10)
 85     __HAL_RCC_GPIOA_CLK_ENABLE();
 86     
 87     // 使能USART1时钟
 88     __HAL_RCC_USART1_CLK_ENABLE();
 89     
 90     // GPIO配置
 91     GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;     // 选择USART1的TX和RX引脚(TX:PA9,RX:PA10)
 92     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;           // 推挽输出
 93     GPIO_InitStruct.Pull = GPIO_PULLUP;               // 上拉
 94     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;// 引脚频率5-80MHz
 95     GPIO_InitStruct.Alternate = GPIO_AF7_USART1;      // 配置为USART1
 96     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);           // 初始化引脚
 97     
 98     // 配置USART1中断优先级
 99     HAL_NVIC_SetPriority(USART1_IRQn, 10, 0);
100     HAL_NVIC_EnableIRQ(USART1_IRQn);
101   }
102 }

初始化串口数据isrCfg结构,并且开始串口数据接收。

112 void HAL_UARTISR_Init(void)
113 {
114   UartIsr_Init(&huart1);
115   HAL_UART_StartRece();
116 }

串口打印函数,串口TX采样中断方式打印数据。

127 void HAL_UART_Write(uint8_t *pData,uint16_t size)
128 {
129   if(size == 0)
130   {
131     return;
132   }
133   if(pData == 0)
134   {
135     return;
136   }
137   HAL_UART_Transmit_IT(&huart1,pData,size);
138 }

启动串口接收的函数,串口RX采用中断方式接收数据。

148 void HAL_UART_StartRece(void)
149 {
150   HAL_UART_Receive_IT(&huart1,UartIsr_GetBuf(),RECE_BUF_MAX_LEN);
151 }

串口轮询函数,用于处理接收一包数据的超时时间。

161 uint8_t HAL_UART_Poll(void)
162 {
163   return UartIsr_Poll();
164 }

读取串口数据的函数,并且返回读取到的数据的长度。

175 uint16_t HAL_UART_Read(uint8_t *buf,uint16_t size)
176 {
177   return UartIsr_Read(buf,size);
178 }

返回当前串口RX缓冲区中的数据长度。

188 uint16_t HAL_UART_RxBufLen(void)
189 {
190   return UartIsr_Avail();
191 }

串口接收中断的回调函数,在这个回调函数中,我们需要开启串口接收中断,用于接收下一包的串口数据。

199 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
200 {
201   HAL_UART_StartRece();
202 }

12.5.5 gyu_uasrt_isr_ex.c

此文件是gyu_uasrt_usr.c中函数的底层处理函数,为了方便大家使用,我们将其从中分离出来。这部分大家根据自己实际情况选择看或者不看,熟悉ti芯片的朋友,可以看出这部分的串口数据处理是copy的ti CC25XX芯片的。

首先是有关串口中断的结构体定义,并且根据结构体参数的具体功能,分别RX缓冲区数据长度。

30 // 串口RX缓冲区数据长度
31 #define HAL_UART_ISR_RX_AVAIL() \
32   (isrCfg.rxTail >= isrCfg.rxHead) ? \
33   (isrCfg.rxTail - isrCfg.rxHead) : \
34   (RECE_BUF_MAX_LEN - isrCfg.rxHead + isrCfg.rxTail)
35 
36 static uartISRCfg_t     isrCfg;

初始化串口中断结构体的isrCfg。

47 void UartIsr_Init(UART_HandleTypeDef*huart)
48 {
49   isrCfg.rxHead = 0;
50   isrCfg.rxTail = 0;
51   isrCfg.rxTick = 0;
52   isrCfg.rxShdw = 0;
53   hUart = huart;
54 }

获取串口RX缓冲区中的数据,由gyu_usart_isr.c的HAL_UART_StartRece()函数中调用,并指向RX缓冲区。

64 uint8_t* UartIsr_GetBuf(void)
65 {
66   return isrCfg.rxBuf;        // 指向串口RX缓冲区
67 }

读取串口RX缓冲区中的数据的函数。

78 uint16_t UartIsr_Read(uint8_t *buf, uint16_t len)
79 {
80   uint16_t cnt = 0;
81   
82   while ((isrCfg.rxHead != isrCfg.rxTail) && (cnt < len))
83   {
84     *buf++ = isrCfg.rxBuf[isrCfg.rxHead++];
85     if (isrCfg.rxHead >= RECE_BUF_MAX_LEN)
86     {
87       isrCfg.rxHead = 0;
88     }
89     cnt++;
90   }
91 
92   return cnt;
93 }

串口RX缓冲区数据长度获取函数。

103 uint16_t UartIsr_Avail(void)
104 {
105   return HAL_UART_ISR_RX_AVAIL();
106 }

串口轮询函数,主要目的是检测串口RX缓冲区数据,一个是是否超过了设置的缓冲区大小,另一个是是否超时了。

116 uint8_t UartIsr_Poll(void)
117 {
118   uint8_t evt = 0;
119   uint16_t cnt = 0;
120   volatile uint16_t tail = RECE_BUF_MAX_LEN - hUart->RxXferCount;
121   
122   if(isrCfg.rxHead != tail)
123   {
124     if(tail != isrCfg.rxTail)
125     {
126       isrCfg.rxTail = tail;
127       
128       if(isrCfg.rxTick == 0)
129       {
130         isrCfg.rxShdw = HAL_GetTick();
131       }
132       isrCfg.rxTick = HAL_UART_ISR_IDLE;
133     }
134     else if(isrCfg.rxTick)
135     {
136       uint32_t Tick = HAL_GetTick();
137       
138       uint32_t delta = Tick >= isrCfg.rxShdw ?
139                        (Tick - isrCfg.rxShdw ): 
140                        (Tick + (UINT32_MAX - isrCfg.rxShdw));
141 
142       if (isrCfg.rxTick > delta)
143       {
144         isrCfg.rxTick -= delta;
145       }
146       else
147       {
148         isrCfg.rxTick = 0;
149       }
150     }
151     cnt = UartIsr_Avail();
152   }
153   else
154   {
155     isrCfg.rxTick = 0;
156   }
157   
158   if (cnt >= HAL_UART_ISR_FULL)
159   {
160     evt = HAL_UART_RX_FULL;
161   }
162   else if (cnt && !isrCfg.rxTick)
163   {
164     evt = HAL_UART_RX_TIMEOUT;
165   }
166   
167   return evt;
168 }

13 实验12-串口DMA

实验11中,我们给大家介绍了串口使用中断的方式去接收和打印数据。这一章给大家带来串口DMA方式的数据交互。

13.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进入静音模式。

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

13.2 硬件设计

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

NBDK-SCH-UART-USB.png

这边给大家简单介绍一下STM32串口USART1和DMA的对应关系,从下面的图中我们可以看到USART1_TX对应DMA的CH4,USART1_RX对应DMA的CH5。

NBDK-DS-USARTDMA.png

13.3 实验准备

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

13.4 实验验证

下载完成后,我们打开miniUSB虚拟出的COM口,按下复位按键,会打印“Uart DMA demo”。我们随意输入一些数据,可以看到STM32串口打印出相同的数据。

NBDK-XSHELL-UASRTDMA.png

13.5 源码详解

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

13.5.1 stm32l4xx_hal_conf.h

此文件位于“实验12-串口DMA\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

13.5.2 main.c

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

接下来我们初始化串口,并且初始化DMA。并且打印欢迎语“USART DMA demo”。

在while()循环中,检测当前串口RX缓冲区中是否有数据(串口是否接收到数据),如果有,则打印到串口显示。

38 int main(void)
39 {
40   uint16_t msgLen = 0;
41 
42   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
43 	// 重置所有外设、flash界面以及系统时钟
44   HAL_Init();
45 
46 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
47   SystemClock_Config();
48 	
49   // 初始化DMA
50   MX_DMA_Init();
51   
52   // 初始化串口USART1
53   MX_USART1_UART_Init();
54   // 串口DMA初始化
55   HAL_UARTDMA_Init();
56   
57   printf("USART DMA demo");   // 展示printf
58   
59   // 
60   while (1)
61   {
62     // 轮训串口是否有数据
63     if(HAL_UART_Poll())
64     {
65       msgLen = HAL_UART_RxBufLen();   // 获取当前串口缓冲区的数据长度
66       // 超过100字节长度的部分不获取
67       if(msgLen > APP_BUF_LEN)
68       {
69         msgLen = APP_BUF_LEN;
70       }
71       msgLen = HAL_UART_Read(app_buf,msgLen);   // 获取串口数据
72       HAL_UART_Write(app_buf,msgLen);   // 将获取的数据通过串口TX打印显示
73     }
74   }
75 }

13.5.3 gyu_util.c

请参照实验01中的介绍。

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

13.5.4 gyu_dma.c

DMA初始化函数,使能DMA1时钟,并且使能DMA1 CH4和CH5中断。

31 void MX_DMA_Init(void) 
32 {
33   // 使能DMA1时钟 
34   __HAL_RCC_DMA1_CLK_ENABLE();
35 
36   // DMA1 CH4中断配置
37   HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 10, 0);
38   HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
39   
40   // DMA1 CH5中断配置
41   HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 10, 0);
42   HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
43 }

13.5.5 gyu_usart_dma.c

串口初始化函数,初始化串口协议部分。

66 void MX_USART1_UART_Init(void)
67 {
68   // 配置串口参数
69   huart1.Instance = USART1;                     // UART寄存器基础地址,定义为USART1的
70   huart1.Init.BaudRate = 115200;                // 串口波特率为115200
71   huart1.Init.WordLength = UART_WORDLENGTH_8B;  // 串口数据位为8位
72   huart1.Init.StopBits = UART_STOPBITS_1;       // 串口停止位为1位
73   huart1.Init.Parity = UART_PARITY_NONE;        // 串口无校验位
74   huart1.Init.Mode = UART_MODE_TX_RX;           // 串口模式,TX和RX作用
75   huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;  // 串口无流控制
76   huart1.Init.OverSampling = UART_OVERSAMPLING_16;              // 16位过采样
77   huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;     // 1位过采样禁能
78   huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; // 没有串口高级功能初始化
79   
80   // 串口初始化
81   if (HAL_UART_Init(&huart1) != HAL_OK)
82   {
83     _Error_Handler(__FILE__, __LINE__);         // 如果初始化失败,进入错误处理任务
84   }
85 }

串口DMA初始化函数,除了使能串口TX及RX引脚时钟,配置这两个引脚状态之外,最主要的还是配置这两个引脚的DMA功能,分别对应我们刚刚在DMA1中初始化的CH4和CH5。

 95 void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
 96 {
 97   // 定义GPIO结构体
 98   GPIO_InitTypeDef GPIO_InitStruct;
 99   
100   // 判断选择的是否为USART1
101   if(uartHandle->Instance==USART1)
102   {
103     // 使能GPIOA引脚时钟(因为选择的TX和RX分别为PA9和PA10)
104     __HAL_RCC_GPIOA_CLK_ENABLE();
105     
106     // 使能USART1时钟
107     __HAL_RCC_USART1_CLK_ENABLE();
108     
109     // GPIO配置
110     GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;     // 选择USART1的TX和RX引脚(TX:PA9,RX:PA10)
111     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;           // 推挽输出
112     GPIO_InitStruct.Pull = GPIO_PULLUP;               // 上拉
113     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;// 引脚频率5-80MHz
114     GPIO_InitStruct.Alternate = GPIO_AF7_USART1;      // 配置为USART1
115     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);           // 初始化引脚
116   }
117   
118   // 配置DMA1 CH5
119   hdma_usart1_rx.Instance = DMA1_Channel5;
120   hdma_usart1_rx.Init.Request = DMA_REQUEST_2;
121   hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
122   hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
123   hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
124   hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
125   hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
126   hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
127   hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
128   if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
129   {
130     _Error_Handler(__FILE__, __LINE__);
131   }
132   // 关联DMA1 CH5 和 串口RX
133   __HAL_LINKDMA(uartHandle,hdmarx,hdma_usart1_rx);
134   
135   
136   // 配置DMA1 CH4
137   hdma_usart1_tx.Instance = DMA1_Channel4;
138   hdma_usart1_tx.Init.Request = DMA_REQUEST_2;
139   hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
140   hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
141   hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
142   hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
143   hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
144   hdma_usart1_tx.Init.Mode = DMA_NORMAL;
145   hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
146   if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
147   {
148     _Error_Handler(__FILE__, __LINE__);
149   }
150   // 关联DMA1 CH4 和 串口TX
151   __HAL_LINKDMA(uartHandle,hdmatx,hdma_usart1_tx);
152   
153   // 配置USART1中断
154   HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
155   HAL_NVIC_EnableIRQ(USART1_IRQn);
156 }

串口DMA初始化,用于在串口DMA模式下去接收数据(真正读取串口RX缓冲区的函数)。通过UartDma_Init()函数和gyu_usart_dma_ex.c的回调函数关系,将获取到的串口RX数据,交给gyu_usart_dma_ex.c去处理。

167 void HAL_UARTDMA_Init(void)
168 {
169   HAL_UART_Receive_DMA(&huart1,UartDma_Init(uart_dma_send,USART1),RECE_BUF_MAX_LEN);
170 }

串口TX打印数据的函数,调用gyu_usart_dma_ex.c中的UartDma_Write()完成数据打印,最终是通过下面的uart_dma_send()函数打印数据。

181 void HAL_UART_Write(uint8_t*buf,uint16_t len)
182 {
183   UartDma_Write(buf,len);
184 }

串口DMA打印的函数。

195 static void uart_dma_send(uint8_t* buf,uint16_t len)
196 {
197   HAL_UART_Transmit_DMA(&huart1,buf,len); 
198 }

读取串口数据的函数,不是真正从串口RX获取数据的函数,而是从gyu_usart_dma_ex.c这个文件获取保存好的数据。

209 uint16_t HAL_UART_Read(uint8_t*buf,uint16_t len)
210 {
211   return UartDma_Read(buf,len);
212 }

串口轮询函数,主要是判断串口RX是否有数据,是否接收超时,以及是否有需要往外打印的串口数据。

222 uint8_t HAL_UART_Poll(void)
223 {
224   return  UartDma_Poll();
225 }

用于获取串口RX缓冲器数据长度。

235 uint16_t HAL_UART_RxBufLen(void)
236 {
237   return UartDma_Avail();
238 }

13.5.6 gyu_usart_dma_ex.c

这个文件主要是串口数据的获取和处理,大家有兴趣可以自行阅读,需要一定的代码阅读能力,并且对于串口理解的比较清晰。

获取当前DMA接收缓存区正在操作的位置。

51 static uint16_t findTail(void)
52 {
53   uint16_t idx = dmaCfg.rxHead;
54   
55   do
56   {
57     if (!DMA_NEW_RX_BYTE(idx))
58     {
59       break;
60     }
61     
62     if (++idx >= RECE_BUF_MAX_LEN)
63     {
64       idx = 0;
65     }
66   } while (idx != dmaCfg.rxHead);
67 
68   return idx;
69 }

串口DMA初始化函数,除了串口dma结构体dmaCfg的初始化参数,主要就是dmaSendCb这个回调函数,以及串口句柄hDmaUart赋值。

79 uint8_t* UartDma_Init(sendData_cb sendCb ,USART_TypeDef* hUart)
80 {
81   memset(dmaCfg.buf,0xff,RECE_BUF_MAX_LEN<<1);
82   dmaCfg.rxHead = 0;
83   dmaCfg.rxTail = 0;
84   dmaCfg.rxTick = 0;
85   dmaCfg.rxShdw = 0;
86   dmaCfg.txSel  = 0;
87   
88   dmaCfg.txIdx[0] = 0;
89   dmaCfg.txIdx[1] = 0;
90   
91   dmaCfg.rxTick = 0;   //delay 1ms
92   
93   dmaCfg.txDMAPending = FALSE;
94   dmaCfg.txShdwValid = FALSE;
95   dmaSendCb = sendCb;
96   hDmaUart = hUart;
97   return (uint8_t*)dmaCfg.buf;
98 }

串口DMA获取数据,只是数据的赋值和空间释放,不是真正的从出纳卡RX缓冲区获取数据。

109 uint16_t UartDma_Read(uint8_t *buf, uint16_t len)
110 {
111   uint16_t cnt;
112 
113   for (cnt = 0; cnt < len; cnt++)
114   {
115     if (!DMA_NEW_RX_BYTE(dmaCfg.rxHead))
116     {
117       break;
118     }
119     *buf++ = DMA_GET_RX_BYTE(dmaCfg.rxHead);
120     
121     //释放占用空间
122     DMA_CLR_RX_BYTE(dmaCfg.rxHead);
123 
124     if (++(dmaCfg.rxHead) >= RECE_BUF_MAX_LEN)
125     {
126       dmaCfg.rxHead = 0;
127     }
128   }
129 
130   return cnt;
131 }

串口DMA打印函数,通用不是真正的TX打印函数,只是将当前要打印的数据分配好,并且使能打印的标识。

142 uint16_t UartDma_Write(uint8_t *buf, uint16_t len)
143 {
144   uint16_t cnt;
145   uint8_t txSel;
146   uint8_t txIdx;
147 
148   // Enforce all or none.
149   if ((len + dmaCfg.txIdx[dmaCfg.txSel]) > SENT_BUF_MAX_LEN)
150   {
151     return 0;
152   }
153 
154   txSel = dmaCfg.txSel;
155   txIdx = dmaCfg.txIdx[txSel];
156 
157   for (cnt = 0; cnt < len; cnt++)
158   {
159     dmaCfg.txBuf[txSel][txIdx++] = buf[cnt];
160   }
161   
162   if (txSel != dmaCfg.txSel)
163   {
164     txSel = dmaCfg.txSel;
165     txIdx = dmaCfg.txIdx[txSel];
166 
167     for (cnt = 0; cnt < len; cnt++)
168     {
169       dmaCfg.txBuf[txSel][txIdx++] = buf[cnt];
170     }
171   }
172 
173   dmaCfg.txIdx[txSel] = txIdx;
174 
175   if (dmaCfg.txIdx[(txSel ^ 1)] == 0)
176   {
177     // TX DMA is expected to be fired
178     dmaCfg.txDMAPending = TRUE;
179   }
180 
181   return cnt;
182 }

获取当前串口RX缓存区的数据长度。

192 extern uint16_t UartDma_Avail(void)
193 {
194   uint16_t cnt = 0;
195   
196   if (DMA_NEW_RX_BYTE(dmaCfg.rxHead))
197   {
198     uint16_t idx;
199     
200     for (idx = 0; idx < RECE_BUF_MAX_LEN; idx++)
201     {
202       if (DMA_NEW_RX_BYTE(idx))
203       {
204         cnt++;
205       }
206     }
207   }
208   
209   return cnt;
210 }

串口轮询函数,分别处理串口RX和TX。

RX部分:判断是否接收超时,接收的数据是否超过我们设置的缓冲区大小,两种情况都会返回对应的事件标识。

TX部分:判断当前是否有数据需要打印,如果有,则打印。

220 uint8_t UartDma_Poll(void)
221 {
222   uint16_t cnt = 0;
223   uint8_t evt = 0;
224 
225   if(DMA_NEW_RX_BYTE(dmaCfg.rxHead))
226   {
227     uint16_t tail = findTail();
228     
229     // If the DMA has transferred in more Rx bytes, reset the Rx idle timer.
230     if (dmaCfg.rxTail != tail)
231     {
232       dmaCfg.rxTail = tail;
233 
234       if (dmaCfg.rxTick == 0)
235       {
236         dmaCfg.rxShdw = HAL_GetTick();
237       }
238       
239       dmaCfg.rxTick = HAL_UART_DMA_IDLE;
240     }
241     else if (dmaCfg.rxTick)
242     {
243       uint32_t Tick = HAL_GetTick();
244       uint32_t delta = Tick >= dmaCfg.rxShdw ?
245                                (Tick - dmaCfg.rxShdw ): 
246                                (Tick + (UINT32_MAX - dmaCfg.rxShdw));
247       
248       if (dmaCfg.rxTick > delta)
249       {
250         dmaCfg.rxTick -= delta;
251         dmaCfg.rxShdw = Tick;
252       }
253       else
254       {
255         dmaCfg.rxTick = 0;
256       }
257     }
258     cnt = UartDma_Avail();
259   }
260   else
261   {
262     dmaCfg.rxTick = 0;
263   }
264 
265   if (cnt >= HAL_UART_DMA_FULL)
266   {
267     evt = HAL_UART_RX_FULL;
268   }
269   else if (cnt && !dmaCfg.rxTick)
270   {
271     evt = HAL_UART_RX_TIMEOUT;
272   }
273   
274   if (dmaCfg.txShdwValid)
275   {
276     uint32_t decr = HAL_GetTick() - dmaCfg.txShdw;;
277 	
278     if (decr > dmaCfg.txTick)
279     {
280       // No protection for txShdwValid is required
281       // because while the shadow was valid, DMA ISR cannot be triggered
282       // to cause concurrent access to this variable.
283       dmaCfg.txShdwValid = FALSE;
284     }
285   }
286   
287   if (dmaCfg.txDMAPending && !dmaCfg.txShdwValid)
288   {
289     // Clear the DMA pending flag
290     dmaCfg.txDMAPending = FALSE;
291     //Send  data
292     if(dmaSendCb)
293     {
294       dmaSendCb(dmaCfg.txBuf[dmaCfg.txSel],dmaCfg.txIdx[dmaCfg.txSel]);
295     }
296     dmaCfg.txSel ^= 1;
297   }
298 
299   return evt;
300 }

串口TX打印完成的回调函数,主要功能就是判断串口是否还没有需要打印的数据,如果有,则继续打印。

310 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
311 {
312   if(huart->Instance == hDmaUart)
313   {
314     // Indicate that the other buffer is free now.
315     dmaCfg.txIdx[(dmaCfg.txSel ^ 1)] = 0;
316     
317     // Set TX shadow
318     dmaCfg.txShdw = HAL_GetTick();
319     dmaCfg.txShdwValid = TRUE;
320 
321     // If there is more Tx data ready to go, re-start the DMA immediately on it.
322     if (dmaCfg.txIdx[dmaCfg.txSel])
323     {
324       // UART TX DMA is expected to be fired
325       dmaCfg.txDMAPending = TRUE;
326     }
327   }
328 }

14 实验13-TFT显示屏

TFT显示屏实验,给大家展示了显示屏的驱动实现,主要是针对显示屏打印英文、汉字、数字以及图片。

14.1 STM32L476 SPI简介

SPI接口说明:

SPI接口可用于使用SPI协议与外部设备通信。SPI模式可由软件选择。 器件复位后,默认选择SPI Motorola模式。

串行外设接口(SPI)协议支持与外部设备的半双工,全双工和单工同步串行通信。 接口可以配置为主设备,在这种情况下,它为外部从设备提供通信时钟(SCK)。 该接口还能够在多主机配置中运行。

SPI主要功能:

•主或从操作

•三条线路上的全双工同步传输

•两条线路上的半双工同步传输(带双向数据线)

•两条线上的单工同步传输(带有单向数据线)

•4位至16位数据大小选择

•多主机模式功能

•8个主模式波特率预分频器,最高可达fPCLK / 2。

•从机模式频率高达fPCLK / 2。

•主机和从机的硬件或软件的NSS管理:动态变化

主/从操作

•可编程时钟极性和相位

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

•具有中断功能的专用传输和接收标志

•SPI总线忙状态标志

•SPI Motorola支持

•硬件CRC功能,可靠通信:

- CRC值可以在Tx模式下作为最后一个字节发送

- 自动CRC错误检查最后接收的字节

•主模式故障,具有中断功能的溢出标志

•CRC错误标志

•两个具有DMA功能的32位嵌入式Rx和Tx FIFO

•SPI TI模式支持

14.2 TFT简介

暂缺,有需要开发驱动的朋友,可以参照我们的源码和TFT使用手册。

14.3 硬件设计

选择STM32L4:

PA4:LCD_CS,TFT屏SPI控制接口CS引脚

PA5:SPI_SCK,TFT屏SPI控制接口SCK引脚

PA6:SPI_MISO,TFT屏SPI控制接口MISO引脚

PA7:SPI_MOSI,TFT屏SPI控制接口MOSI引脚

PB0:LCD_BLED,TFT屏背光控制引脚

NBDK-SCH-TFT.png

14.4 实验准备

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

14.5 实验验证

下载完成后,我们分别按下4个按键,可以看到分别会打印 图片、汉字、英文、以及十进制和16进制的数字。

14.6 源码详解

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

14.6.1 stm32l4xx_hal_conf.h

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

此例程我们主要给大家展示STM32L4驱动TFT显示屏,使用的是SPI接口,所以我们使能SPI的宏定义。

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

14.6.2 main.c

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

接下来我们初始化串口和按键相关,按键用于展示TFT显示的功能(串口未使用,之前的工程集成下来的)。

然后我们分别初始化了驱动TFT工作的软硬件部分,也就是硬件的SPI以及软件的GUI(图形管理,这边只是指的TFT打印的)。并且打印了一个logo图片。

在while()循环中我们检测是否有按键按下,如果有,则调试对应的功能。

41 int main(void)
42 {
43   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
44 	// 重置所有外设、flash界面以及系统时钟
45   HAL_Init();
46 
47 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
48   SystemClock_Config();
49   
50 	// 初始化USART1
51 	MX_USART1_UART_Init();
52   
53   // 初始化按键引脚
54 	MX_KEY_Init();
55   
56   // 注册按钮回调函数
57   KEY_RegisterCb(AppKey_cb);
58   
59   // LCD SPI初始化
60   LCD_GPIO_Init();  // LCD IO控制引脚(例如背光)
61   MX_SPI1_Init();   // LCD SPI控制引脚
62   
63   // 图形界面初始化
64   GUI_Init();       // GUI界面初始化
65   GUI_Clear();      // 清屏
66   
67   // 打印logo到位置X->0,Y->0
68   GUI_DrawBitmap(&bmLogo,0,0);
69   
70   // 
71   while(1)
72   {
73 		KEY_Poll();   // 按键轮训,监测是否有按键被按下
74   }
75 }

按键回调函数,如果按下UP(S1)按键,则打印一个按键的小图片;如果按下DOWN(S3)按键,则打印"谷雨物联"这四个汉字;如果按下LEFT(S4)按键,则打印"Temp:35°C"这个英文和数字组成的字符串;如果按下RIGHT(S2)按键,则分别打印0x5566这个数字的16进制和10进制的值。

 86 void AppKey_cb(uint8_t key)
 87 {
 88   // 如果有相应按键被按下,则串口打印调试信息
 89   if(key & KEY_UP)
 90   {
 91     GUI_DrawBitmap(&bmKey,168,120);      // 打印按键图标到位置X->168,Y->120
 92   }
 93   if(key & KEY_DOWN)
 94   {
 95     GUI_SetColor(GUI_RED);              // 红色字体
 96     GUI_SetBkColor(GUI_GREEN);          // 绿色背景
 97     GUI_GotoXY(24,108);                 // 光标指到X->24,Y->108
 98     GUI_DispString("谷雨物联");         // 打印汉字“谷雨物联”
 99   }
100   if(key & KEY_LEFT)
101   {
102     GUI_SetColor(GUI_RED);                // 红色字体
103     GUI_SetBkColor(GUI_YELLOW);           // 金色背景
104     GUI_DispStringAt("Temp:35℃",24,132); // 打印字符串“Temp:35℃”到位置X->24,Y->132
105   }
106   if(key & KEY_RIGHT)
107   {
108     uint16_t value = 0x5566;        // 定义value值0x5566(十进制:21862)
109     GUI_SetColor(GUI_YELLOW);       // 黄色字体
110     GUI_SetBkColor(GUI_BLUE);       // 蓝色背景
111     GUI_DispHexAt(value,24,156,1);  // 打印16进制值 0x5566到位置X->24,Y->156
112     GUI_DispDecAt(value,24,180);    // 打印10进制值 21862到位置X->24,Y->180
113   }
114 }

14.6.3 gui.c等

TFT驱动文件,请大家查看谷雨显示接口原理说明

15 实验14-二维码显示

二维码显示实验,是在TFT彩屏实验的基础上,新增了二维码生产的部分。此实验仅供大家参考,我们没有进行深入的二维码协议了解,所以有关二维码部分不做介绍。

15.1 硬件设计

选择STM32L4:

PA4:LCD_CS,TFT屏SPI控制接口CS引脚

PA5:SPI_SCK,TFT屏SPI控制接口SCK引脚

PA6:SPI_MISO,TFT屏SPI控制接口MISO引脚

PA7:SPI_MOSI,TFT屏SPI控制接口MOSI引脚

PB0:LCD_BLED,TFT屏背光控制引脚

NBDK-SCH-TFT.png

15.2 实验准备

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

15.3 实验验证

下载完成后,我们按下S1按键,可以看到显示屏上打印一个二维码,使用微信扫描二维码,会弹出"www.iotxx.com"。

15.4 源码详解

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

15.4.1 stm32l4xx_hal_conf.h

此文件位于“实验14-二维码显示\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4驱动TFT显示屏,使用的是SPI接口,所以我们使能SPI的宏定义。

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

15.4.2 main.c

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

接下来我们初始化串口和按键相关,按键用于展示TFT显示的功能(串口未使用,之前的工程集成下来的)。

然后我们分别初始化了驱动TFT工作的软硬件部分,也就是硬件的SPI以及软件的GUI(图形管理,这边只是指的TFT打印的)。并且打印字符串“*QR CODE SHOW*”。

在while()循环中我们检测是否有按键按下,如果有,则调试对应的功能。

64 int main(void)
65 {
66   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
67 	// 重置所有外设、flash界面以及系统时钟
68   HAL_Init();
69   
70 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
71   SystemClock_Config();
72   
73   // 初始化按键 
74   MX_KEY_Init();
75   
76   // 按键回调函数初始化
77   KEY_RegisterCb(AppKey_cb);
78   
79   // 初始化USART1 
80   MX_USART1_UART_Init();
81   
82   // LCD SPI初始化
83   LCD_GPIO_Init();   // LCD IO控制引脚(例如背光)
84   MX_SPI1_Init();    // LCD SPI控制引脚
85  
86   // GUI界面初始化
87   GUI_Init();
88   GUI_SetColor(GUI_BLUE);
89   GUI_DispString("*QR CODE SHOW*");
90   
91   while (1)
92   {
93     KEY_Poll();
94   }
95 }

按键回调函数,如果按下UP(S1)按键,我们调用二维码处理的函数,最终在TFT彩屏上打印二维码。微信扫描二维码,会弹出"www.iotxx.com"字样。

105 void AppKey_cb(uint8_t key)
106 {
107   HAL_LED_Blink(100,20,1);
108   if(key & KEY_UP)
109   {
110     QRcode   *qrcode;   //2D Code 
111     qrcode= QRcode_encodeString("www.iotxx.com",2, QR_ECLEVEL_L, QR_MODE_8,1);
112     
113     GUI_DispStringAtCL("2D Code Showing",0,72,GUI_Context.colorInfo.bkColor);
114     GUI_DispString("\r\nVersion=2");
115 
116     int XPOSI  = 120 - qrcode->width;
117     int YPOSI  = 156 - qrcode->width;
118 
119     for(int y = 0; y < qrcode->width ; y++)
120     {
121       for(int x = 0 ; x < qrcode->width ; x++)
122       {
123         if(qrcode->data[ y * qrcode->width + x ] & 0x01)
124         {
125           //放大一倍
126           LCD_DrawPixel(XPOSI +(x<<1),YPOSI + (y << 1) + 0,  GUI_LIGHTRED);
127           LCD_DrawPixel(XPOSI + (x<<1)+1,YPOSI +(y << 1) + 0,GUI_LIGHTRED);
128           LCD_DrawPixel(XPOSI + (x<<1),YPOSI +(y << 1) + 1,  GUI_LIGHTRED);
129           LCD_DrawPixel(XPOSI + (x<<1)+1,YPOSI +(y << 1) + 1,GUI_LIGHTRED);
130         }
131         else
132         {
133           //放大一倍
134           //LCD_DrawPixel(x,y,GUI_DEFAULT_BKCOLOR);
135           LCD_DrawPixel(XPOSI + (x<<1),YPOSI +(y << 1) + 0,  GUI_DEFAULT_BKCOLOR);
136           LCD_DrawPixel(XPOSI + (x<<1) +1,YPOSI +(y << 1) + 0,GUI_DEFAULT_BKCOLOR);
137           LCD_DrawPixel(XPOSI + (x<<1),YPOSI +(y << 1) + 1,  GUI_DEFAULT_BKCOLOR);
138           LCD_DrawPixel(XPOSI + (x<<1)+1,YPOSI +(y << 1) + 1,GUI_DEFAULT_BKCOLOR);
139         }
140       }
141     }
142     QRcode_free(qrcode);
143   }
144   if(key & KEY_RIGHT)
145   {
146     //
147   }
148   if(key & KEY_DOWN)
149   {
150     //
151   }
152   if(key & KEY_LEFT)
153   {
154     //
155   }
156 }

15.4.3 gui.c等

TFT驱动文件,请大家查看谷雨显示接口原理说明

15.4.4 qrencode.c等

二维码部分不做介绍。

16 实验15-RNG随机发生器

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

16.1 STM32L476 随机发生器简介

随机发生器定义:

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

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

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

  • RNG时钟rng_clk = 48 MHz
  • AHB时钟rng_hclk = 60 MHz
Icon-info.png
由于真随机数发生器生成条件的要求,本次实验的gyu_util.c文件中有关系统时钟的配置,和之前的实验有所不同,请大家仔细查看一下该文件。
NBDK-DS-RNG.png

16.2 硬件设计

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

16.3 实验准备

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

16.4 实验验证

下载完成后,我们打开miniUSB虚拟出的COM口,每按下一次S1(btn_up),STM32L476都会向串口打印一个随机数。

NBDK-XSHELL-RNG.png

16.5 源码详解

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

16.5.1 stm32l4xx_hal_conf.h

此文件位于“实验15-RNG随机发生器\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的随机发生器生成随机数的功能,所以我们宏定义中打开RNG相关的。

103 #define HAL_MODULE_ENABLED          // 芯片
104 #define HAL_FLASH_MODULE_ENABLED    // Flash
105 #define HAL_PWR_MODULE_ENABLED      // 电源
106 #define HAL_RCC_MODULE_ENABLED      // 时钟
107 #define HAL_CORTEX_MODULE_ENABLED   // NVIC
108 
109 #define HAL_GPIO_MODULE_ENABLED     // GPIO
110 #define HAL_DMA_MODULE_ENABLED      // DMA
111 #define HAL_UART_MODULE_ENABLED     // UART
112 #define HAL_SPI_MODULE_ENABLED      // SPI
113 #define HAL_RNG_MODULE_ENABLED      // RNG随机发生器

16.5.2 main.c

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

接下来我们初始化串口UASRT1、按键、以及TFT显示屏。

接下来是我们这个工程的关键,初始化RNG部分。

在while()循环中,我们调用KEY_Poll()函数去轮询是否有按键被按下。

41 int main(void)
42 {
43   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
44 	// 重置所有外设、flash界面以及系统时钟
45   HAL_Init();
46 
47 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
48   SystemClock_Config();
49   
50 	// 初始化USART1
51 	MX_USART1_UART_Init();
52   
53   // 初始化按键引脚
54 	MX_KEY_Init();
55   
56   // 注册按钮回调函数
57   KEY_RegisterCb(AppKey_cb);
58   
59   // LCD SPI初始化
60   LCD_GPIO_Init();  // LCD IO控制引脚(例如背光)
61   MX_SPI1_Init();   // LCD SPI控制引脚
62   
63   // 图形界面初始化
64   GUI_Init();       // GUI界面初始化
65   GUI_Clear();      // 清屏
66   
67   // 打印logo到位置X->0,Y->0
68   GUI_DrawBitmap(&bmLogo,0,0);
69   
70   // 随机发生器初始化
71   MX_RNG_Init();
72   
73   // 
74   while(1)
75   {
76 		KEY_Poll();   // 按键轮训,监测是否有按键被按下 
77   }
78 }

在按键回调函数中,可以看到,每次S1(key_up)按键被按下,都是调用RNG_Get()函数去获取一次随机数。

89 void AppKey_cb(uint8_t key)
90 {
91   // 如果有相应按键被按下,则串口打印调试信息
92   if(key & KEY_UP)
93   {
94     RNG_Get();    // 获取随机数
95   }
96 }

16.5.3 gyu_util.c

此实验因为真随机数发生器的条件限制,我们配置的时钟和之前的例程有稍许不同。

首先是我们的AHB时钟,此实验配置为60MHz,之前的实验都是80MHz。

其次是此实验我们使用到了MSI(并且配置为RCC_MSIRANGE_11,也就是48MHz),在下面的外设功能配置中,我们选择此48MHz时钟作为RNG的时钟。

 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|RCC_OSCILLATORTYPE_MSI; // 设置需要配置的振荡器为HSI、HSE、LSE、MSI
 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   // 配置MSI
 69   RCC_OscInitStruct.MSIState = RCC_MSI_ON;                    // 激活MSI时钟(内部高频,最高可配置48MHz)
 70   RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11;          // 配置为48MHz
 71   // 配置PLL
 72   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;                // 打开PLL
 73   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;        // 选择HSE时钟作为PLL入口时钟源,8MHz
 74   RCC_OscInitStruct.PLL.PLLM = 1;                             // 配置PLL VCO输入分频为1,8/1 = 8MHz
 75   RCC_OscInitStruct.PLL.PLLN = 15;                            // 配置PLL VCO输入倍增为20,8MHz*15 = 120MHz
 76   RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7;                 // SAI时钟7分频,120/7 = 17.14MHz
 77   RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2;                 // SDMMC、RNG、USB时钟2分频,120/2 = 60MHz
 78   RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2;                 // 系统主时钟分区2分频,120/2 = 60MHz
 79   // RCC时钟配置,出错则进入错误处理函数
 80   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
 81   {
 82     _Error_Handler(__FILE__, __LINE__);
 83   }
 84   
 85   // 初始化CPU,AHB和APB总线时钟
 86   RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
 87                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2
 88   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;             // 配置系统时钟为PLLCLK输入,60MHz
 89   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;                    // AHB时钟为系统时钟1分频,60/1 = 60MHz
 90   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;                     // APB1时钟为系统时钟1分频,60/1 = 60MHz
 91   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;                     // APB2时钟为系统时钟1分频,60/1 = 60MHz
 92   // RCC时钟配置,出错则进入错误处理函数
 93   if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // HCLK=60MHz,Vcore=3.3V,所以选择SW2(FLASH_LATENCY_2)
 94    {
 95     _Error_Handler(__FILE__, __LINE__);
 96   }
 97 
 98   // 初始化外设时钟
 99   PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2
100                               |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1
101                               |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC
102                               |RCC_PERIPHCLK_RNG;// 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC、RNG
103   PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2;     // 配置串口USART1时钟为PCLK2,60MHz
104   PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1;     // 配置串口USART2时钟为PCLK1,60MHz
105   PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI;     // 配置LPUART时钟为HSI,16MHz
106   PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1;         // 配置I2C2时钟为PCLK1,60MHz
107   PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE;       // 配置LPTIM1时钟为LSE,32.768KHz
108   PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1;         // 配置ADC时钟为PLLSAI1,现在为60MHz,下面会重新定义
109   PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE;            // 配置PLLSAI1时钟为HSE,8MHz
110   PeriphClkInit.PLLSAI1.PLLSAI1M = 1;                                 // 配置PLLSAI1分频为1
111   PeriphClkInit.PLLSAI1.PLLSAI1N = 8;                                 // 配置PLLSAI1倍增为8
112   PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7;                     // SAI时钟7分频,64/7 = 9.142857MHz
113   PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2;                     // SDMMC、USB时钟2分频,64/2 = 32MHz
114   PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2;                     // 系统主时钟分区2分频,64/2 = 32MHz             
115   PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK;        // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz
116   PeriphClkInit.RngClockSelection = RCC_RNGCLKSOURCE_MSI;
117   
118   // 外设时钟配置,出错则进入错误处理函数
119   if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
120   {
121     _Error_Handler(__FILE__, __LINE__);
122   }
123 
124   // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz
125   if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
126   {
127     _Error_Handler(__FILE__, __LINE__);
128   }
129 
130   // 配置系统定时器中断时间,配置为HCLK的千分频
131   HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
132 
133   // 配置系统定时器,配置为HCLK
134   HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
135 
136   // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)
137   HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
138 }

16.5.4 gyu_rng.c

初始化RNG。

43 void MX_RNG_Init(void)
44 {
45   hrng.Instance = RNG;
46   if (HAL_RNG_Init(&hrng) != HAL_OK)
47   {
48     _Error_Handler(__FILE__, __LINE__);
49   }
50 }

使能RNG时钟,并且配置和使能RNG中断。

60 void HAL_RNG_MspInit(RNG_HandleTypeDef* hrng)
61 {
62   if(hrng->Instance==RNG)
63   {
64     // 使能RNG时钟
65     __HAL_RCC_RNG_CLK_ENABLE();
66     
67     // 使能NVIC中断及优先级
68     HAL_NVIC_SetPriority(RNG_IRQn, 10, 0);
69     HAL_NVIC_EnableIRQ(RNG_IRQn);
70   }
71 }

随机数的获取函数,在此处打开中断。此处中断开启,会触发HAL_RNG_ErrorCallback()或者HAL_RNG_ReadyDataCallback()回调函数,当有随机数成功生成时,返回HAL_RNG_ReadyDataCallback()回调。

81 void RNG_Get(void)
82 {
83   HAL_RNG_GenerateRandomNumber_IT(&hrng);   // 随机数获取函数(开启中断)
84 }

真随机数成功生成的回调函数,参数random32bit就是生成的随机数,我们利用printf函数将随机数打印到串口。

108 void HAL_RNG_ReadyDataCallback(RNG_HandleTypeDef* hrng, uint32_t random32bit)
109 {
110   printf("%u\r\n",random32bit);   // 将随机数打印到串口
111 }

17 实验16-RTC实时时钟

实时时钟实验,给大家展示一下STM32L4的实时计时功能,我们提供给大家设置当前时间、日期、以及星期的函数接口,并且提供了一个闹钟配置的函数接口。这样就可以完成一个简单的老式闹钟的功能展示。

17.1 STM32L476 RTC时钟简介

简介:

RTC提供自动唤醒功能,可管理所有低功耗模式。

实时时钟(RTC)是一个独立的BCD定时器/计数器。 RTC提供具有可编程报警中断的时间时钟/日历。

RTC还包括具有中断功能的周期性可编程唤醒标志。

两个32位寄存器包含秒,分钟,小时(12或24小时格式),日(星期几),日期(星期几),月和年,以二进制编码的十进制格式(BCD)表示。sub-seconds值也以二进制格式提供。

自动执行28,29(闰年),30天和31天月的补偿。还可以执行夏令时补偿。

其他32位寄存器包含可编程报警亚秒,秒,分钟,小时,日和日期。

数字校准功能可用于补偿晶体振荡器精度的任何偏差。

备份域复位后,所有RTC寄存器都受到保护,以防止可能的寄生写访问。

只要电源电压保持在工作范围内,RTC就不会停止,无论器件状态如何(运行模式,低功耗模式或欠复位)。

RTC主要功能:

•日历,包括亚秒,秒,分钟,小时(12或24格式),日(星期几),日期(日期),月和年。

•可通过软件编程的夏令时补偿。

•带中断功能的可编程报警。可以通过日历字段的任意组合触发警报。

•自动唤醒单元生成周期性标志,触发自动唤醒中断。

•参考时钟检测:可以使用更精确的第二个源时钟(50或60 Hz)来提高日历精度。

•使用亚秒移位功能与外部时钟精确同步。

•数字校准电路(周期性计数器校正):在几秒钟的校准窗口中获得0.95 ppm的精度

•用于事件保存的时间戳功能

•具有可配置滤波器和内部上拉的篡改检测事件

•可屏蔽中断/事件:

- 报警A.

- 闹钟B.

- 唤醒中断

- 时间戳

- 篡改检测

•32个备份寄存器

17.2 硬件设计

选择STM32L4内部RTC实时时钟。

17.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 使用Keil打开基础实验的实验16-RTC实时时钟
  4. 下载程序,并完成功能测试。

17.4 实验验证

下载完成后,可以看到TFT屏幕上打印当前的实时时间、日期以及星期,且10s后会触发闹钟(表现为TFT打印"Alarm"、蜂鸣器哔一声)。

17.5 源码详解

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

17.5.1 stm32l4xx_hal_conf.h

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

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

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI
114 #define HAL_RTC_MODULE_ENABLED      // RTC实时时钟

17.5.2 main.c

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

接下来我们初始化TFT彩屏相关的SPI控制接口,以及GUI图形界面初始化。并且格式化打印一些内容,字体显示为红色的部分。

最后我们初始化RTC时钟,并且调用实现好的有关RTC的函数,去配置当前的时钟、日期、星期、以及闹钟。

在while()循环当中,持续获取当前的时钟、日期等相关信息,并且打印到TFT彩屏上显示。

当检测到闹钟信息,则在TFT彩屏上打印"Alarm"字样指示闹钟,停止蜂鸣器工作,并且让闹钟标识置为0。

 44 int main(void)
 45 {
 46   HAL_StatusTypeDef status;
 47   
 48   RTC_TimeTypeDef sTime;
 49   RTC_DateTypeDef sDate;
 50   
 51   HAL_Init();
 52 
 53   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
 54 	// 重置所有外设、flash界面以及系统时钟
 55 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
 56   SystemClock_Config();
 57   
 58   // LCD SPI初始化
 59   LCD_GPIO_Init();  // LCD IO控制引脚(例如背光)
 60   MX_SPI1_Init();   // LCD SPI控制引脚
 61   
 62   // 图形界面初始化
 63   GUI_Init();       // GUI界面初始化
 64   GUI_Clear();      // 清屏
 65   GUI_SetColor(GUI_RED);            // 红色字体
 66   GUI_SetBkColor(GUI_YELLOW);       // 黄色背景
 67   // 格式化打印如下的内容
 68   GUI_DispStringAt("Time:   :  :",24,24); 
 69   GUI_DispStringAt("Date: 20  /  /",24,72);
 70   GUI_DispStringAt("Week: ",24,120); 
 71   
 72   // 初始化蜂鸣器
 73   Buzzer_Init();
 74   
 75   // RTC初始化
 76   MX_RTC_Init();
 77   RTC_TIME_Set(16,12,30);     // 设置当前时间   时,分,秒
 78   RTC_DATE_Set(2,18,12,25);   // 设置当前日期   星期,年,月,日
 79   RTC_AlarmA_Set(16,12,40,2); // 设置闹钟       时,分,秒,星期
 80   
 81   //
 82   while(1)
 83   {
 84     GUI_SetColor(GUI_CYAN);
 85     
 86     // 获取时分秒,并且打印到TFT屏
 87     status = HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
 88     if(status == HAL_OK)
 89     {
 90       GUI_DispDecAt(sTime.Hours,96,24,2);
 91       GUI_DispDecAt(sTime.Minutes,132,24,2);
 92       GUI_DispDecAt(sTime.Seconds,168,24,2);
 93     }
 94     
 95     // 获取年月日、星期,并且打印到TFT屏
 96     status = HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
 97     if(status == HAL_OK)
 98     {
 99       GUI_DispDecAt(sDate.Year,120,72,2);
100       GUI_DispDecAt(sDate.Month,156,72,2);
101       GUI_DispDecAt(sDate.Date,192,72,2);
102       GUI_DispDecAt(sDate.WeekDay,96,120,2);
103     }
104     
105     status = HAL_ERROR;
106     
107     if(rtcAlarm)
108     {
109       rtcAlarm = 0;
110       GUI_DispStringAt("AlarmA",24,168);  // 显示屏打印闹钟A标志
111       Buzzer_SET(GPIO_PIN_RESET);         // 关闭蜂鸣器
112     }
113   }
114 }

17.5.3 gyu_util.c

请参照实验01中的介绍。

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

17.5.4 gyu_rtc.c

RTC时钟初始化函数,我们配置为24小时模式,禁止RTC输出,并且初始化RTC时钟。

50 void MX_RTC_Init(void)
51 {
52   hrtc.Instance = RTC;                        // 配置为RTC  
53   hrtc.Init.HourFormat = RTC_HOURFORMAT_24;   // 24小时模式
54   hrtc.Init.AsynchPrediv = 127;               // 异步预分频器,固定127(0x7F)
55   hrtc.Init.SynchPrediv = 255;                // 同步预分频器,固定255(0xFF)
56   hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;      // 禁止RTC输出
57   
58   // 初始化RTC时钟
59   if (HAL_RTC_Init(&hrtc) != HAL_OK)
60   {
61     _Error_Handler(__FILE__, __LINE__);
62   }
63 }

RTC初始化函数,使能RTC时钟。

73 void HAL_RTC_MspInit(RTC_HandleTypeDef* hrtc)
74 {  
75   if(hrtc->Instance==RTC)
76   {
77     __HAL_RCC_RTC_ENABLE(); // 使能RTC时钟
78   }
79 }

设置RTC时钟的函数,用于设置当前的时间(分别设置时分秒)。

 91 void RTC_TIME_Set(uint8_t hour, uint8_t min, uint8_t sec)
 92 {
 93   RTC_TimeTypeDef sTime;  // RTC时间结构体
 94   
 95   sTime.Hours = hour;     // 时
 96   sTime.Minutes = min;    // 分
 97   sTime.Seconds = sec;    // 秒
 98   sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;   // 不定义AM和PM,因为24小时模式
 99   sTime.StoreOperation = RTC_STOREOPERATION_RESET;  // 
100   
101   // 设置时钟
102   if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BIN) != HAL_OK)
103   {
104     _Error_Handler(__FILE__, __LINE__);
105   }
106 }

设置RTC日期、星期的函数,用于设置当前的年月日、以及星期。

119 void RTC_DATE_Set(uint8_t week, uint8_t year, uint8_t monty, uint8_t date)
120 {
121   RTC_DateTypeDef sDate;  // RTC日期结构体
122   
123   sDate.WeekDay = week;   // 星期 
124   
125   sDate.Year = year;      // 年
126   sDate.Month = monty;    // 月
127   sDate.Date = date;      // 日
128   
129   // 设置日期
130   if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BIN) != HAL_OK)
131   {
132     _Error_Handler(__FILE__, __LINE__);
133   }
134 }

闹钟时间设置的函数,用于设置闹钟触发的时分秒。

147 void RTC_AlarmA_Set(uint8_t hour, uint8_t min, uint8_t sec, uint8_t week)
148 {
149   RTC_AlarmTypeDef sAlarm;            // RTC闹钟A结构体
150   
151   sAlarm.AlarmTime.Hours = hour;      // 时
152   sAlarm.AlarmTime.Minutes = min;     // 分
153   sAlarm.AlarmTime.Seconds = sec;     // 秒
154   sAlarm.AlarmTime.SubSeconds = 0;    // 亚秒(这边没有作比较,任意设置)
155   
156   sAlarm.AlarmMask = RTC_ALARMMASK_NONE;  // 精确到时分秒、日期或者星期
157   sAlarm.AlarmSubSecondMask = RTC_ALARMSUBSECONDMASK_ALL;   // 亚秒不作比较
158   sAlarm.AlarmDateWeekDaySel = RTC_ALARMDATEWEEKDAYSEL_WEEKDAY; // 比较星期(设置星期或者日期)
159   sAlarm.AlarmDateWeekDay = week;     // 星期
160   
161   sAlarm.Alarm = RTC_ALARM_A;         // 闹钟A
162   
163   // 开启闹钟中断
164   if (HAL_RTC_SetAlarm_IT(&hrtc, &sAlarm, RTC_FORMAT_BIN) != HAL_OK)
165   {
166     _Error_Handler(__FILE__, __LINE__);
167   }
168   
169   HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 10, 0); // 抢占优先级10,子优先级0
170   HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);          // 使能中断 
171 }

闹钟回调函数,当RTC实时时钟计时到达设置的闹钟时间,就会触发此回调,我们在回调中打开蜂鸣器,并且置位闹钟的标识。

181 void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)
182 {
183   Buzzer_SET(GPIO_PIN_SET);           // 蜂鸣器哔一声
184   rtcAlarm = 1;
185 }

18 实验17-定时器中断

定时器中断实验,这个实验大家可以和之前的红外接收实验对照查看,使用的手段是类似的,都是利用定时器的计时功能,不同点在于红外接收用的是捕获,而此实验则是触发中断。

18.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.通用定时器中断触发的条件

配置定时器自动装载值,以及分频系数,使能定时器的中断。当计数值向上或者向下溢出时,定时器中断触发。

计算公式为:Tout=(ARR+1)(PSC+1)/TIMxCLK (us)

18.2 硬件设计

选择STM32L4内部TIM2定时器,展示使用的是开发板上的LED指示灯D3。

NBDK-SCH-LED.png

18.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 使用Keil打开基础实验的实验17-定时器中断。
  4. 下载程序,并完成功能测试。

18.4 实验验证

下载完成后,可以看到LED周期闪烁(1s周期,也就是点亮1s,熄灭1s)。

18.5 源码详解

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

18.5.1 stm32l4xx_hal_conf.h

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

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

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI
114 #define HAL_TIM_MODULE_ENABLED      // TIM定时器

18.5.2 main.c

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

接下来我们分别初始化了LED及TIM2定时器。

32 int main(void)
33 {
34   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
35 	// 重置所有外设、flash界面以及系统时钟
36   HAL_Init();
37 
38 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
39   SystemClock_Config();
40 	
41 	// 初始化LED引脚
42 	LED_Init();
43   
44   // 初始化tim2定时器
45 	MX_TIM2_Init();
46   
47   // 
48   while (1)
49   {
50     
51   }
52 }

18.5.3 gyu_util.c

请参照实验01中的介绍。

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

18.5.4 gyu_tim2it.c

TIM2定时器初始化函数,我们配置TIM2时钟不分频(也就是系统时钟80MHZ),设置TIM2预分频为10000-1(也就是TIM2时钟为80M/10000 = 8000Hz),设置自动装载值为8000(也就是说一次装载满的时间为8000*(1/8000Hz) = 1s)。

这边有总结好的公式:Tout=(ARR+1)(PSC+1)/TIMxCLK (us)。ARR代表预分频,PSC代表自动装载值。

47 void MX_TIM2_Init(void)
48 {
49   htim2.Instance = TIM2;                        // 配置为TIM2
50   htim2.Init.Prescaler = 10000-1;               // ARR+1 = 10000
51   htim2.Init.CounterMode = TIM_COUNTERMODE_UP;  // 向上计数
52   htim2.Init.Period = 8000-1;                   // PSC+1 = 8000
53   htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;  // 不分频,80MHz
54   
55   // 初始化TIM2,出错则进入错误处理函数
56   if (HAL_TIM_Base_Init(&htim2) != HAL_OK)      
57   {
58     _Error_Handler(__FILE__, __LINE__);
59   }
60   
61   HAL_TIM_Base_Start_IT(&htim2);      // 开启TIM2,并且使能中断
62 }

TIM2初始化函数,使能定时器TIM2时钟,并且使能中断。

72 void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
73 {
74   if(htim->Instance == TIM2)
75   {
76     __HAL_RCC_TIM2_CLK_ENABLE();            //使能定时器TIM2
77     HAL_NVIC_SetPriority(TIM2_IRQn, 10, 0); //设置中断优先级10,子优先级0
78     HAL_NVIC_EnableIRQ(TIM2_IRQn);          //使能ITM3中断
79   }
80 }

定时器中断回调函数,当计数值溢出时,触发此回调,我们判断是否是TIM2触发,如果是,则翻转LED的状态。 在此实验中的表现是,LED灯1s改变一次状态(闪烁)。

90 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
91 {
92   if(htim == (&htim2))
93   {
94     LED_TOGGLE();     // 翻转LED状态,1s翻转一次
95   }
96 }

19 实验18-独立看门狗

独立看门狗实验,STM32L4内部自带两个看门狗,分别叫做“独立看门狗(IWDG)”以及“窗口看门狗(WWDG)”。这两个看门狗的使用,将分别在本例程以及下一个例程给大家讲解使用的方式。

19.1 STM32L476 独立看门狗简介

独立看门狗的作用:

独立看门狗外设检测并解决由于软件故障引起的故障,并在计数器达到给定超时值时触发系统复位。

也就是说当程序跑飞的时候,独立看门狗会复位系统。

独立看门狗的时钟:

独立看门狗(IWDG)由其自己的专用低速时钟(LSI)提供时钟,因此即使主时钟发生故障也能保持活动状态。

也就是说独立看门狗完全是独立运行,不受其他部分程序影响。

19.2 硬件设计

选择STM32L4内部独立看门狗,展示使用的是开发板上的LED指示灯D3、以及用于喂狗的按键引脚S2(BTN2 PC2引脚)。

NBDK-SCH-LED.png
NBDK-SCH-BUTTON.png

19.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 使用Keil打开基础实验的实验18-独立看门狗。
  4. 下载程序,并完成功能测试。

19.4 实验验证

下载完成后,可以看到LED周期闪烁,这代表我们的STM32运行程序一直在被复位。当我们将按键S2(RIGHT)按键持续按下时,这个时候LED停止闪烁(保持常亮的状态),说明程序没有再被复位。

19.5 源码详解

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

19.5.1 stm32l4xx_hal_conf.h

此文件位于“实验18-独立看门狗\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

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

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI
114 #define HAL_IWDG_MODULE_ENABLED     // 独立看门狗

19.5.2 main.c

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

接下来我们初始化了LED,并且初始化了IWDG配置、以及控制IWDG喂狗的S2按键引脚。

然后我们设置LED点亮,在LED点亮之前我们延时500ms,用来展示程序是否被独立看门狗重启。

在while()循环中,我们持续监测IWDG控制喂狗的按键引脚电平,如果为低电平(按键被按下),则调用喂狗的函数(喂狗之后,独立看门狗不会再复位程序)。

32 int main(void)
33 {
34   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
35 	// 重置所有外设、flash界面以及系统时钟
36   HAL_Init();
37   
38 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
39   SystemClock_Config();
40 	
41   // 初始化LED引脚
42   LED_Init();
43   
44   // 初始化独立看门狗
45   MX_IWDG_Init();
46   // 初始化独立看门狗用于触发喂狗的S2按键引脚
47   IWDG_FeedIO_Init();
48   
49   // 延时500ms,展示软件是否重启(重启则LED闪烁,不重启则LED常亮)
50   HAL_Delay(500);
51   LED_SET(GPIO_PIN_SET);
52   
53   // 
54   while (1)
55   {
56     // 如果按键RIGHT按下,检测为低电平,则喂狗
57     if(!IWDG_FeedIO_Read())
58     {
59       IWDG_Feed();  // 喂狗
60     }
61   }
62 }

19.5.3 gyu_util.c

请参照实验01中的介绍。

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

19.5.4 gyu_iwdg.c

看门狗初始化函数,我们配置IWDG 32分频(也就是32KHz /32 = 1KHz = 1us),配置重载计数值为1000(也就是装载满一次需要1000*1us = 1s)。

或者大家使用公式计算:Tout=(4*(2^prer)*rlr)/32 (ms)。此例程prer = 3,rlr = 1000。

37 void MX_IWDG_Init(void)
38 {
39   hiwdg.Instance = IWDG;                    // 配置为IWDG
40   hiwdg.Init.Prescaler = IWDG_PRESCALER_32; // 32分频,prer = 3
41   hiwdg.Init.Window = IWDG_WINDOW_DISABLE;  // 
42   hiwdg.Init.Reload = 1000;                 // 重载计数1000,rlr = 1000 
43   if (HAL_IWDG_Init(&hiwdg) != HAL_OK)      // 初始化IWDG,出错则进入错误处理函数
44   {
45     _Error_Handler(__FILE__, __LINE__);
46   }
47 }

独立看门狗喂狗函数,也就是清除计数值。

57 void IWDG_Feed(void)
58 {
59   HAL_IWDG_Refresh(&hiwdg); 	// 喂狗
60 }

初始化按键S2(PC2引脚)为一般上拉输入。

72 void IWDG_FeedIO_Init(void)
73 {
74 	GPIO_InitTypeDef  GPIO_InitStructure;               // 定义引脚参数结构体
75 
76 	__HAL_RCC_GPIOC_CLK_ENABLE();                       // 使能GPIOC时钟
77   
78 	GPIO_InitStructure.Pin= GPIO_PIN_2;                 // 引脚编号为2
79 	GPIO_InitStructure.Mode = GPIO_MODE_INPUT;          // 输入
80 	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW;     // 低频率
81 	GPIO_InitStructure.Pull = GPIO_PULLUP;              // 上拉
82 	HAL_GPIO_Init(GPIOC, &GPIO_InitStructure);          // 初始化PC2
83 }

检测当前S2引脚电平,并且返回当前电平状态。

94 uint8_t IWDG_FeedIO_Read(void)
95 {
96   return HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_2);
97 }

20 实验19-窗口看门狗

窗口看门狗实验,STM32L4内部自带两个看门狗,分别叫做“独立看门狗(IWDG)”以及“窗口看门狗(WWDG)”。这一章节给大家介绍一下窗口看门狗的使用,窗口看门狗相对于独立看门狗的好处在于喂狗的时间精确可控,但是需要使用PCLK1的时钟,而不是独立的时钟源。

20.1 STM32L476 窗口看门狗简介

系统窗口看门狗(WWDG)用于检测软件故障的发生,通常由外部干扰或不可预见的逻辑条件产生,这会导致应用程序放弃其正常序列。

看门狗电路在编程时间段到期时产生MCU复位,除非程序在T6位清零之前刷新向下计数器的内容。如果在向下计数器达到窗口寄存器值之前刷新7位向下计数器值(在控制寄存器中),也会产生MCU复位。 这意味着必须在有限的窗口中刷新计数器。(这一段的意思,就是说窗口看门狗必须在一段时间内喂狗,早了或者晚了都会复位)。

窗口看门狗喂狗时间的计算公式:Twwdg = (T[6:0]+1)/(Pckl1/4096/(2^WDGTB)) (us)

NBDK-DS-WWDG1.png

WWDG时钟从APB时钟预分频,并具有可配置的时间窗口,可对其进行编程以检测异常迟到或早期应用行为。

WWDG最适合需要看门狗在精确定时窗口内作出反应的应用(这句话大概可以理解为,如果不需要精确时间去看门狗复位,就建议使用独立看门狗)。

20.2 硬件设计

选择STM32L4内部窗口看门狗,展示使用的是开发板上的LED指示灯D3。

NBDK-SCH-LED.png

20.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 使用Keil打开基础实验的实验19-窗口看门狗。
  4. 下载程序,并完成功能测试。

20.4 实验验证

下载完成后,可以看到LED保持常亮,说明我们的程序没有被复位。而当我们注释掉HAL_WWDG_EarlyWakeupCallback()回调函数中的喂狗函数HAL_WWDG_Refresh(hwwdg),重新编译下载,可以看到LED灯周期闪烁,说明程序一直被复位(因为没有喂狗)。

20.5 源码详解

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

20.5.1 stm32l4xx_hal_conf.h

此文件位于“实验19-窗口看门狗\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

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

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI
114 #define HAL_WWDG_MODULE_ENABLED     // 窗口看门狗

20.5.2 main.c

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

接下来我们初始化了LED,设置LED点亮,在LED点亮之前我们延时500ms,用来展示程序是否被窗口看门狗重启。

最后我们初始化窗口看门狗。

32 int main(void)
33 {
34   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
35 	// 重置所有外设、flash界面以及系统时钟
36   HAL_Init();
37 
38   // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
39   SystemClock_Config();
40 	
41   // 初始化LED引脚
42   LED_Init();
43   
44   // 延时500ms,展示软件是否重启(重启则LED闪烁,不重启则LED常亮)
45   HAL_Delay(500);
46   LED_SET(GPIO_PIN_SET);
47   
48   // 初始化窗口看门狗
49   MX_WWDG_Init();
50   
51   // 
52   while (1)
53   {
54     
55   }
56 }

20.5.3 gyu_util.c

请参照实验01中的介绍。

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

20.5.4 gyu_iwdg.c

窗口看门狗初始化函数,初始化窗口看门狗时钟8分频(时钟来源PCLK1 80MHz),配置上窗口值0x50,下窗口值默认0x40,并且使能唤醒中断(可以在唤醒中断中喂狗)。

利用窗口看门狗的喂狗时间计算公式,我们分别得到上下窗口值的时间:

上窗口(0x50):33.17ms

下窗口(0x40):26.21ms

46 void MX_WWDG_Init(void)
47 {
48   hwwdg.Instance = WWDG;                    // 配置为WWDG
49   hwwdg.Init.Prescaler = WWDG_PRESCALER_8;  // 8分频,WDGTB = 3
50   hwwdg.Init.Window = 0x50;                 // 上窗口值为0x50
51   hwwdg.Init.Counter = 0x7F;                // 计数CNT为0x7F(最大值)
52   hwwdg.Init.EWIMode = WWDG_EWI_ENABLE;     // 使能唤醒中断
53   if (HAL_WWDG_Init(&hwwdg) != HAL_OK)      // 初始化WWDG,出错则进入错误处理函数
54   {
55     _Error_Handler(__FILE__, __LINE__);
56   }
57 }

窗口看门狗初始化,使能窗口看门狗时钟,使能中断。

67 void HAL_WWDG_MspInit(WWDG_HandleTypeDef *hwwdg)
68 {
69   __HAL_RCC_WWDG_CLK_ENABLE();            //使能窗口看门狗时钟
70   
71   HAL_NVIC_SetPriority(WWDG_IRQn, 10, 0); //抢占优先级10,子优先级为0
72   HAL_NVIC_EnableIRQ(WWDG_IRQn);          //使能窗口看门狗中断
73 }

窗口看门狗唤醒中断回调函数,我们可以在这个函数中去调用喂狗的函数。

83 void HAL_WWDG_EarlyWakeupCallback(WWDG_HandleTypeDef* hwwdg)
84 {
85   HAL_WWDG_Refresh(hwwdg); 	// 喂狗
86 }

21 实验20-外部Flash

对于flash芯片大家想必都不陌生,我们在使用单片机的时候,经常需要保存一些较大的数据,导致我们MCU的内部flash不够使用,因此我们需要了解如何利用外部flash去保存我们的数据,我们开发板上集成的flash芯片为W25Q80,本章节我们给大家展示如何利用STM32L476去读写这个外部的flash芯片。

21.1 STM32L476 SPI简介

SPI接口说明:

SPI接口可用于使用SPI协议与外部设备通信。SPI模式可由软件选择。 器件复位后,默认选择SPI Motorola模式。

串行外设接口(SPI)协议支持与外部设备的半双工,全双工和单工同步串行通信。 接口可以配置为主设备,在这种情况下,它为外部从设备提供通信时钟(SCK)。 该接口还能够在多主机配置中运行。

SPI主要功能:

•主或从操作

•三条线路上的全双工同步传输

•两条线路上的半双工同步传输(带双向数据线)

•两条线上的单工同步传输(带有单向数据线)

•4位至16位数据大小选择

•多主机模式功能

•8个主模式波特率预分频器,最高可达fPCLK / 2。

•从机模式频率高达fPCLK / 2。

•主机和从机的硬件或软件的NSS管理:动态变化

主/从操作

•可编程时钟极性和相位

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

•具有中断功能的专用传输和接收标志

•SPI总线忙状态标志

•SPI Motorola支持

•硬件CRC功能,可靠通信:

- CRC值可以在Tx模式下作为最后一个字节发送

- 自动CRC错误检查最后接收的字节

•主模式故障,具有中断功能的溢出标志

•CRC错误标志

•两个具有DMA功能的32位嵌入式Rx和Tx FIFO

•SPI TI模式支持

21.2 外部Flash W25Q80简介

W25Q80内存:8M bit / 1M byte

W25Q80数据接口:SPI

W25Q80 Manufacturer ID:0x13

W25Q80 Device ID:0x4014

其他具体参数及寄存器接口请大家查看:W25Q80芯片手册_EN。

21.3 硬件设计

选择STM32L4 SPI1引脚(PA5、PA6、PA7),选择PB2作为flash芯片W25Q80的CSN片选引脚。

NBDK-SCH-FLASH.png

21.4 实验准备

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

21.5 实验验证

下载完成后,大家直接按下S2和S3按键,可以看到Xshell上打印一些乱码(因为flash还未写值,所以都是0xFF,0xFF为不可视字符)。

当我们按下S1时,向flash的0x000000地址处写入一些A,此时按下S3,可以看到Xshell打印A。

当我们按下S4时,向flash的0x001000地址处写入一些B,此时按下S2,可以看到Xshell打印B。

NBDK-XSHELL-W25Q80.png

21.6 源码详解

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

21.6.1 stm32l4xx_hal_conf.h

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

此例程我们主要给大家展示STM32L4通过SPI接口向外部flash读写数据的功能,所以我们宏定义中打开SPI相关的。

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI

21.6.2 main.c

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

接下来我们分别初始化了串口和按键,用于辅助展示此例程的实验现象。

最后我们初始化了SPI接口,用于与外部flash通信,读写flash中的数据。

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

从按键回调函数中我们可以看到,UP(S1)及LEFT(S4)按键分别用于向W25Q80的地址0x000000以及0x001000写入一些字符'A'和'B'。而DOWN(S3)和RIGHT(S2)按键则负责从以上两个写入数据的地址读取数据。

注意W25Q80的写入数据流程:一定是先擦除,后写入。

读取流程:直接读取。

 80 void AppKey_cb(uint8_t key)
 81 {
 82   // 如果有相应按键被按下,则串口打印调试信息
 83   
 84   if(key & KEY_UP)
 85   {
 86     // 擦除页面地址000000
 87     HwFlashErase(0x000000);
 88     for(int i=0;i<W25Q80_PROGRAM_PAGE_SIZE;i++)
 89     {
 90       txbuf[i]= 0x41;
 91     }
 92     // 向页面地址000000写入数据
 93     HwFlashWrite(0x000000, txbuf, W25Q80_PROGRAM_PAGE_SIZE);
 94   }
 95   if(key & KEY_DOWN)
 96   {
 97     // 读取页面地址000000数据,将获取到的数据打印到串口 
 98     HwFlashRead(0x000000, rxbuf, W25Q80_PROGRAM_PAGE_SIZE); 
 99     printf("%s",rxbuf);
100   }
101   
102   if(key & KEY_LEFT)
103   {
104     // 擦除页面地址001000
105     HwFlashErase(0x001000);
106     for(int i=0;i<W25Q80_PROGRAM_PAGE_SIZE;i++)
107     {
108       txbuf[i]= 0x42;
109     }
110     // 向页面地址001000写入数据
111     HwFlashWrite(0x001000, txbuf, W25Q80_PROGRAM_PAGE_SIZE);
112   }
113   if(key & KEY_RIGHT)
114   {
115     // 读取页面地址001000数据,将获取到的数据打印到串口 
116     HwFlashRead(0x001000, rxbuf, W25Q80_PROGRAM_PAGE_SIZE); 
117     printf("%s",rxbuf);
118   }
119 }

21.6.3 gyu_util.c

请参照实验01中的介绍。

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

21.6.4 gyu_spi.c

下面4个函数,用于初始化SPI1接口,包含它的MOSI、MISO、CLK,以及单独的控制flash芯片的CSN引脚配置。

这边我们需要注意的是,配置了SPI1为主模式,数据位8bit,支持双向通信(收发都支持)。

35 void MX_SPI1_Init(void)
36 {
37   hspi1.Instance = SPI1;                                    // 配置为SPI1
38   hspi1.Init.Mode = SPI_MODE_MASTER;                        // 配置SPI1主模式
39   hspi1.Init.Direction = SPI_DIRECTION_2LINES;              
40   hspi1.Init.DataSize = SPI_DATASIZE_8BIT;                  // 配置数据大小为8bit
41   hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;              
42   hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
43   hspi1.Init.NSS = SPI_NSS_SOFT;                            // NSS由软件控制
44   hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; 
45   hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;                   // 配置数据MSB(高位在前)
46   hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
47   hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;   // 禁能CRC
48   hspi1.Init.CRCPolynomial = 7;
49   hspi1.Init.CRCLength = SPI_CRC_LENGTH_DATASIZE;
50   hspi1.Init.NSSPMode = SPI_NSS_PULSE_ENABLE;     
51   // 初始化SPI1,如果出错则进入错误处理程序
52   if (HAL_SPI_Init(&hspi1) != HAL_OK)
53   {
54     _Error_Handler(__FILE__, __LINE__);
55   }
56 }
66 void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
67 {
68   // 定义GPIO结构体
69   GPIO_InitTypeDef GPIO_InitStruct;
70   if(hspi->Instance==SPI1)
71   {
72     // 使能SPI1时钟
73     __HAL_RCC_SPI1_CLK_ENABLE();
74   
75     // 配置SPI1引脚    
76     //PA5     ------> SPI1_SCK
77     //PA6     ------> SPI1_MISO
78     //PA7     ------> SPI1_MOSI 
79     GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7; // 配置SPI1引脚
80     GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;                 // 复用推挽输出
81     GPIO_InitStruct.Pull = GPIO_NOPULL;                     // 无上下拉
82     GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;      // 高速模式
83     GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;              // 用做SPI1
84     HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                 // 初始化引脚
85   }
 96 void SPI_CSN_Init(void)
 97 {
 98 	GPIO_InitTypeDef  GPIO_InitStructure;               // 定义引脚参数结构体
 99 
100 	__HAL_RCC_GPIOA_CLK_ENABLE();                       // 使能GPIOA时钟
101   __HAL_RCC_GPIOB_CLK_ENABLE();
102   
103 	GPIO_InitStructure.Pin= GPIO_PIN_12;                // 引脚编号为15
104 	GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;      // 推挽输出
105 	GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW;     // 低频率
106 	GPIO_InitStructure.Pull = GPIO_PULLUP;              // 上拉
107 	HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);          // 初始化PA15
108 	
109 	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);    // 设置PA15默认输出低电平
110 }
120 void SPI_CSN_Ctr(GPIO_PinState pinSate)
121 {   
122 	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, pinSate);   // 设置PA15输出
123 }

剩下的4个函数是留给用户去调用的SPI初始化、SPI读写、以及判断SPI是否繁忙的函数。 在这个例程中,除了Spi_Init()函数需要在main函数中调用初始化SPI接口,其他3个函数均使用于gyu_flash_ex.c中,用于flash芯片W25Q80的控制。

139 void Spi_Init(void)
140 { 
141   // 初始化CSN引脚
142   SPI_CSN_Init();
143   // 初始化SPI1接口
144   MX_SPI1_Init();
145 }
156 int Spi_write(const uint8_t *buf, size_t len)
157 {
158   return HAL_SPI_Transmit(&hspi1,(uint8_t*)buf,len,HAL_MAX_DELAY) ? -1 : 0;
159 }
170 int Spi_read(uint8_t *buf, size_t len)
171 {
172   return HAL_SPI_Receive(&hspi1,(uint8_t*)buf,len,HAL_MAX_DELAY) ? -1 : 0;
173 }
183 void Spi_flash(void)
184 {
185     // 确保SPI硬件模块完成(也就是返回状态不为busy)
186     while(HAL_SPI_GetState(&hspi1) == HAL_SPI_STATE_BUSY)
187     { };
188 }

21.6.5 gyu_flash.c

此文件中的函数仅有3个,分别为flash擦除,flash写入,flash读取。

结合我们main.c中的按键回调函数,就可以清楚的知道,这个文件中的函数是留给开发者去读写W25Q80的。

W25Q80擦除指定page的函数,注意写入数据前,一定要先擦除数据。

36 void HwFlashErase(uint32_t page)
37 {
38   // 判断W25Q80是否正常
39   if (W25Q80_Flash_open())
40   {
41     W25Q80_Flash_erase(page, HAL_FLASH_PAGE_SIZE);  // 擦除数据
42   }
43 }

W25Q80向指定page写入数据的函数。

53 void HwFlashWrite(uint32_t page, uint8_t *pBuf, uint16_t len)
54 {
55   // 判断W25Q80是否正常
56   if (W25Q80_Flash_open())
57   {
58     W25Q80_Flash_write(page, len, pBuf); // 写入数据
59   }
60 }

W25Q80从指定page读取数据的函数。

72 void HwFlashRead(uint32_t page, uint8_t *pBuf, uint16_t len)
73 {
74   // 判断W25Q80是否正常
75   if (W25Q80_Flash_open())
76   {
77     W25Q80_Flash_read(page, len, pBuf);  // 读取数据
78   }
79 }

21.6.6 gyu_flash_ex.c

这个文件,是我们这个例程中的重中之重,所以我们将它放在最后来说明。这个文件阅读之前,请大家一定对于W25W80的寄存器有一定了解,不然理解上会出现空缺。

首先我们封装了两个函数,分别是控制SPI CSN引脚上拉和下拉的功能。

84 static void W25Q80_Flash_Select(void)
85 {
86   SPI_CSN_Ctr(GPIO_PIN_RESET);    // 拉低Flash CSN引脚
87 }
 97 static void W25Q80_Flash_Deselect(void)
 98 {
 99   SPI_CSN_Ctr(GPIO_PIN_SET);    // 拉高Flash CSN引脚
100 }

紧接着这一些函数,均是用于控制W25Q80,这部分大家可以参照代码后的注释,搭配W25Q80的芯片手册去理解。 主要功能就是几个:读取flash ID判断是否为W25Q80,唤醒W25Q80,判断W25Q80工作状态,驱动W25Q80写使能。

103 //******************************************************************
104 // fn : W25Q80_Flash_PowerStandby
105 //
106 // brief : 将W25Q80设备退出省电模式并准备正常运行
107 //
108 // param : none
109 //
110 // return : 返回1代表命令成功
111 static bool W25Q80_Flash_PowerStandby(void)
112 {
113   uint8_t cmd;
114   bool success = 0;
115   
116   cmd = BLS_CODE_RDP;
117   W25Q80_Flash_Select();                        // 使能CSN引脚
118   success = Spi_write(&cmd,sizeof(cmd)) == 0;   // 发送使能W25Q80的命令
119   W25Q80_Flash_Deselect();                      // 禁止CSN引脚
120   
121   if (success)                                  // 如果使能W25Q80命令发送成功    
122   {
123     if (W25Q80_Flash_waitReady() == 0)        // 等待指令完成  
124     {
125       success = 1;
126     }
127   }
128 
129   return success;
130 }
131 
132 //******************************************************************
133 // fn : W25Q80_Flash_readInfo
134 //
135 // brief : 读取W25Q80 Flash信息(制造商和设备ID)
136 //
137 // param : none
138 //
139 // return : 返回1代表命令成功
140  bool W25Q80_Flash_readInfo(void)
141 {
142   const uint8_t wbuf[] = { BLS_CODE_MDID, 0xFF, 0xFF, 0x00 };
143 
144   W25Q80_Flash_Select();                    // 使能CSN引脚
145 
146   int ret = Spi_write(wbuf, sizeof(wbuf));  // 发送设备信息的命令
147   if (ret)                      // 如果SPI写失败
148   { 
149     W25Q80_Flash_Deselect();    // 禁止CSN引脚
150     return 0;                   // 返回0
151   }
152 
153   ret = Spi_read(infoBuf, sizeof(infoBuf));   // 读取设备信息的命令
154   W25Q80_Flash_Deselect();      // 禁止CSN引脚
155   
156   if(ret == 0)                  // 如果SPI读成功
157   {
158     return 1;                   // 返回1
159   }
160   
161   return 0;                     // 返回0
162 }
163 
164 //******************************************************************
165 // fn : W25Q80_Flash_VerifyPart
166 //
167 // brief : 检测W25Q80 Flash信息是否正确
168 //
169 // param : none
170 //
171 // return : 返回1代表命令成功
172 static bool W25Q80_Flash_VerifyPart(void)
173 {
174   // 判断是否成功读取设备信息
175   if (!W25Q80_Flash_readInfo())
176   {
177     return false;
178   }
179 
180   // 下面一段是判断读取的设备ID是否为W25Q80
181   pFlashInfo = flashInfo;
182   while (pFlashInfo->deviceSize > 0)
183   {
184     if (infoBuf[0] == pFlashInfo->manfId && infoBuf[1] == pFlashInfo->devId)
185     {
186       break;
187     }
188     pFlashInfo++;
189   }
190 
191   return pFlashInfo->deviceSize > 0;  // 成功返回1
192 }
193 
194 //******************************************************************
195 // fn : W25Q80_Flash_waitReady
196 //
197 // brief : 等待上一次擦除/编程操作完成
198 //
199 // param : none
200 //
201 // return : 返回0代表命令成功
202 static int W25Q80_Flash_waitReady(void)
203 {
204   const uint8_t wbuf[1] = { BLS_CODE_READ_STATUS };
205   int ret;
206 
207   // 等待SPI完成
208   W25Q80_Flash_Select();
209   Spi_flash();
210   W25Q80_Flash_Deselect();
211 
212   for (;;)
213   {
214     uint8_t buf;
215 
216     W25Q80_Flash_Select();            // 使能CSN引脚
217     Spi_write(wbuf, sizeof(wbuf));    // 发送读取W25Q80状态的命令
218     ret = Spi_read(&buf,sizeof(buf)); // 读取状态返回值
219     W25Q80_Flash_Deselect();          // 禁止CSN引脚
220 
221     if (ret)                          // 如果SPI写失败
222     {
223       return -2;                    // 返回错误-2
224     }
225     if (!(buf & BLS_STATUS_BIT_BUSY)) // 如果状态返回为busy,则等待
226     {
227       /* Now ready */
228       break;
229     }
230   }
231 
232   return 0;
233 }
234 
235 //******************************************************************
236 // fn : W25Q80_Flash_writeEnable
237 //
238 // brief : W25Q80 Flash写使能
239 //
240 // param : none
241 //
242 // return : 返回0代表命令成功
243 static int W25Q80_Flash_writeEnable(void)
244 {
245   const uint8_t wbuf[] = { BLS_CODE_WRITE_ENABLE };
246 
247   W25Q80_Flash_Select();                      // 使能CSN引脚
248   int ret = Spi_write(wbuf,sizeof(wbuf));     // 发送W25Q80写使能命令
249   W25Q80_Flash_Deselect();                    // 禁止CSN引脚
250 
251   if (ret)            // 如果SPI写失败
252   {
253       return -3;      // 返回错误-3
254   }
255   return 0;
256 }

接下来的4个函数,我们将其与上面的几个W25Q80的控制函数分开讲解,原因在于这几个函数是用户用于读写flash的。 首先是W25Q80_Flash_open()函数,这个函数是用户唤醒W25Q80,并且检测flash ID是否正确。

272 bool W25Q80_Flash_open(void)
273 {
274   bool f;
275   
276   // 设置唤醒,返回1代表成功
277   f = W25Q80_Flash_PowerStandby();
278   
279   // 如果成功,则获取ID
280   if (f)
281   {
282     // 获取ID(Verify manufacturer and device ID),记录成功状态
283     f = W25Q80_Flash_VerifyPart();
284   }
285   
286   // 返回1则获取ID成功,返回0则失败
287   return f;
288 }

剩下的3个函数,分别是W25Q80擦除、写入、读取的功能,这3个函数都在gyu_flash.c中经过一层封装,留给大家使用了,这边大家感兴趣的可以仔细看下代码原型。

290 //******************************************************************
291 // fn : W25Q80_Flash_read
292 //
293 // brief : W25Q80 flash芯片读函数
294 //
295 // param : offset -> flash地址
296 //         length -> 准备读取的数据长度
297 //         buf -> 指向准备读取的数据指针
298 //
299 // return : 返回1代表命令成功
300 bool W25Q80_Flash_read(size_t offset, size_t length, uint8_t *buf)
301 {
302   uint8_t wbuf[4];
303   
304   // 等待之前的flash处理完成
305   int ret = W25Q80_Flash_waitReady();
306   if (ret)
307   {
308     return false;
309   }
310 
311   // 设置读指令
312   wbuf[0] = BLS_CODE_READ;            // 读命令
313   wbuf[1] = (offset >> 16) & 0xff;    // 页面地址
314   wbuf[2] = (offset >> 8) & 0xff;     // 页面地址
315   wbuf[3] = offset & 0xff;            // 页面地址
316 
317   W25Q80_Flash_Select();              // 使能CSN引脚
318 
319   if (Spi_write(wbuf, sizeof(wbuf)))  // 写读取数据命令,失败则进入下面的处理
320   {
321     W25Q80_Flash_Deselect();          // 禁止CSN引脚
322     return false;                     // 返回0
323   }
324 
325   ret = Spi_read(buf, length);        // SPI读取数据
326 
327   W25Q80_Flash_Deselect();            // 禁止CSN引脚
328 
329   return ret == 0;                    // SPI读取数据成功,返回1
330 }
331 
332 //******************************************************************
333 // fn : W25Q80_Flash_write
334 //
335 // brief : W25Q80 flash芯片写函数
336 //
337 // param : offset -> flash地址
338 //         length -> 准备写入的数据长度
339 //         buf -> 指向准备写入的数据指针
340 //
341 // return : 返回1代表命令成功
342 bool W25Q80_Flash_write(size_t offset, size_t length, const uint8_t *buf)
343 {
344   uint8_t wbuf[4];
345 
346   while (length > 0)
347   {
348     // 检测之前的W25Q80 flash操作是否完成
349     int ret = W25Q80_Flash_waitReady();
350     if (ret)  // 如果失败,返回0
351     {
352       return false;
353     }
354 
355     // W25Q80 flash写使能
356     ret = W25Q80_Flash_writeEnable();
357     if (ret)  // 如果失败,返回0
358     {
359       return false;
360     }
361 
362     // 下面一段是计算数据长度,并且将扇区信息赋值给SPI的写缓冲区(wbuf)
363     size_t ilen;
364     ilen = BLS_PROGRAM_PAGE_SIZE - (offset % BLS_PROGRAM_PAGE_SIZE);
365     if (length < ilen)
366     {
367       ilen = length;
368     }
369 
370     wbuf[0] = BLS_CODE_PROGRAM;       // 编写页面指令
371     wbuf[1] = (offset >> 16) & 0xff;  // 页面地址
372     wbuf[2] = (offset >> 8) & 0xff;   // 页面地址
373     wbuf[3] = offset & 0xff;          // 页面地址
374     
375     offset += ilen;
376     length -= ilen;
377 
378     W25Q80_Flash_Select();            // 使能CSN引脚
379 
380     if (Spi_write(wbuf, sizeof(wbuf)))// 写编写页面的命令,失败则进入如下处理
381     {
382       W25Q80_Flash_Deselect();      // 禁止CSN引脚
383       return false;                 // 返回0
384     }
385 
386     if (Spi_write(buf,ilen))          // 写数据,失败则进入如下处理
387     {
388       W25Q80_Flash_Deselect();      // 禁止CSN引脚
389       return false;                 // 返回0
390     }
391     buf += ilen;              
392     W25Q80_Flash_Deselect();          // 禁止CSN引脚  
393   }
394 
395   return true;    // 返回1
396 }
397 
398 //******************************************************************
399 // fn : W25Q80_Flash_erase
400 //
401 // brief : W25Q80 flash芯片擦除函数
402 //
403 // param : offset -> flash地址
404 //         length -> 准备擦除的数据长度
405 //
406 // return : 返回1代表命令成功
407 bool W25Q80_Flash_erase(size_t offset, size_t length)
408 {
409   uint8_t wbuf[4];
410   size_t i, numsectors;
411 
412   wbuf[0] = BLS_CODE_SECTOR_ERASE;
413 
414   // 下面一段是计算擦除的扇区大小
415   {
416     size_t endoffset = offset + length - 1;
417     offset = (offset / BLS_ERASE_SECTOR_SIZE) * BLS_ERASE_SECTOR_SIZE;
418     numsectors = (endoffset - offset + BLS_ERASE_SECTOR_SIZE - 1) / BLS_ERASE_SECTOR_SIZE;
419   }
420   
421   for (i = 0; i < numsectors; i++)
422   {
423     // 等待上一个擦除命令完成
424     int ret = W25Q80_Flash_waitReady();
425     if (ret)  // 返回失败
426     {
427       return false; // 返回0
428     }
429 
430     // 写使能
431     ret = W25Q80_Flash_writeEnable();
432     if (ret)  // 返回失败
433     {
434       return false; // 返回0
435     }
436 
437     wbuf[1] = (offset >> 16) & 0xff;  // 页面地址
438     wbuf[2] = (offset >> 8) & 0xff;   // 页面地址
439     wbuf[3] = offset & 0xff;          // 页面地址
440 
441     W25Q80_Flash_Select();            // 使能CSN引脚
442 
443     if (Spi_write(wbuf, sizeof(wbuf)))// 发送擦除命令,失败则进入下面处理
444     {
445       W25Q80_Flash_Deselect();      // 禁止CSN引脚
446       return false;                 // 返回0
447     }
448     W25Q80_Flash_Deselect();          // 禁止CSN引脚
449     
450     offset += BLS_ERASE_SECTOR_SIZE;
451   }
452 
453   return true;  // 返回1
454 }

22 实验21-内部Flash

上一章我们讲解了如何将大量的用户数据存储到外部flash,那么在进行小数据量存储的时候,我们一定需要再额外增加一颗flash芯片吗,显然不是如此,这样会造成资源的浪费。

当我们的程序没有完全占用所有flash内部空间的情况,我们完成可以将未被使用的内部flash用来存储少量的用户个人数据。

22.1 STM32L476 内部Flash简介

STM32L476芯片有3种flash大小的型号,分别是1MB、512KB以及256KB。

我们选择的STM32L476RC芯片是256KB大小的flash空间,下面是和我们本章节有关的flash说明信息:

•内存组织:2个bank(Bank 1和Bank 2)

- 主存储器:每个存储区128 KB

•72位宽数据读取(64位加8个ECC位)

•72位宽数据写入(64位加8个ECC位)

•页擦除(2 KB),存储区擦除和批量擦除(两个存储区)

...

STM32L476RC有两个bank,都是128K大小,每个bank分为64个page,每个page 2K大小。

bank1的page编号从0-63,bank2的page编号从256-319。

这边我们给大家简单介绍一下如何计算某个地址位于哪一个page页下,起始地址是0x08000000,且每个page大小是2K,也就是2048byte,计算公式分为两种情况:

第一种是地址位于bank1,例如地址0x08001800:

计算公式:page = (0x0x08001800-0x08000000)/2048 = 3

第二种是地址位于bank2,例如我们此例程选择的地址0x08031800,因为bank首页编码为page 256,所以计算的值要加上192,才是正确的page编码:

计算公式:page = (0x08031800-0x08000000)/2048 + 192 = 291

NBDK-DS-FLASH.png

22.2 硬件设计

选择STM32L4的内部FLASH。

22.3 实验准备

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

22.4 实验验证

下载完成后,我们可以先按下UP按键,然后按下RIGHT按键,可以看到串口打印“0xA1A2A3A4A5A6A7A8,0xB1B2B3B4B5B6B7B8,”字样。

然后我们按下DOWN按键,然后按下RIGHT按键,可以看到串口打印“0xC1C2C3C4C5C6C7C8,0xD1D2D3D4D5D6D7D8,”字样。

以此类推,当我们不断重新按下UP或者DOWN按键,然后按下RIGHT按键,可以看到串口打印的值一直在变动。所以我们实验成功,可以对内部进行擦除、写入、读取的操作。

NBDK-XSHELL-FLASH.png

22.5 源码详解

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

22.5.1 stm32l4xx_hal_conf.h

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

此例程我们主要给大家展示STM32L4的内部flash读取,flash是任何例程都需要用到的,所以flash的宏定义一直都是有使能的。

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

22.5.2 main.c

定义待会准备写入数据的flash地址,注意一定是程序未被使用的部分。在这个例程中,我们展示用的是bank2中的地址,且能被8整除,不然在后面的地址判断中会被认为是错误的地址。

其次我们还定义了两个不同的数组,用于向flash写入不同的值,用于展示擦除重写功能。

31 // 定义准备写入的flash地址(bank2中地址,且能被8整除)
32 #define FLASH_SAVE_ADDR   0x08031800 	
33 
34 // 定义准备写入flash中的数据
35 #define textLen   0x02
36 uint64_t textBuf1[textLen] = {0xA1A2A3A4A5A6A7A8,0xB1B2B3B4B5B6B7B8};
37 uint64_t textBuf2[textLen] = {0xC1C2C3C4C5C6C7C8,0xD1D2D3D4D5D6D7D8};

下面这个函数,是一个转换格式的函数,主要功能是将64bit的数据转换成字符串,这样方便我们待会将从flash中获取的数据可视化打印到串口。

39 //******************************************************************
40 // 64bit数据转成字符串函数,用来展示串口打印从flash读出的值
41 //******************************************************************
42 char *FlashData_Hex2Str( uint64_t pData )
43 {
44   char        hex[] = "0123456789ABCDEF";
45   static char str[17];      // 长度设置为17,字符串最后有一个\0
46   char        *pStr = str;
47   
48   for(uint8_t i = 1; i < 17; i++ )
49   {
50     *pStr++ = hex[(pData>>(64-4*i))&0x000000000000000F];
51   }
52   
53   *pStr = 0;
54   
55   return str;
56 }

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

因为这个例程用到的是内部flash,所以我们不需要去进行flash部分的初始化,只需要初始化串口和按键,用于展示我们的实验现象。

66 int main(void)
67 {
68   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
69 	// 重置所有外设、flash界面以及系统时钟
70   HAL_Init();
71 
72 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
73   SystemClock_Config();
74   
75 	// 初始化USART1
76 	MX_USART1_UART_Init();
77   
78   // 初始化按键引脚
79 	MX_KEY_Init();
80   
81   //注册按钮回调函数
82   KEY_RegisterCb(AppKey_cb);
83 
84   // 
85   while(1)
86   {
87 		KEY_Poll();   // 按键轮训,监测是否有按键被按下
88   }
89 }

按键回调函数中,我们可以分别按下UP或者DOWN按键,来向内部flash未被程序使用的地址中写入不同的数据,然后可以通过按下按键RIGHT的方式,来获取flash中刚刚写入

100 void AppKey_cb(uint8_t key)
101 {
102   // 如果有相应按键被按下,则串口打印调试信息
103   if(key & KEY_UP)
104   {
105     ST_Flash_Write(FLASH_SAVE_ADDR, textBuf1, textLen); // flash中写入textBuf1
106   }
107   if(key & KEY_DOWN)
108   {
109     ST_Flash_Write(FLASH_SAVE_ADDR, textBuf2, textLen); // flash中写入textBuf2
110   }
111   if(key & KEY_LEFT)
112   {
113     
114   }
115   if(key & KEY_RIGHT)
116   {
117     uint64_t dataBuf[textLen];
118     ST_Flash_Read(FLASH_SAVE_ADDR, dataBuf, textLen);   // 读取flash中的数据
119     
120     for(uint32_t i=0; i< textLen; i++)
121     {
122       printf("0x%s,",FlashData_Hex2Str(dataBuf[i]));  // 将读取的数据转成字符串打印到串口
123     }
124   }
125 }

22.5.3 gyu_util.c

请参照实验01中的介绍。

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

22.5.4 gyu_stflash.c

检测准备写入数据的flash地址,flash地址必须满足位于bank2、且能被8整除的条件限制,否则认定为非法地址。

如果是合法的地址,则返回地址编号,如果是非法的,则返回0。

32 uint32_t ST_Flash_AddrCheck(uint32_t addr)
33 {
34   uint32_t page;
35   
36   // 如果地址小于STM32 flash bank2起始地址,或者不能整除8(一个字节8bit)
37   if(addr < STM32_FLASH_BANK2 || addr % 8)
38   {
39     return 0;   // 认为是错误的地址,返回0
40   }
41   
42   // 计算page编号,注意:bank2 page页编码从256开始,所以计算的值要加192
43   // 例如计算得出64,则代表是page256,具体请参考芯片手册
44   page = (addr - STM32_FLASH_BASE) / STM_SECTOR_SIZE + 192;
45   
46   // 判断是否为bank2 page
47   if(page>255 && page<320)
48   {
49     return page;
50   }
51   else
52   {
53     return 0;
54   }
55 }

内部flash擦除函数,用于擦除bank2中的一页,具体是哪一页,由上一个函数的返回值决定。

66 void ST_Flash_Erase(uint32_t page)
67 {
68   FLASH_EraseInitTypeDef FLASH_EraseInitStruct; // Flash擦除结构体
69   FLASH_EraseInitStruct.Banks = FLASH_BANK_2;   // 选择Bank2
70   FLASH_EraseInitStruct.NbPages =1;             // 只擦除1页
71   FLASH_EraseInitStruct.Page = page;            // 页编号
72   FLASH_EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;  // 擦除页
73   
74   uint32_t	PageError;
75   // 擦除所选页,并使能擦除中断
76   if(HAL_FLASHEx_Erase(&FLASH_EraseInitStruct,&PageError) != HAL_OK)
77   {
78     _Error_Handler(__FILE__, __LINE__);
79   }
80 }

内部flash写函数,用于向内部flash的指定地址,写入我们指定的数据。

 92 void ST_Flash_Write(uint32_t addr, uint64_t* pbuf, uint16_t len)
 93 {
 94   uint32_t page;
 95   
 96   // 检测地址,确认地址是否是bank2的page
 97   page = ST_Flash_AddrCheck(addr);
 98   
 99   // 如果地址不正确,则退出写函数
100   if(!page)
101   {
102     return;
103   }
104   
105   HAL_FLASH_Unlock();   // flash解锁
106   
107   // 擦除对应的flash page
108   ST_Flash_Erase(page);
109   
110   for(uint32_t i = 0; i < len; i++)
111   {
112     // 向指定flash地址写入数据,数据为64bit
113     if( HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, addr, pbuf[i]) != HAL_OK )
114     {
115       _Error_Handler(__FILE__, __LINE__);
116     }
117     addr += 8;  // 地址位移8个字节(一个字节是8bit)
118   }
119   
120   HAL_FLASH_Lock();   // flash上锁
121 }

内部flash读函数,用于从内部flash指定地址中读取我们想要获取的数据。

133 void ST_Flash_Read(uint32_t addr, uint64_t *pbuf, uint32_t len)
134 {
135   uint16_t buf[4];
136   for(uint32_t i = 0; i < len; i++)
137   {
138     for(uint32_t j = 0; j < 4; j++)
139     {
140       buf[j] = *(__IO uint16_t*)addr;   // 获取地址的值(16bit)
141       addr += 2;                        // 地址位移2个字节(16bit)
142     }
143     
144     // 将获取的4个16bit数据拼接一下
145     pbuf[i] = buf[0] + ((uint64_t)buf[1]<<16) + ((uint64_t)buf[2]<<32) + ((uint64_t)buf[3]<<48);
146   }
147 }

23 实验22-串口升级(bootloader)

在如今的多样性需求当中,很多用户都需要不定期的对MCU的程序进行升级,如果是类似蓝牙这种具有无线通信功能的设备,那么大家普遍会使用OAD(无线升级)方式,而如果是STM32这种,那么一般都是使用USB或者UART等接口进行程序的升级。此实验就是给大家展示的STM32L4通过串口升级程序,搭配实验23-串口升级(app)一起使用。

23.1 STM32L476 IAP简介

IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。

通常在用户需要实现IAP功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信管道(如USB、[/baike.baidu.com/item/USART USART])接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在User Flash中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:

1)检查是否需要对第二部分代码进行更新

2)如果不需要更新则转到4)

3)执行更新操作

4)跳转到第二部分代码执行

第一部分代码必须通过其它手段,如[/baike.baidu.com/item/JTAG JTAG]或[/baike.baidu.com/item/ISP ISP]烧入;第二部分代码可以使用第一部分代码IAP功能烧入,也可以和第一部分代码一道烧入,以后需要程序更新是再通过第一部分IAP代码更新。

对于STM32来说,因为它的中断向量表位于程序存储器的最低地址区,为了使第一部分代码能够正确地响应中断,通常会安排第一部分代码处于Flash的开始区域,而第二部分代码紧随其后。

在第二部分代码开始执行时,首先需要把CPU的中断向量表映像到自己的向量表,然后再执行其他的操作。

如果IAP程序被破坏,产品必须返厂才能重新烧写程序,这是很麻烦并且非常耗费时间和金钱的。针对这样的需求,STM32在对Flash区域实行读保护的同时,自动地对用户Flash区的开始4页设置为写保护,这样可以有效地保证IAP程序(第一部分代码)区域不会被意外地破坏。

23.2 bootloader工程配置简介

由于我们使用的STM32L476RC芯片具有两个BANK,分别为BANK1和BANDK2,所以我们选择BANK1作为bootloader程序存储的地址,BANK2预留给app程序。

我们配置bootloader程序的flash地址,从0x08000000开始,大小为0x20000,也就是我们BANK1的部分。

NBDK-KEIL-BOOTLOADER.png

23.3 硬件设计

选择STM32L4的串口以及内部flash的BANK1,并且使用了SPI接口的显示屏展示实验现象。

NBDK-SCH-UART-USB.png

23.4 实验准备

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

23.5 实验验证

下载完成后,运行的情况分为2种:

1.未按下S1,按下RST复位:

可以看到TFT显示屏打印"谷雨物联网"的logo,然后打印调试的信息"<Check APP...>""APP Lose ->",代表没有检测到APP程序,此时运行的程序是bootloader部分,可以分别按下S1、S4,可以看到会打印"aa"、"bb"。

2.保持按下S1,此时按下RST复位:

可以看到TFT显示屏打印"谷雨物联网"的logo,然后打印调试的信息"<<Enter IAP",代表进入IAP状态,等待下载APP程序。

在运行情况2的基础上,我们使用超级终端去下载APP部分程序。

1)打开超级终端,超级终端位于:\实验22-串口升级(bootloader)\超级终端\hypertrm.exe

NBDK-HyperTerminal-00.png

2)双击打开超级终端后如下所示

NBDK-HyperTerminal-01.png

3)此时我们点击文件下的新建连接

NBDK-HyperTerminal-02.png

4)输入一个任意的连接名称,我们这边以xx代替

NBDK-HyperTerminal-03.png

5)选择连接时使用的COM口,我们这边选择的开发板上的USB接口虚拟的COM

NBDK-HyperTerminal-04.png

6)配置端口协议,主要是这两个:波特率115200,没有流控制,点击应用,点击确定

NBDK-HyperTerminal-05.png

7)配置好端口协议之后,我们保持按下S1,然后按下RST,可以看到超级终端打印如下数据,并且周期性的打印字节'C'

NBDK-HyperTerminal-06.png

8)选择超级终端,传送(T)中的发送文件,选择实验23-串口升级(app)中生成的23_iap_app.bin文件,协议选择Ymodem

NBDK-HyperTerminal-07.png

9)点击发送后,等待发送完成

NBDK-HyperTerminal-08.png

10)发送完成后,打印本次传输的23_iap_app.bin文件的名称及大小

NBDK-HyperTerminal-09.png

11)此时我们按下开发板上的RST按键,可以看到显示屏打印“<<RUN IAP APP>>”字样,并且LED周期闪烁(app程序中的tim2中断控制),代表app程序成功烧写,并且正常运行。

12)实验成功。

23.6 源码详解

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

23.6.1 stm32l4xx_hal_conf.h

此文件位于“实验24-低功耗待机\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们用于串口升级的bootloader程序,所以打开UART用于串口写入,打开SPI用于TFT展示实验现象,打开CRC用于超级终端写入app程序时的Yomdem协议校验。

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI
114 #define HAL_CRC_MODULE_ENABLED      // CRC

23.6.2 main.c

main函数中,我们主要关注一下IAP相关部分代码。

首先我们监测S1按键的引脚电平,如果是低电平,则进入IAP模式,等待更新APP程序。

如果是高电平,则继续向下执行,判断BNAK2是否有app程序,如果有app程序,则执行app程序;如果没有app程序,则打印“APP Lose”提示。

 49 int main(void)
 50 {
 51   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
 52 	// 重置所有外设、flash界面以及系统时钟
 53   HAL_Init();
 54   
 55 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
 56   SystemClock_Config();
 57   
 58   // 初始化按键引脚
 59 	MX_KEY_Init();
 60   
 61   //注册按钮回调函数
 62   KEY_RegisterCb(AppKey_cb);
 63   
 64   // LCD SPI初始化
 65   LCD_GPIO_Init();  // LCD IO控制引脚(例如背光)
 66   MX_SPI1_Init();   // LCD SPI控制引脚
 67   COM_Init();       // 初始化串口1.其波特率为115200
 68   
 69   // 图形界面初始化
 70   GUI_Init();       // GUI界面初始化
 71   GUI_Clear();      // 清屏
 72   
 73   // 打印logo到位置X->0,Y->0
 74   GUI_DrawBitmap(&bmLogo,0,0);
 75   
 76   // IAP监视引脚,检测UP按键引脚电平
 77   // 如果上电是低电平(需要按下),则进入IAP,等待升级
 78   if(HAL_GPIO_ReadPin(IAP_PORT,IAP_PIN) == GPIO_PIN_RESET)
 79   {
 80     GUI_DispStringAt("<<Enter IAP>>\r\n",0,80);
 81     Main_Menu();
 82   }
 83   // 如果上电是高电平(默认高电平),则检测并运行APP程序
 84   else
 85   {
 86     // 检查Flash_bank2 是否存在用户APP
 87     GUI_DispStringAt("<Check APP...>\r\n",0,80);
 88     // 如果有用户APP,则切换到APP程序
 89     if(FLASHIF_OK == FLASH_If_Check(FLASH_START_BANK2))
 90     {
 91       GUI_DispString("APP Exist ->\r\nRun\r\n");    // LCD提示用户APP程序存在,并运行
 92       __HAL_RCC_GPIOC_FORCE_RESET();
 93       SwitchToBand2(FLASH_START_BANK2);             // 切换到APP程序存储的bank2中
 94     }
 95     // 如果没有用户APP
 96     else
 97     {
 98       GUI_DispString("APP Lose ->\r\n");            // LCD提示用户APP程序缺失
 99     }
100   }
101   // 
102   while(1)
103   {
104 		KEY_Poll();
105   }
106 }

23.6.3 gyu_util.c

请参照实验01中的介绍。

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

23.6.4 gyu_iap_uart_crc.c

初始化IAP需要使用到的串口和CRC校验部分的文件。

定义对齐方式,定义512字节对齐。并且定义了中断向量表的地址存储buf。

23 // 定义对齐方式
24 #if defined ( __ICCARM__ )
25  #pragma data_alignment=512
26  #elif defined ( __CC_ARM )
27  __align(512)
28  #elif defined ( __GNUC__ ) /* GCC Compiler */
29  __attribute__ ((aligned (512)))
30  #endif
31 uint32_t vector_t[72];

用于重新定义中断向量表的位置。

42 void Relocate_NVIC(uint32_t address)
43 {
44   uint32_t i;
45 
46   for (i = 0;i < 72;i++)
47   {
48     vector_t[i] = *((uint32_t*)(address + (i << 2)));
49   }
50   SCB->VTOR = (uint32_t)vector_t;
51 }

初始化串口和CRC校验,为后面的串口升级做准备。这边的串口协议就是我们在超级终端中选择的COM口协议。

 54 // 串口结构体
 55 UART_HandleTypeDef UartHandle;
 56 // CRC结构体
 57 CRC_HandleTypeDef CrcHandle;
 58 
 59 //******************************************************************
 60 // fn : COM_Init
 61 //
 62 // brief : 串口初始化以及CRC初始化
 63 //
 64 // param : none
 65 //
 66 // return : none
 67 void COM_Init(void)
 68 {
 69   // 初始化串口功能
 70   UartHandle.Instance = USART1;
 71   UartHandle.Init.BaudRate = 115200;
 72   UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
 73   UartHandle.Init.StopBits = UART_STOPBITS_1;
 74   UartHandle.Init.Parity = UART_PARITY_NONE;
 75   UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
 76   UartHandle.Init.Mode = UART_MODE_TX_RX;
 77   if (HAL_UART_Init(&UartHandle) != HAL_OK)
 78   {
 79     _Error_Handler(__FILE__, __LINE__);
 80   }
 81   
 82   // 初始化CRC功能
 83   CrcHandle.Instance = CRC;
 84   CrcHandle.Init.DefaultPolynomialUse    = DEFAULT_POLYNOMIAL_DISABLE;
 85   CrcHandle.Init.GeneratingPolynomial    = 0x1021;
 86   CrcHandle.Init.CRCLength               = CRC_POLYLENGTH_16B;
 87   CrcHandle.Init.DefaultInitValueUse     = DEFAULT_INIT_VALUE_DISABLE;
 88   CrcHandle.Init.InitValue               = 0;
 89   CrcHandle.Init.InputDataInversionMode  = CRC_INPUTDATA_INVERSION_NONE;
 90   CrcHandle.Init.OutputDataInversionMode = CRC_OUTPUTDATA_INVERSION_DISABLE;
 91   CrcHandle.InputDataFormat              = CRC_INPUTDATA_FORMAT_BYTES;
 92   if (HAL_CRC_Init(&CrcHandle) != HAL_OK)
 93   {
 94     _Error_Handler(__FILE__, __LINE__);
 95   }
 96 }
 97 
 98 //******************************************************************
 99 // fn : HAL_UART_MspInit
100 //
101 // brief : 串口初始化函数
102 //
103 // param : huart -> 串口句柄
104 //
105 // return : none
106 void HAL_UART_MspInit(UART_HandleTypeDef *huart)
107 {
108   GPIO_InitTypeDef  GPIO_InitStruct;
109 
110   __HAL_RCC_GPIOA_CLK_ENABLE();   // 使能GPIOA时钟
111   __HAL_RCC_USART1_CLK_ENABLE();  // 使能USART1时钟
112 
113   // 配置串口TX引脚
114   GPIO_InitStruct.Pin       = GPIO_PIN_9;
115   GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
116   GPIO_InitStruct.Pull      = GPIO_PULLUP;
117   GPIO_InitStruct.Speed     = GPIO_SPEED_HIGH;
118   GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
119   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
120 
121   // 配置串口RX引脚
122   GPIO_InitStruct.Pin = GPIO_PIN_10;
123   GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
124   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
125 }
126 
127 //******************************************************************
128 // fn : COM_Init
129 //
130 // brief : 串口初始化以及CRC初始化
131 //
132 // param : none
133 //
134 // return : none
135 void HAL_CRC_MspInit(CRC_HandleTypeDef *hcrc)
136 {
137   // 使能CRC时钟
138   __HAL_RCC_CRC_CLK_ENABLE();
139 }

23.7 menu.c

这个文件是IAP在超级终端中主要做显示菜单的,也就是格式化打印一些调试的数据,当然也是底层其他函数的入口(这边说的底层函数指的是flash_if、ymodem、common这三个)。

这个函数是使用汇编语言编写,目的是切换程序运行的指针,将指针指向bank2地址(这个函数编译的时候,keil中会打一个X,但是不会报错)。

42 __asm void SwitchToBand2(int addr)
43 {
44   ldr sp, [r0]
45   add r0, #4
46   ldr pc, [r0]
47 }

串口下载数据的函数,超级终端升级app文件的时候,就是调用的此函数。

57 void SerialDownload(void)
58 {
59   uint8_t number[11] = {0};
60   uint32_t size = 0;
61   COM_StatusTypeDef result;
62 
63   // 打印等待文件烧写的提示
64   Serial_PutString((uint8_t *)"Waiting for the file to be sent ...\n\r");
65   
66   // 通过超级终端使用Ymodem协议去处理文件的下载
67   result = Ymodem_Receive( &size, BankActive );
68   // 如果写入成功,打印文件名及其大小
69   if (result == COM_OK)
70   {
71     Serial_PutString((uint8_t *) "\n\n\r Programming Completed Successfully!\n\r--------------------------------\r\n Name: ");
72     Serial_PutString(aFileName);
73     Int2Str(number, size);
74     Serial_PutString((uint8_t *)"\n\r Size: ");
75     Serial_PutString(number);
76     Serial_PutString((uint8_t *)" Bytes\r\n");
77     Serial_PutString((uint8_t *)"-------------------\n");
78   }
79   // 如果写入失败,打印失败提示
80   else
81   {
82     Serial_PutString((uint8_t *)"\n\r Programming Failed!\n\r");
83   }
84 }

主菜单函数,用于格式化打印一个菜单栏,这边给大家说一下,我们的IAP例程是脱胎于ST提供的一个IAP实例,所以这边保留了ST本身的菜单栏(也就是作者是ST公司)。

 94 void Main_Menu(void)
 95 {
 96   // 超级终端上打印菜单栏说明
 97   Serial_PutString((uint8_t *)"\r\n======================================================================");
 98   Serial_PutString((uint8_t *)"\r\n=              (C) COPYRIGHT 2016 STMicroelectronics                 =");
 99   Serial_PutString((uint8_t *)"\r\n=                                                                    =");
100   Serial_PutString((uint8_t *)"\r\n=   STM32L476 Dual Bank Usage Example Application   (Version 0.1.0)  =");
101   Serial_PutString((uint8_t *)"\r\n=                                                                    =");
102   Serial_PutString((uint8_t *)"\r\n=                                   By MCD Application Team          =");
103   Serial_PutString((uint8_t *)"\r\n======================================================================");
104   Serial_PutString((uint8_t *)"\r\n\r\n");
105 
106   // 获取当前配置
107   HAL_FLASHEx_OBGetConfig( &OBConfig );
108   
109   // 解锁flash写保护,允许写入数据
110   FLASH_If_WriteProtectionClear();
111 
112   // 通过串口下载文件
113   SerialDownload();
114 }

23.8 flash_if.c

此文件用于处理STM32L4的内部flash,主要是负责擦除、写入,并且留了一个检测bank2中是否有app程序的函数。

擦除flash的函数,使用的方式是擦除整个扇区,和我们实验21-内部flash一页一页擦除有所不同。

34 uint32_t FLASH_If_Erase(uint32_t bank_active)
35 {
36   uint32_t bank_to_erase, error = 0;
37   FLASH_EraseInitTypeDef pEraseInit;
38   HAL_StatusTypeDef status = HAL_OK;
39 
40   if (bank_active == 0)
41   {
42     bank_to_erase = FLASH_BANK_2;
43     Serial_PutString((uint8_t *)"Erasing bank 2.\r\n");
44   }
45   else
46   {
47     bank_to_erase = FLASH_BANK_1;
48     Serial_PutString((uint8_t *)"Erasing bank 1.\r\n");
49   }
50 
51   // 解锁Flash以启用闪存控制寄存器访问
52   HAL_FLASH_Unlock();
53 
54   pEraseInit.Banks = bank_to_erase;
55   pEraseInit.NbPages = 255;
56   pEraseInit.Page = 0;
57   pEraseInit.TypeErase = FLASH_TYPEERASE_MASSERASE;
58   
59   status = HAL_FLASHEx_Erase(&pEraseInit, &error);
60   
61   // 锁定Flash以禁用闪存控制寄存器访问(建议保护FLASH存储器免受可能的意外操作)
62   HAL_FLASH_Lock();
63 
64   if (status != HAL_OK)
65   {
66     // 页面擦除时出错
67     return FLASHIF_ERASEKO;
68   }
69   
70   return FLASHIF_OK;
71 }

检测bank2中是否存在app程序,留给main()函数调用的。

81 uint32_t FLASH_If_Check(uint32_t start)
82 {
83   // 检查数据是否可以是代码(第一个字是堆栈位置)
84   if ((*(uint32_t*)start >> 24) != 0x20 ) return FLASHIF_EMPTY;
85 
86   return FLASHIF_OK;
87 }

在闪存中写入数据缓冲区(数据为32位对齐),写入数据缓冲区后,将检查Flash内容。

 99 uint32_t FLASH_If_Write(uint32_t destination, uint32_t *p_source, uint32_t length)
100 {
101   uint32_t status = FLASHIF_OK;
102   uint32_t i = 0;
103 
104   // 解锁Flash以启用闪存控制寄存器访问
105   HAL_FLASH_Unlock();
106 
107   // DataLength必须是64位的倍数
108   for (i = 0; (i < length / 2) && (destination <= (USER_FLASH_END_ADDRESS - 8)); i++)
109   {
110     //器件电压范围应为[2.7V至3.6V],通过DOUBLEWORD操控
111     if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD, destination, *((uint64_t *)(p_source + 2*i))) == HAL_OK)
112     {
113       // 检测写入的值,如果flash内容与SRAM不匹配
114       if (*(uint64_t*)destination != *(uint64_t *)(p_source + 2*i))
115       {
116         status = FLASHIF_WRITINGCTRL_ERROR;
117         break;
118       }
119       // 增加FLASH目标地址,加8bit
120       destination += 8;
121     }
122     else
123     {
124       // 在闪存中写入数据时出错
125       status = FLASHIF_WRITING_ERROR;
126       break;
127     }
128   }
129 
130   // 锁定Flash以禁用闪存控制寄存器访问(建议保护FLASH存储器免受可能的意外操作)
131   HAL_FLASH_Lock();
132 
133   return status;
134 }

配置写保护,调用此函数,可以解锁flash写保护,允许写入数据。

144 uint32_t FLASH_If_WriteProtectionClear( void )
145 {
146   FLASH_OBProgramInitTypeDef OptionsBytesStruct1;
147   HAL_StatusTypeDef retr;
148 
149   // 解锁Flash以启用闪存控制寄存器访问
150   retr = HAL_FLASH_Unlock();
151 
152   // 解锁选项字节
153   retr |= HAL_FLASH_OB_Unlock();
154 
155   OptionsBytesStruct1.RDPLevel = OB_RDP_LEVEL_0;
156   OptionsBytesStruct1.OptionType = OPTIONBYTE_WRP;
157   OptionsBytesStruct1.WRPArea = OB_WRPAREA_BANK2_AREAA;
158   OptionsBytesStruct1.WRPEndOffset = 0x00;
159   OptionsBytesStruct1.WRPStartOffset = 0xFF;
160   retr |= HAL_FLASHEx_OBProgram(&OptionsBytesStruct1);
161 
162   OptionsBytesStruct1.WRPArea = OB_WRPAREA_BANK2_AREAB;
163   retr |= HAL_FLASHEx_OBProgram(&OptionsBytesStruct1);
164 
165   return (retr == HAL_OK ? FLASHIF_OK : FLASHIF_PROTECTION_ERRROR);
166 }

23.9 ymodem.c

这个文件里面处理的是超级终端下载app程序时的ymodem协议,具体协议代码这边不做详解,需要的朋友烦请自行阅读。

23.10 common.c

通用文件,里面分别是字符串与整数的转化,以及串口格式化打印。

整数转字符串、字符串转整数的两个函数。

 24 //******************************************************************
 25 // fn : Int2Str
 26 //
 27 // brief : 将整数转换为字符串
 28 //
 29 // param : p_str -> 字符串输出指针
 30 //         intnum -> 要转换的整数
 31 //
 32 // return : none
 33 void Int2Str(uint8_t *p_str, uint32_t intnum)
 34 {
 35   uint32_t i, divider = 1000000000, pos = 0, status = 0;
 36 
 37   for (i = 0; i < 10; i++)
 38   {
 39     p_str[pos++] = (intnum / divider) + 48;
 40 
 41     intnum = intnum % divider;
 42     divider /= 10;
 43     if ((p_str[pos-1] == '0') & (status == 0))
 44     {
 45       pos = 0;
 46     }
 47     else
 48     {
 49       status++;
 50     }
 51   }
 52 }
 53 
 54 //******************************************************************
 55 // fn : Str2Int
 56 //
 57 // brief : 将字符串转换为整数
 58 //
 59 // param : p_inputstr -> 要转换的字符串
 60 //         p_intnum -> 整数值
 61 //
 62 // return : 1正确,0错误
 63 uint32_t Str2Int(uint8_t *p_inputstr, uint32_t *p_intnum)
 64 {
 65   uint32_t i = 0, res = 0;
 66   uint32_t val = 0;
 67 
 68   if ((p_inputstr[0] == '0') && ((p_inputstr[1] == 'x') || (p_inputstr[1] == 'X')))
 69   {
 70     i = 2;
 71     while ( ( i < 11 ) && ( p_inputstr[i] != '\0' ) )
 72     {
 73       if (ISVALIDHEX(p_inputstr[i]))
 74       {
 75         val = (val << 4) + CONVERTHEX(p_inputstr[i]);
 76       }
 77       else
 78       {
 79         res = 0;
 80         break;
 81       }
 82       i++;
 83     }
 84 
 85     if (p_inputstr[i] == '\0')
 86     {
 87       *p_intnum = val;
 88       res = 1;
 89     }
 90   }
 91   else
 92   {
 93     while ( ( i < 11 ) && ( res != 1 ) )
 94     {
 95       if (p_inputstr[i] == '\0')
 96       {
 97         *p_intnum = val;
 98         res = 1;
 99       }
100       else if (((p_inputstr[i] == 'k') || (p_inputstr[i] == 'K')) && (i > 0))
101       {
102         val = val << 10;
103         *p_intnum = val;
104         res = 1;
105       }
106       else if (((p_inputstr[i] == 'm') || (p_inputstr[i] == 'M')) && (i > 0))
107       {
108         val = val << 20;
109         *p_intnum = val;
110         res = 1;
111       }
112       else if (ISVALIDDEC(p_inputstr[i]))
113       {
114         val = val * 10 + CONVERTDEC(p_inputstr[i]);
115       }
116       else
117       {
118         res = 0;
119         break;
120       }
121       i++;
122     }
123   }
124 
125   return res;
126 }

串口格式化打印数据的函数。

128 //******************************************************************
129 // fn : Serial_PutString
130 //
131 // brief : 在超级终端上打印一个字符串
132 //
133 // param : p_string -> 要打印的字符串
134 //
135 // return : none
136 void Serial_PutString(uint8_t *p_string)
137 {
138   uint16_t length = 0;
139 
140   while (p_string[length] != '\0')
141   {
142     length++;
143   }
144   HAL_UART_Transmit(&UartHandle, p_string, length, TX_TIMEOUT);
145 }
146 
147 //******************************************************************
148 // fn : Serial_PutString
149 //
150 // brief : 将一个字节传输到超级终端
151 //
152 // param : param -> 要发送的字节
153 //
154 // return : HAL_StatusTypeDef -> 如果正确则返回HAL_OK
155 HAL_StatusTypeDef Serial_PutByte( uint8_t param )
156 {
157   /* May be timeouted... */
158   if ( UartHandle.gState == HAL_UART_STATE_TIMEOUT )
159   {
160     UartHandle.gState = HAL_UART_STATE_READY;
161   }
162   return HAL_UART_Transmit(&UartHandle, &param, 1, TX_TIMEOUT);
163 }

24 实验23-串口升级(app)

此实验由实验17-定时器中断改编,主要是修改了程序烧写的flash地址,以及中断向量表,并且添加了显示屏部分,用来展示实验现象。

24.1 STM32L476 IAP简介

IAP是In Application Programming的首字母缩写,IAP是用户自己的程序在运行过程中对User Flash的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。

通常在用户需要实现IAP功能时,即用户程序运行中作自身的更新操作,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信管道(如USB、[/baike.baidu.com/item/USART USART])接收程序或数据,执行对第二部分代码的更新;第二个项目代码才是真正的功能代码。这两部分项目代码都同时烧录在User Flash中,当芯片上电后,首先是第一个项目代码开始运行,它作如下操作:

1)检查是否需要对第二部分代码进行更新

2)如果不需要更新则转到4)

3)执行更新操作

4)跳转到第二部分代码执行

第一部分代码必须通过其它手段,如[/baike.baidu.com/item/JTAG JTAG]或[/baike.baidu.com/item/ISP ISP]烧入;第二部分代码可以使用第一部分代码IAP功能烧入,也可以和第一部分代码一道烧入,以后需要程序更新是再通过第一部分IAP代码更新。

对于STM32来说,因为它的中断向量表位于程序存储器的最低地址区,为了使第一部分代码能够正确地响应中断,通常会安排第一部分代码处于Flash的开始区域,而第二部分代码紧随其后。

在第二部分代码开始执行时,首先需要把CPU的中断向量表映像到自己的向量表,然后再执行其他的操作。

如果IAP程序被破坏,产品必须返厂才能重新烧写程序,这是很麻烦并且非常耗费时间和金钱的。针对这样的需求,STM32在对Flash区域实行读保护的同时,自动地对用户Flash区的开始4页设置为写保护,这样可以有效地保证IAP程序(第一部分代码)区域不会被意外地破坏。

24.2 app工程配置简介

首先选择了BANK2(起始地址0x08020000,大小0x20000),作为app程序在flash中的存储地址。

并且配置了本工程生成.bin文件,勾选Run#1,添加:fromelf --bin --output "$L@L.bin" "#L"。

还有就是修改了程序的中断向量表地址(这个是源码详解中的main()函数中说明)。

NBDK-KEIL-IAPAPP.png
NBDK-KEIL-BIN.png

24.3 硬件设计

选择STM32L4的内部flash的BANK2,并且使用了SPI接口的显示屏、TIM2定时器和LED来展示实验现象。

24.4 实验准备

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

24.5 实验验证

请查看实验22-串口升级(bootloader)中的实验验证。

24.6 源码详解

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

24.6.1 stm32l4xx_hal_conf.h

此文件位于“实验24-低功耗待机\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们用于串口升级的app程序,所以打开SPI、TIM2等用来展示实验现象。

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
113 #define HAL_SPI_MODULE_ENABLED      // SPI
114 #define HAL_TIM_MODULE_ENABLED      // TIM定时器

24.6.2 main.c

main函数中,包括整个串口升级app工程,我们只需要关注如下部分,也就是修改中断向量表地址,其他的都不重要(其他的就是实验17+显示屏)。

// 修改中断向量表地址

SCB->VTOR = 0x08020000;

39 int main(void)
40 {
41   // 修改中断向量表地址
42   SCB->VTOR = 0x08020000;
43   
44   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
45 	// 重置所有外设、flash界面以及系统时钟
46   HAL_Init();
47 
48 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
49   SystemClock_Config();
50   
51   // 复位所有IO
52   //__HAL_RCC_AHB2_FORCE_RESET();
53   
54 	// 初始化LED引脚
55 	LED_Init();
56   
57   // 初始化tim2定时器
58 	MX_TIM2_Init();
59   
60   // LCD SPI初始化
61   LCD_GPIO_Init();  // LCD IO控制引脚(例如背光)
62   MX_SPI1_Init();   // LCD SPI控制引脚
63 
64   // 图形界面初始化
65   GUI_Init();       // GUI界面初始化
66   GUI_Clear();      // 清屏
67   
68   // 打印logo到位置X->0,Y->0
69   GUI_DrawBitmap(&bmLogo,0,0);
70   
71   GUI_DispStringAt("<<RUN IAP APP >>\r\n",0,80);
72   // 
73   while (1)
74   {
75     
76   }
77 }

25 实验24-低功耗待机

STM32L476芯片作为一颗低功耗的ST芯片,我们不能浪费了它的低功耗才能。在这一章和下一章我们将分别给大家展示一下他的两种常用的低功耗模式配置:待机模式、停止模式。

25.1 STM32L476 待机模式简介

从下面的图上我们可以看出,STM32L476有多种运行模式,分别为:

• Run 运行模式

• Sleep 休眠模式

• Low-power run 低功耗运行模式

• Low-power sleep 低功耗休眠模式

• Stop0 停止模式0

• Stop1 停止模式1

• Stop2 停止模式2

• Standby 待机模式

• Shutdown 关机模式

NBDK-DS-SLEEPMODE.png

其中我们常用的低功耗模式为:Standby待机模式以及stop停止模式(因为这两种模式在实际的应用场景中比较实用)。

这一章节主要讲解Standby待机模式:

待机模式允许通过BOR实现最低功耗。 它基于Cortex®-M4深度睡眠模式,禁用稳压器(保留SRAM2内容时除外)。 PLL,HSI16,MSI和HSE振荡器也被关闭。除了备份域和备用电路中的寄存器外,SRAM1和寄存器内容都会丢失。 如果在PWR_CR3寄存器中设置了比特RRS,则可以保留SRAM2内容。 在这种情况下,低功耗稳压器为ON,仅为SRAM2提供电源。BOR始终处于待机模式。 当使用高于VBOR0的阈值时,消耗增加。

如何进出Standby待机模式(代码中的具体体现)
进入Standby模式的流程 退出Standby模式的流程
1.复位RTC和备份区域

2.清除唤醒标志,重新设置唤醒引脚

3.调用进入standby模式的函数

1.唤醒引脚外部中断触发

2.程序从main()函数处重启

STM32L476RC唤醒引脚说明
WakeUP引脚编号 对应引脚名称
PWR_CR3_EWUP1 PA0
PWR_CR3_EWUP2 PC13
PWR_CR3_EWUP3
PWR_CR3_EWUP4 PA2
PWR_CR3_EWUP5 PC5

25.2 硬件设计

选择STM32L4的电源控制。

25.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 使用Keil打开基础实验的实验24-低功耗待机。
  4. 下载程序,并完成功能测试。

25.4 实验验证

下载完成后,实现现象分为如下几种情况:

1.PC5上拉高电平,此时按下复位按键重新启动程序,可以看到:LED先缓慢闪烁2次后,快速周期闪烁

现象分析:程序上电检测到PC5为高电平,程序正常运行,模块没有进入低功耗待机模式。

2.PC5下拉低电平,此时按下复位按键重新启动程序,可以看到:LED缓慢闪烁2次后,停止闪烁

现象分析:程序上电检测到PC5为低电平,模块进入低功耗待机模式。

3.先执行完现象2。然后我们将PC5上拉高电平唤醒模块,唤醒后PC5保持低电平,可以看到:LED缓慢闪烁2次后,停止闪烁

现象分析:执行完现象2,模块进入低功耗待机模式后,我们利用PC5引脚上拉唤醒模块,程序重新从main()函数处重新启动,检测到PC5此时为低电平,模块重新进入到低功耗待机。

4.先执行完现象2。然后我们将PC5上拉高电平唤醒模块,唤醒后PC5保持高电平,可以看到:LED先缓慢闪烁2次后,快速周期闪烁

现象分析:执行完现象2,模块进入低功耗待机模式后,我们利用PC5引脚上拉唤醒模块,程序重新从main()函数处重新启动,检测到PC5此时为高电平,程序正常运行,模块没有进入到低功耗待机模式。

25.5 源码详解

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

25.5.1 stm32l4xx_hal_conf.h

此文件位于“实验24-低功耗待机\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的电源控制部分,PWR宏定义是每个例程都需要使能的。

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

25.5.2 main.c

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

接下来我们初始化了LED,在这个例程中LED是用于展示实验现象的。程序上电,或者从待机模式中唤醒重新启动,都会缓慢闪烁2次。而后程序如果没有进入待机模式,则LED 500ms周期闪烁。

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   // 初始化LED
46   LED_Init();
47   
48   // 控制LED闪烁2次,展示待机唤醒后会复位
49   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
50   HAL_Delay(1000);
51   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
52   HAL_Delay(1000);
53   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
54   HAL_Delay(1000);
55   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
56   
57   // 进入待机模式,初始化唤醒引脚
58 	WKUP_Init();
59   
60   // 
61   while (1)
62   {
63     HAL_Delay(500);
64     HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);   // 控制LED引脚状态取反,LED闪烁
65   }
66 }

25.5.3 gyu_util.c

请参照实验01中的介绍。

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

25.5.4 gyu_standby.c

系统进入待机模式的函数,在系统进入待机模式之前,为了将功耗降到最低,我们将复位所有的IO,禁止所有IO口的时钟,并且复位RTC和备份区域,为了后续能够通过唤醒引脚唤醒,我们使能一下唤醒引脚,最后调用系统函数进入待机模式。

34 void Sys_Enter_Standby(void)
35 {  
36 	__HAL_RCC_AHB2_FORCE_RESET();       // 复位所有IO口
37   __HAL_RCC_GPIOC_CLK_DISABLE();
38   __HAL_RCC_GPIOD_CLK_DISABLE();
39   __HAL_RCC_GPIOA_CLK_DISABLE();
40   __HAL_RCC_GPIOB_CLK_DISABLE();
41   
42   __HAL_RCC_PWR_CLK_ENABLE();         // 使能PWR时钟
43   
44   // 复位RTC和备份区域
45   __HAL_RCC_BACKUPRESET_FORCE();      // 复位备份区域
46   HAL_PWR_EnableBkUpAccess();         // 后备区域访问使能 
47   __HAL_RCC_BACKUPRESET_RELEASE();    // 备份区域复位结束
48   
49   // 清楚唤醒标志位,设置唤醒引脚
50   HAL_PWR_DisableWakeUpPin(PWR_WAKEUP_PIN5);  // 禁用WKUP唤醒,WKUP5 = PC5
51   __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);          // 清除Wake_UP标志
52   HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN5);   // 设置WKUP用于唤醒,WKUP5 = PC5
53   
54   HAL_PWR_EnterSTANDBYMode();                 // 进入待机模式
55 }

PC5中断函数以及外部中断触发时的回调函数。

58 // EXTI line5 中断函数
59 void EXTI9_5_IRQHandler(void)
60 {
61   HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_5);
62 }
63 
64 //******************************************************************
65 // fn : HAL_GPIO_EXTI_Callback
66 //
67 // brief : 外部中断回调函数
68 //
69 // param : GPIO_Pin-> 引脚编号
70 //
71 // return : none
72 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
73 {
74   // 
75   if(GPIO_Pin == GPIO_PIN_5)
76   {
77     
78   }
79 }

上电后检测PC5电平,用于判断是否进入低功耗待机的函数。

89 uint8_t PWR_Check_Standby(void) 
90 {
91   if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_5))  // 采集PC5引脚电压
92   {
93     return 1;
94   }
95   else 
96   {
97     return 0;
98   }
99 }

待机模式初始化函数,包含了对于唤醒引脚PC5的初始化和中断使能,最后根据检测的PC5电平判断是否进入低功耗待机模式。

109 void WKUP_Init(void)
110 {
111   // 定义GPIO结构体
112   GPIO_InitTypeDef GPIO_InitStruct;
113 
114   // 使能GPIOC引脚时钟(引脚:PC5)
115   __HAL_RCC_GPIOC_CLK_ENABLE();
116   
117   // 配置按键引脚
118   GPIO_InitStruct.Pin = GPIO_PIN_5;             // 选择PC5
119   GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;   // 上升沿中断触发
120   GPIO_InitStruct.Pull = GPIO_PULLDOWN;         // 下拉
121   GPIO_InitStruct.Speed = GPIO_SPEED_FAST;      // 快速
122   HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);       // 初始化引脚
123 	
124   // 配置PC5的上升沿中断,也就是EXTI line5  
125 	HAL_PWREx_EnableGPIOPullDown(PWR_GPIO_C, PWR_GPIO_BIT_5);
126 	HAL_PWREx_EnablePullUpPullDownConfig();
127   HAL_NVIC_SetPriority(EXTI9_5_IRQn, 10, 0);
128   HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
129   
130   // 检查是否是正常开机
131   if(PWR_Check_Standby()==0)
132   {
133     Sys_Enter_Standby();  // 不是开机,进入待机模式
134   }
135 }

26 实验25-低功耗停止

上一章我们给大家展示了STM32L4的低功耗待机模式,这一章给大家带来的是低功耗停止模式,我们用于展示的是stop1模式。

26.1 STM32L476 停止模式简介

从下面的图上我们可以看出,STM32L476有多种运行模式,分别为:

• Run 运行模式

• Sleep 休眠模式

• Low-power run 低功耗运行模式

• Low-power sleep 低功耗休眠模式

• Stop0 停止模式0

• Stop1 停止模式1

• Stop2 停止模式2

• Standby 待机模式

• Shutdown 关机模式

NBDK-DS-SLEEPMODE.png

其中我们常用的低功耗模式为:Standby待机模式以及stop停止模式(因为这两种模式在实际的应用场景中比较实用)。

这一章节主要讲解Stop停止模式:

Stop 0模式基于Cortex®-M4深度睡眠模式和外设时钟门控。 电压调节器配置为主调节器模式。 在Stop 0模式下,VCORE域中的所有时钟都停止; PLL,MSI,HSI16和HSE振荡器被禁用。 一些具有唤醒功能的外设(I2Cx(x = 1,2,3),U(S)ARTx(x = 1,2 ... 5)和LPUART)可以打开HSI16接收帧,如果不是唤醒帧,则在接收帧后关闭HSI16。 在这种情况下,HSI16时钟仅传播到请求它的外设。 保留SRAM1,SRAM2和寄存器内容。 BOR始终在Stop 0模式下可用。 当使用高于VBOR0的阈值时,消耗增加。

停止1模式与停止0模式相同,只是主调节器关闭,只有低功率调节器开启。 可以从运行模式和低功耗运行模式进入停止1模式。

如何进出Stop停止模式(代码中的具体体现)
进入Stop模式的流程 退出Stop模式的流程
1.关闭程序中的IO和外设功能(我们这边关闭LED)

2.调用进入stop模式的函数

1.唤醒引脚外部中断触发

2.配置系统时钟源 3.程序从外部中断回调处继续运行

26.2 硬件设计

选择STM32L4的电源控制。

26.3 实验准备

  1. 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
  2. 使用miniUSB线,连接PC与开发板USB接口。
  3. 使用Keil打开基础实验的实验25-低功耗停止。
  4. 下载程序,并完成功能测试。

26.4 实验验证

下载完成后,按下RST复位,可以看到LED闪烁一次后停止。然后我们将PC5引脚上拉,唤醒模块,此时LED 500ms周期闪烁。

26.5 源码详解

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

26.5.1 stm32l4xx_hal_conf.h

此文件位于“实验25-低功耗停止\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的电源控制部分,PWR宏定义是每个例程都需要使能的。

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

26.5.2 main.c

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

接下来我们初始化了LED,在这个例程中LED是用于展示实验现象的。程序上电,LED会闪烁1次,而后程序进入停止模式,可以LED熄灭。等待PC5中断唤醒模块,LED将500ms周期闪烁。

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   // 初始化LED,LED点亮
46   LED_Init();
47   
48   // 上电状态指示,LED闪烁一次,最后为熄灭状态
49   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
50   HAL_Delay(1000);
51   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
52   
53   // 进入停止模式,初始化唤醒引脚
54   WKUP_Init();
55   
56   // 
57   while (1)
58   {
59     HAL_Delay(500);
60     HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);   // 控制LED引脚状态取反,LED闪烁
61   }
62 }

26.5.3 gyu_util.c

请参照实验01中的介绍。

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

26.5.4 gyu_stop.c

系统进入停止模式的函数,为了将STM32的功耗降到最低,我们关闭当前开启的LED指示灯,然后调用进入停止模式的函数。

34 void Sys_Enter_Stop(void)
35 { 
36   // 为了将功耗降到最低,将LED引脚恢复初始化状态
37   LED_DeInit();
38 
39   // 进入停止模式(这边配置的是STOP1)
40   HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON,PWR_STOPENTRY_WFI);  
41 }

重新配置系统时钟的函数,当系统从STOP模式下被唤醒的时候,我们需要调用此函数去重新配置系统的时钟。

51 void SYSCLKConfig_STOP(void)
52 {
53   // 使能HSE
54   __HAL_RCC_HSE_CONFIG(RCC_HSE_ON);
55 
56   // 等待HSE准备就绪
57   while (__HAL_RCC_GET_FLAG(RCC_FLAG_HSERDY) == RESET);
58 
59   // 配置PLL时钟,并且使能
60   __HAL_RCC_PLL_DISABLE();
61   __HAL_RCC_PLL_CONFIG(RCC_PLLSOURCE_HSE,1,20,RCC_PLLP_DIV7,RCC_PLLQ_DIV2,RCC_PLLR_DIV2);
62   __HAL_RCC_PLL_ENABLE();
63   
64   // 等待PLL准备就绪
65   while (__HAL_RCC_GET_FLAG(RCC_FLAG_PLLRDY) == RESET);
66   
67   // 配置PLL 80MHz输出
68   __HAL_RCC_PLLCLKOUT_ENABLE(RCC_PLL_SYSCLK);
69   
70   // 选择PLL作为系统时钟源
71   __HAL_RCC_SYSCLK_CONFIG(RCC_SYSCLKSOURCE_PLLCLK);
72 }

PC5中断函数,以及中断触发的回调函数。因为中船唤醒模块之后,程序是从此回调函数中继续执行的,所以我们需要重新初始化LED,并且重新配置系统时钟。

 75 // EXTI line5 中断函数
 76 void EXTI9_5_IRQHandler(void)
 77 {
 78   HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_5);
 79 }
 80 
 81 //******************************************************************
 82 // fn : HAL_GPIO_EXTI_Callback
 83 //
 84 // brief : 外部中断回调函数
 85 //
 86 // param : GPIO_Pin-> 引脚编号
 87 //
 88 // return : none
 89 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
 90 {
 91   // 
 92   if(GPIO_Pin == GPIO_PIN_5)
 93   {
 94     // 重新初始化LED
 95     LED_Init();
 96     // 从停止模式下唤醒,重新配置时钟
 97     SYSCLKConfig_STOP();
 98         
 99   }
100 }

停止模式初始化函数,我们首先初始化了唤醒引脚PC5,并且使能了它的中断,最后调用进入停止模式的函数。

109 void WKUP_Init(void)
110 {
111   // 定义GPIO结构体
112   GPIO_InitTypeDef GPIO_InitStruct;
113   
114   // 使能GPIOC引脚时钟(引脚:PC5)
115   __HAL_RCC_GPIOC_CLK_ENABLE();
116   
117   // 配置按键引脚
118   GPIO_InitStruct.Pin = GPIO_PIN_5;             // 选择PC5
119   GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;   // 上升沿中断触发
120   GPIO_InitStruct.Pull = GPIO_PULLDOWN;         // 下拉
121   GPIO_InitStruct.Speed = GPIO_SPEED_FAST;      // 快速
122   HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);       // 初始化引脚
123   
124   HAL_NVIC_SetPriority(EXTI9_5_IRQn, 10, 0);
125   HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
126   
127   Sys_Enter_Stop();  // 进入停止模式
128 }

27 实验26-低功耗串口

前两章我们分别给大家讲解了低功耗待机和低功耗停止,但是都没有添加任何外设功能,这一章我们将给大家带来低功耗串口的例程。这一章的内容实际是实验25-低功耗停止和实验12-串口DMA的结合,所以本章我们仅重点介绍串口在进出低功耗停止模式时应做的处理,有关串口和停止模式,请大家查看之前对应的实验章节说明。

27.1 STM32L476 低功耗停止模式下的串口使用简介

首先有一点我们需要明确,这一章我们使用的是STM32L4的USART,而不是LPUART。

本章的重点在于,USART在模块准备进入低功耗停止模式和退出停止模式时应做的处理:

USART进出低功耗停止模式处理流程
进入停止模式前 退出停止模式后
1.因为我们使用的是DMA串口,所以先暂停DMA接收

2.将串口TX及RX恢复为默认IO配置(LED也同样恢复默认IO配置)

3.调用系统进入停止模式的函数

1.重新配置串口引脚(LED引脚也重新配置)

2.恢复DMA接收

3.重新配置系统时钟

27.2 硬件设计

选择STM32L4的电源控制以及串口。

NBDK-SCH-UART-USB.png

27.3 实验准备

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

27.4 实验验证

下载完成后,按下RST复位,可以看到LED闪烁一次后停止。我们将PC5引脚上拉,唤醒模块,然后我们便可以通过Xshell向开发板串口发送数据,串口接收到数据之后,会将此数据打印到Xshell显示(没成功接收打印一次串口数据,LED也会跟着翻转一下状态)。

NBDK-XSHELL-LPUSART.png

27.5 源码详解

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

27.5.1 stm32l4xx_hal_conf.h

此文件位于“实验25-低功耗停止\Inc”路径中,主要用途是选择使能此例程使用到的库文件。

此例程我们主要给大家展示STM32L4的电源控制部分以及串口的使用,所以我们打开PWR和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

27.5.2 main.c

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

我们初始化LED,并且在上电的时候让它闪烁一下,用来确认程序已经开始运行。

接下来我们初始化串口及DMA相关的部分。

然后我们设置进入停止模式。

在while()循环中,我们轮询是否有接收到串口数据,如果接收到数据,则将接收到的数据打印到串口显示,并且翻转一下LED状态。

39 int main(void)
40 {
41   uint16_t msgLen = 0;
42   
43   /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
44 	// 重置所有外设、flash界面以及系统时钟
45   HAL_Init();
46 
47 	// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)
48   SystemClock_Config();
49 	
50   // 初始化LED,LED点亮
51   LED_Init();
52   
53   // 上电状态指示,LED闪烁一次,最后为熄灭状态
54   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
55   HAL_Delay(1000);
56   HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
57   
58   
59   // 初始化DMA
60   MX_DMA_Init();
61   
62   // 初始化串口USART1
63   MX_USART1_UART_Init();
64   
65   // 串口DMA初始化
66   HAL_UARTDMA_Init();
67   
68   // 进入停止模式,初始化唤醒引脚
69   WKUP_Init();
70   
71   // 
72   while (1)
73   {
74     // 轮训串口是否有数据
75     if(HAL_UART_Poll())
76     {
77       msgLen = HAL_UART_RxBufLen();   // 获取当前串口缓冲区的数据长度
78       // 超过100字节长度的部分不获取
79       if(msgLen > APP_BUF_LEN)
80       {
81         msgLen = APP_BUF_LEN;
82       }
83       msgLen = HAL_UART_Read(app_buf,msgLen);   // 获取串口数据
84       HAL_UART_Write(app_buf,msgLen);   // 将获取的数据通过串口TX打印显示
85       
86       HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
87     }
88   }
89 }

27.5.3 gyu_stop.c

这个文件在实验25-低功耗停止章节中已经讲解过了,这边我们仅说明一下串口在进出停止模式时要做的处理。

进入停止模式前,我们需要先暂停DMA接收,并且将串口恢复为默认的IO配置,然后才进入停止模式。

35 void Sys_Enter_Stop(void)
36 { 
37   // 为了将功耗降到最低,将LED引脚恢复初始化状态
38   LED_DeInit();
39   
40   // 暂停DMA接收,将串口引脚恢复初始化状态
41   HAL_UARTDMA_Pause();
42   USART1_DeInit();
43   
44   // 进入停止模式(这边配置的是STOP1)
45   HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON,PWR_STOPENTRY_WFI);  
46 }

退出停止模式,并且恢复正常运行模式后,我们重新初始化硬件串口引脚,并且使能串口DMA接收。

 94 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
 95 {
 96   // 
 97   if(GPIO_Pin == GPIO_PIN_5)
 98   {
 99     // 重新初始化LED
100     LED_Init();
101     HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_15);
102     
103     // 恢复串口引脚硬件配置,恢复DMA接收
104     USART1_ReInit();
105     HAL_UARTDMA_Resume();
106     
107     // 从停止模式下唤醒,重新配置时钟
108     SYSCLKConfig_STOP();
109   }
110 }

本PDF由谷雨文档中心自动生成,点击下方链接阅读最新内容。

取自“http://doc.iotxx.com/index.php?title=NBDK-L4:基础实验教程&oldid=1677