打开主菜单

谷雨文档中心 β

更改

NBDK-L4:基础实验教程

删除51,687字节2019年2月19日 (二) 10:35
无编辑摘要
==== gyu_util.c ====
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。请参照实验01中的介绍。
主要包含了三个部分的初始化配置。基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。# 内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。==== gyu_motor.c ====# 时钟配置,选择系统、AHB总线及APB总线的时钟来源。马达引脚初始化函数,初始化PC7推挽输出低电平。<syntaxhighlight lang="c++" line="1" start="31"># 外设时钟配置,选择外设时钟来源。void Motor_Init(void)为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。{ GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。<syntaxhighlight lang="c++" line="1" start="49">void SystemClock_Config __HAL_RCC_GPIOC_CLK_ENABLE(void){ RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 // 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);使能GPIOC时钟
// 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct GPIO_InitStructure.OscillatorType Pin= RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSEGPIO_PIN_7; // 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE引脚编号为7 RCC_OscInitStruct GPIO_InitStructure.HSEState Mode = RCC_HSE_ONGPIO_MODE_OUTPUT_PP; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE推挽输出 RCC_OscInitStruct GPIO_InitStructure.LSEState Speed = RCC_LSE_ONGPIO_SPEED_FREQ_LOW; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL低频率 RCC_OscInitStruct GPIO_InitStructure.PLL.PLLState Pull = RCC_PLL_ONGPIO_PULLUP; // 打开PLL上拉 RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE HAL_GPIO_Init(GPIOC, &GPIO_InitStructure); // 选择HSE时钟作为PLL入口时钟源,8MHz初始化PC7 RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, GPIO_PIN_RESET); // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz设置PC7默认输出低电平 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz} RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; <// SDMMC、RNG、USB时钟2分频,160/2 syntaxhighlight>马达引脚电平设置函数,设置为高电平,马达起振,设置低电平,马达停止。<syntaxhighlight lang= 80MHz RCC_OscInitStruct.PLL.PLLR "c" line= RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 "1" start= 80MHz // RCC时钟配置,出错则进入错误处理函数"54"> if (HAL_RCC_OscConfigvoid Motor_SET(&RCC_OscInitStruct) != HAL_OKGPIO_PinState pinSate){ { _Error_Handler HAL_GPIO_WritePin(__FILE__GPIOC, GPIO_PIN_7, __LINE__pinSate); } // 初始化CPU,AHB和APB总线时钟设置PC7输出 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK} |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; /</ 配置系统时钟为PLLCLK输入,80MHzsyntaxhighlight> RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK实验03-蜂鸣器 =80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4) { _Error_Handler(__FILE__, __LINE__); }蜂鸣器实验通过控制 GPIO 引脚输出高低电平,用于控制蜂鸣器发出蜂鸣声或者停止发声 。
// 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 STM32L476 IO简介 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; /每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz O切换。 如果需要,可以锁定I /O备用功能配置序列,以避免虚假写入I / 外设时钟配置,出错则进入错误处理函数 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }O寄存器。
// 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是: if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)# 输入浮空 {# 输入上拉 _Error_Handler(__FILE__, __LINE__);# 输入下拉 }# 模拟# 带上拉或下拉的开漏输出# 带上拉或下拉的推挽输出# 带上拉或下拉的复用功能推挽# 带上拉或下拉的复用功能开漏
// 配置系统定时器中断时间,配置为HCLK的千分频=== 硬件设计 === HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);选择STM32L4引脚PB2作为蜂鸣器的控制引脚,PB2高电平时蜂鸣器发出蜂鸣声。[[文件:NBDK-SCH-BUZZER.png|居中|无框|344x344像素]]
// 配置系统定时器,配置为HCLK=== 实验准备 === HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用Keil打开基础实验 03-蜂鸣器实验工程。# 下载程序,并完成功能测试。
// 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)=== 实验验证 === HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight>下载完成后,按下开发板上按键S1,蜂鸣器发声,按下S3,蜂鸣器停止。
===源码详解 = gyu_motor.c ====马达引脚初始化函数,初始化PC7推挽输出低电平。<syntaxhighlight lang="c++" line="1" start="31">void Motor_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
__HAL_RCC_GPIOC_CLK_ENABLE(); // 使能GPIOC时钟==== stm32l4xx_hal_conf.h ====此文件位于“03-蜂鸣器实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
GPIO_InitStructure.Pin= GPIO_PIN_7; // 引脚编号为7 GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW; // 低频率 GPIO_InitStructure.Pull = GPIO_PULLUP; // 上拉 HAL_GPIO_Init(GPIOC, &GPIO_InitStructure); // 初始化PC7 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, GPIO_PIN_RESET); // 设置PC7默认输出低电平}</syntaxhighlight>马达引脚电平设置函数,设置为高电平,马达起振,设置低电平,马达停止。此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。<syntaxhighlight lang="c" line="1" start="54103">void Motor_SET(GPIO_PinState pinSate)// 使能的宏#define HAL_MODULE_ENABLED // 芯片{ #define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟 HAL_GPIO_WritePin(GPIOC, GPIO_PIN_7, pinSate); #define HAL_CORTEX_MODULE_ENABLED // 设置PC7输出}NVIC
#define HAL_GPIO_MODULE_ENABLED // GPIO
</syntaxhighlight>
== 实验03-蜂鸣器 == main.c ====蜂鸣器实验通过控制 GPIO 引脚输出高低电平,用于控制蜂鸣器发出蜂鸣声或者停止发声 。main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
=== STM32L476 IO简介 ===每个GPIO引脚都可以通过软件配置为输出(推挽或漏极开路),输入(带或不带上拉或下拉)或外设备用功能。 大多数GPIO引脚与数字或模拟备用功能共用。 由于它们在AHB2总线上的映射,可以实现快速I / O切换。 如果需要,可以锁定I / O备用功能配置序列,以避免虚假写入I / O寄存器。接下来是初始化按键,有关按键的部分会在04-按键实验中给大家讲解。
经过上一段对GPIO口模式的说明,在这里对它的工作模式进行一个小结,它一共有八种组合,即有八种可配置的工作模式,分别是:# 输入浮空# 输入上拉# 输入下拉# 模拟# 带上拉或下拉的开漏输出# 带上拉或下拉的推挽输出# 带上拉或下拉的复用功能推挽# 带上拉或下拉的复用功能开漏最后我们初始化蜂鸣器引脚,配置蜂鸣器引脚为默认输出低电平。
<syntaxhighlight lang="c++" line="1" start= 硬件设计 ==="36">int main(void)选择STM32L4引脚PB2作为蜂鸣器的控制引脚,PB2高电平时蜂鸣器发出蜂鸣声。{[[文件:NBDK-SCH-BUZZER /* Reset of all peripherals, Initializes the Flash interface and the Systick.png|居中|无框|344x344像素]]*/ // 重置所有外设、flash界面以及系统时钟 HAL_Init();
=== 实验准备 === // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。 SystemClock_Config();# 使用Keil打开基础实验 03-蜂鸣器实验工程。 # 下载程序,并完成功能测试。 // 初始化按键引脚 MX_KEY_Init();=== 实验验证 === 下载完成后,按下开发板上按键S1,蜂鸣器发声,按下S3,蜂鸣器停止。 //注册按钮回调函数 KEY_RegisterCb(AppKey_cb);=== 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==== stm32l4xx_hal_conf.h ====此文件位于“03-蜂鸣器实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。 此例程因为我们需要展示IO的使用,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。<syntaxhighlight lang="c" line="1" start="103"> // 使能的宏初始化蜂鸣器#define HAL_MODULE_ENABLED // 芯片 Buzzer_Init(); #define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源 while (1)#define HAL_RCC_MODULE_ENABLED // 时钟 {#define HAL_CORTEX_MODULE_ENABLED KEY_Poll(); // NVIC按键轮训,监测是否有按键被按下 }#define HAL_GPIO_MODULE_ENABLED // GPIO}</syntaxhighlight> ==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 接下来是初始化按键,有关按键的部分会在04-按键实验中给大家讲解。 最后我们初始化蜂鸣器引脚,配置蜂鸣器引脚为默认输出低电平。 在按键的处理回调函数中,我们可以看到,按键S1(UP)按下后,设置蜂鸣器引脚高电平,按键S3(DOWN)按下后,设置蜂鸣器引脚低电平<syntaxhighlight lang="c++" line="1" start="3669">int mainvoid AppKey_cb(voiduint8_t key)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟 HAL_Init();  // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config(); // 初始化按键引脚 MX_KEY_Init(); //注册按钮回调函数 KEY_RegisterCb(AppKey_cb); //初始化蜂鸣器 Buzzer_Init(); // while (1) { KEY_Poll(); // 按键轮训,监测是否有按键被按下 }}</syntaxhighlight>在按键的处理回调函数中,我们可以看到,按键S1(UP)按下后,设置蜂鸣器引脚高电平,按键S3(DOWN)按下后,设置蜂鸣器引脚低电平<syntaxhighlight lang="c" line="1" start="69">void AppKey_cb(uint8_t key){ // 如果有相应按键被按下,则串口打印调试信息 if(key & KEY_UP) { Motor_SET(GPIO_PIN_SET);
}
if(key & KEY_LEFT)
==== gyu_util.c ====
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。请参照实验01中的介绍。
主要包含了三个部分的初始化配置。基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。==== gyu_buzzer.c ====蜂鸣器引脚初始化函数,初始化PB2推挽输出低电平。<syntaxhighlight lang="c++" line="1" start="31">void Buzzer_Init(void){ GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。 __HAL_RCC_GPIOB_CLK_ENABLE(); // 使能GPIOB时钟
2 GPIO_InitStructure.时钟配置,选择系统、AHB总线及APB总线的时钟来源。Pin= GPIO_PIN_2; // 引脚编号为2 GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW; // 低频率 GPIO_InitStructure.Pull = GPIO_PULLUP; // 上拉 HAL_GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化PB2 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // 设置PB2默认输出低电平}</syntaxhighlight>蜂鸣器引脚电平设置函数,设置为高电平,蜂鸣器发出蜂鸣声,设置低电平,蜂鸣器停止。<syntaxhighlight lang="c" line="1" start="54">void Buzzer_SET(GPIO_PinState pinSate){ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, pinSate); // 设置PB2输出}</syntaxhighlight>== 实验04-按键中断 ==按键中断实验,是通过外部引脚中断来判断是否有按键被按下,按键部分的代码这个在马达以及蜂鸣器实验中其实已经展示过了,这边给大家做一个详细的讲解。我们分别选择PC0、PC1、PC2、PC3这4个引脚作为我们的按键引脚,对应EXTI line0、EXTI line1、EXTI line2、EXTI line3。 === STM32L476 外部中断简介 ===首先我们看一下外部中断/事件的GPIO映射图。[[文件:NBDK-DS-EXTI.png|边框|居中|无框|606x606像素]]
3.外设时钟配置,选择外设时钟来源。由上面的映射图可以知道,多个GPIO引脚(GPIOA、GPIOB、GPIOC、GPIOD等等的GPIO_Pin_0)都会触发同一个中断线(EXTI line0)。也就是说,当EXTI0被触发时,我们无法判断他是PA0触发,还是PB0触发,因此大家在设计自己的硬件的时候,需要选择合适的中断引脚。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。源码中我们配置外部中断的步骤如下:
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。<syntaxhighlight lang="c++" line="1" start="49">void SystemClock_Config(void){ RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 // 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);.使能GPIO时钟
// 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHzGPIO初始化,配置GPIO的边沿触发条件 // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); } // 初始化CPU,AHB和APB总线时钟 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct3.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz设置EXTI线,配置GPIO与EXTI的关系 // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=34.3V,所以选择SW4(FLASH_LATENCY_4) { _Error_Handler(__FILE__, __LINE__); }中断向量初始化
// 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz PeriphClkInit.Lpuart1ClockSelection 硬件设计 = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义选择STM32L4引脚PC0、PC1、PC2、PC3作为按键的控制引脚。 PeriphClkInit[[文件:NBDK-SCH-BUTTON.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHzpng|边框|居中|无框|434x434像素]] PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 实验准备 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)# 将SW1拨到DBG端,SW2拨到MCU。 {# 使用Keil打开基础实验 04-按键中断实验工程。 _Error_Handler(__FILE__, __LINE__);# 使用Xshell打开Jlink虚拟出的COM口 }# 下载程序,并完成功能测试。
// 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)== 实验验证 === {下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下: _Error_Handler(__FILE__, __LINE__); }[[文件:NBDK-XSHELL-BTN.png|边框|居中|无框|759x759像素]]
// 配置系统定时器中断时间,配置为HCLK的千分频=== 源码详解 === HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==== stm32l4xx_hal_conf.h ====此文件位于“04-按键中断实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。
此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。<syntaxhighlight lang="c" line="1" start="103">// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 配置系统定时器,配置为HCLK时钟#define HAL_CORTEX_MODULE_ENABLED HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);// NVIC
#define HAL_GPIO_MODULE_ENABLED // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)GPIO HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);#define HAL_DMA_MODULE_ENABLED // DMA}#define HAL_UART_MODULE_ENABLED // UART
</syntaxhighlight>
==== gyu_buzzermain.c ====蜂鸣器引脚初始化函数,初始化PB2推挽输出低电平。<syntaxhighlight lang="c++" line="1" start="31">void Buzzer_Initmain函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config(void)函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。{ GPIO_InitTypeDef GPIO_InitStructure; // 定义引脚参数结构体接下来我们初始化了串口部分,目的是打印按键按下的调试信息。
__HAL_RCC_GPIOB_CLK_ENABLE(); // 使能GPIOB时钟接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。
GPIO_InitStructure.Pin= GPIO_PIN_2; // 引脚编号为2 GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出 GPIO_InitStructure在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.Speed = GPIO_SPEED_FREQ_LOW; // 低频率 GPIO_InitStructurec中会将按键信息,通过上面说的回调函数,传到应用层(mian.Pull = GPIO_PULLUP; // 上拉 HAL_GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化PB2 HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET); // 设置PB2默认输出低电平}</syntaxhighlight>蜂鸣器引脚电平设置函数,设置为高电平,蜂鸣器发出蜂鸣声,设置低电平,蜂鸣器停止。<syntaxhighlight lang="c" line="1" start="54">void Buzzer_SET(GPIO_PinState pinSate){ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, pinSate); // 设置PB2输出}</syntaxhighlight>== 实验04-按键中断 ==按键中断实验,是通过外部引脚中断来判断是否有按键被按下,按键部分的代码这个在马达以及蜂鸣器实验中其实已经展示过了,这边给大家做一个详细的讲解。我们分别选择PC0、PC1、PC2、PC3这4个引脚作为我们的按键引脚,对应EXTI line0、EXTI line1、EXTI line2、EXTI line3。c)中进行处理。
<syntaxhighlight lang="c++" line="1" start= STM32L476 外部中断简介 ==="36">int main(void){ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */首先我们看一下外部中断 /事件的GPIO映射图。/ 重置所有外设、flash界面以及系统时钟[[文件:NBDK-DS-EXTI.png|边框|居中|无框|606x606像素]] HAL_Init();
由上面的映射图可以知道,多个GPIO引脚(GPIOA、GPIOB、GPIOC、GPIOD等等的GPIO_Pin_0)都会触发同一个中断线(EXTI line0)。也就是说,当EXTI0被触发时,我们无法判断他是PA0触发,还是PB0触发,因此大家在设计自己的硬件的时候,需要选择合适的中断引脚。 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config();源码中我们配置外部中断的步骤如下: // 初始化USART11.使能GPIO时钟 MX_USART1_UART_Init(); // 初始化按键引脚 MX_KEY_Init(); //注册按钮回调函数 KEY_RegisterCb(AppKey_cb); 2.GPIO初始化,配置GPIO的边沿触发条件 // while (1)3.设置EXTI线,配置GPIO与EXTI的关系 { KEY_Poll(); // 按键轮训,监测是否有按键被按下4.中断向量初始化 }}</syntaxhighlight>在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。<syntaxhighlight lang="c" line="1" start= 硬件设计 ==="69">void AppKey_cb(uint8_t key){ // 如果有相应按键被按下,则串口打印调试信息 if(key & KEY_UP) { printf("key_up press\r\n"); } if(key & KEY_LEFT) { printf("key_left press\r\n"); } if(key & KEY_DOWN) {选择STM32L4引脚PC0、PC1、PC2、PC3作为按键的控制引脚。 printf("key_down press\r\n");[[文件:NBDK-SCH-BUTTON.png|边框|居中|无框|434x434像素]] }=== 实验准备 === if(key & KEY_RIGHT)# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。 {# 将SW1拨到DBG端,SW2拨到MCU。 printf("key_right press\r\n");# 使用Keil打开基础实验 04-按键中断实验工程。 }# 使用Xshell打开Jlink虚拟出的COM口}# 下载程序,并完成功能测试。</syntaxhighlight>
=== 实验验证 = gyu_util.c ====下载完成后,分别按下开发板上的S1、S2、S3、S4按键,可以看到Xshell中Jlink虚拟的COM口分别打印如下:[[文件:NBDK-XSHELL-BTN.png|边框|居中|无框|759x759像素]]请参照实验01中的介绍。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。=== 源码详解 =gyu_key.c ==本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。<syntaxhighlight lang="c++" line= stm32l4xx_hal_conf.h ==="1" start="75">void MX_KEY_Init(void){ // 定义GPIO结构体此文件位于“04-按键中断实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件,一般情况下,我们默认需要使用的为前5个,包含芯片、flash、电源、时钟以及NVIC。 GPIO_InitTypeDef GPIO_InitStruct;
此例程我们只要展示的是外部GPIO中断,所以我们额外使能 HAL_GPIO_MODULE_ENABLED。另外为了辅助展示按键信息,我们额外添加了串口相关的DMA、UART这两个宏定义。<syntaxhighlight lang="c" line="1" start="103">// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3)#define HAL_CORTEX_MODULE_ENABLED // NVIC__HAL_RCC_GPIOC_CLK_ENABLE();
#define HAL_GPIO_MODULE_ENABLED // GPIO配置按键引脚#define HAL_DMA_MODULE_ENABLED GPIO_InitStruct.Pin = KEY_LEFT_Pin|KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_Pin; // DMA选择PC0、PC1、PC2、PC3#define HAL_UART_MODULE_ENABLED GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // UART下降沿中断触发</syntaxhighlight> ==== main GPIO_InitStruct.c ===Pull =GPIO_PULLUP; // 上拉main函数,我们的例程由此处开始执行,首先调用HAL_Init HAL_GPIO_Init(GPIOC, &GPIO_InitStruct)函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 接下来我们初始化了串口部分,目的是打印按键按下的调试信息。; // 初始化引脚 接下来是初始化按键,并且注册了按键回调函数(回调函数负责的是不同层之间的数据传输)。 在最后的while()循环中,我们调用按键轮训函数,这样一旦有外部中断触发,我们首先会进行一下按键消抖,确认是否为误判。如果判断是正常触发,则认为是有按键按下,此时按键处理文件gyu_key.c中会将按键信息,通过上面说的回调函数,传到应用层(mian.c)中进行处理。 <syntaxhighlight lang="c++" line="1" start="36">int main(void){ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟配置中断优先级,并且使能中断 HAL_Init();{  // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等)配置PC0的中断,也就是EXTI line0 SystemClock_ConfigHAL_NVIC_SetPriority(KEY_LEFT_EXTI_IRQn, 10, 0); // 初始化USART1 MX_USART1_UART_Init HAL_NVIC_EnableIRQ(KEY_LEFT_EXTI_IRQn);
// 初始化按键引脚配置PC1的中断,也就是EXTI line1 MX_KEY_Init HAL_NVIC_SetPriority(KEY_DOWN_EXTI_IRQn, 10, 0); HAL_NVIC_EnableIRQ(KEY_DOWN_EXTI_IRQn);
//注册按钮回调函数配置PC2的中断,也就是EXTI line2 KEY_RegisterCbHAL_NVIC_SetPriority(AppKey_cbKEY_RIGHT_EXTI_IRQn, 10, 0); HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn);
// 配置PC3的中断,也就是EXTI line3 while HAL_NVIC_SetPriority(1KEY_UP_EXTI_IRQn, 10, 0); { KEY_PollHAL_NVIC_EnableIRQ(KEY_UP_EXTI_IRQn); // 按键轮训,监测是否有按键被按下
}
}
</syntaxhighlight>在应用层的按键回调函数中,我们可以看到,当我们分别按下S1、S2、S3、S4按键后,STM32L4会通过串口向外部打印按键信息。如下,是我们在初始化函数中打开的四个中断线。<syntaxhighlight lang="c" line="1" start="6943">// EXTI line0 中断函数void AppKey_cbEXTI0_IRQHandler(uint8_t keyvoid)
{
// 如果有相应按键被按下,则串口打印调试信息 ifHAL_GPIO_EXTI_IRQHandler(key & KEY_UP) { printf("key_up press\r\n"); } if(key & KEY_LEFT) { printf("key_left press\r\n"); } if(key & KEY_DOWN) { printf("key_down press\r\n"); } if(key & KEY_RIGHT) { printf("key_right press\r\n"KEY_LEFT_Pin); }
}
</syntaxhighlight>
==== gyu_util.c ====// EXTI line1 中断函数时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。void EXTI1_IRQHandler(void){主要包含了三个部分的初始化配置。 HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_Pin);}
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。// EXTI line2 中断函数void EXTI2_IRQHandler(void){ HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_Pin);}
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。// EXTI line3 中断函数void EXTI3_IRQHandler(void)3.外设时钟配置,选择外设时钟来源。{ HAL_GPIO_EXTI_IRQHandler(KEY_UP_Pin);为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。} 基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。</syntaxhighlight>我们配置好上面所说的引脚外部中断后,一旦有按键被按下,则会触发中断,最终会跑到如下的HAL_GPIO_EXTI_Callback()函数中。我们在这个函数中,判断一下是哪一个中断线触发的中断,并且记录一下按键信息,以及触发的时间(记录触发的时间,是为了进行按键消抖,防止误操作)。<syntaxhighlight lang="c++" line="1" start="49119">void SystemClock_ConfigHAL_GPIO_EXTI_Callback(voiduint16_t GPIO_Pin)
{
RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体如果是UP键被触发,记录按键任务为KEY_UP,并记录当前时钟 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 // 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIGif(RCC_LSEDRIVE_LOW);  // 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType GPIO_Pin = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)KEY_UP_Pin) // 配置LSE{ RCC_OscInitStruct key_check_press.LSEState key_event = RCC_LSE_ONKEY_UP; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL RCC_OscInitStruct.PLLkey_check_press.PLLState start_tick = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20HAL_GetTick(); // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz} RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz // RCC时钟配置,出错则进入错误处理函数如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) !GPIO_Pin == HAL_OKKEY_LEFT_Pin)
{
_Error_Handlerkey_check_press.key_event = KEY_LEFT; key_check_press.start_tick = HAL_GetTick(__FILE__, __LINE__);
}
// 初始化CPU,AHB和APB总线时钟如果是DOWN键被触发,记录按键任务为KEY_DOWN,并记录当前时钟 RCC_ClkInitStruct.ClockType if(GPIO_Pin = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHzKEY_DOWN_Pin) RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz{ RCC_ClkInitStruct key_check_press.APB1CLKDivider key_event = RCC_HCLK_DIV1KEY_DOWN; // APB1时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct key_check_press.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 start_tick = 80MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4) { _Error_HandlerHAL_GetTick(__FILE__, __LINE__);
}
  // 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) !GPIO_Pin == HAL_OKKEY_RIGHT_Pin)
{
_Error_Handlerkey_check_press.key_event = KEY_RIGHT; key_check_press.start_tick = HAL_GetTick(__FILE__, __LINE__);
}
}</syntaxhighlight>按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。<syntaxhighlight lang="c++" line="1" start="171">void KEY_Poll(void){ uint8_t key_event = 0; // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz如果有按键任务 if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OKkey_check_press.key_event)
{
_Error_Handler(__FILE__, __LINE__); }  // 配置系统定时器中断时间,配置为HCLK的千分频获取当前时钟 减去 记录的按键触发时钟,如果大于消抖延时,则继续向下判断 HAL_SYSTICK_Config if(HAL_RCC_GetHCLKFreqHAL_GetTick()/1000- key_check_press.start_tick >= KEY_DELAY_TICK ); { // 配置系统定时器,配置为HCLK如果按键任务记录为KEY_UP HAL_SYSTICK_CLKSourceConfig if(SYSTICK_CLKSOURCE_HCLKkey_check_press.key_event & KEY_UP); { // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)获取当前KEY_UP引脚电平,如果是低电平,则认为UP按键被按下 HAL_NVIC_SetPriority if(HAL_GPIO_ReadPin(SysTick_IRQnKEY_UP_GPIO_Port, 0, 0KEY_UP_Pin) == GPIO_PIN_RESET) { key_event |= KEY_UP; // 记录app按键任务 }< key_check_press.key_event ^= KEY_UP; /syntaxhighlight>/ 删除按键中断任务 } // 如果按键任务记录为KEY_LEFT==== gyu_key if(key_check_press.c ====key_event & KEY_LEFT) { // 获取当前KEY_LEFT引脚电平,如果是低电平,则认为LEFT按键被按下首先我们看一下按键的初始化函数,在按键初始化函数中我们配置按键引脚的状态,四个按键引脚都被配置为默认上拉,下降沿中断触发。并且开启EXTI0、EXTI1、EXTI2、EXTI3这四个外部中断线。<syntaxhighlight lang="c++" line if(HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port,KEY_LEFT_Pin) ="1" start="75">void MX_KEY_Init(voidGPIO_PIN_RESET) { key_event |= KEY_LEFT; // 定义GPIO结构体记录app按键任务 } GPIO_InitTypeDef GPIO_InitStruct key_check_press.key_event ^= KEY_LEFT; // 删除按键中断任务 } // 使能GPIOC引脚时钟(按键引脚:PC0、PC1、PC2、PC3)如果按键任务记录为KEY_DOWN __HAL_RCC_GPIOC_CLK_ENABLE if(key_check_press.key_event & KEY_DOWN); { // 配置按键引脚获取当前KEY_DOWN引脚电平,如果是低电平,则认为DOWN按键被按下 GPIO_InitStruct.Pin if(HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port,KEY_DOWN_Pin) == KEY_LEFT_PinGPIO_PIN_RESET) { key_event |KEY_DOWN_Pin|KEY_RIGHT_Pin|KEY_UP_Pin= KEY_DOWN; // 选择PC0、PC1、PC2、PC3记录app按键任务 } GPIO_InitStruct key_check_press.Mode key_event ^= GPIO_MODE_IT_FALLINGKEY_DOWN; // 下降沿中断触发删除按键中断任务 } GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉如果按键任务记录为KEY_RIGHT HAL_GPIO_Init if(GPIOC, key_check_press.key_event &GPIO_InitStructKEY_RIGHT); // 初始化引脚 // 配置中断优先级,并且使能中断 { // 配置PC0的中断,也就是EXTI line0获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下 HAL_NVIC_SetPriority if(HAL_GPIO_ReadPin(KEY_LEFT_EXTI_IRQnKEY_RIGHT_GPIO_Port, 10, 0KEY_RIGHT_Pin); HAL_NVIC_EnableIRQ(KEY_LEFT_EXTI_IRQn== GPIO_PIN_RESET); { key_event |= KEY_RIGHT; // 配置PC1的中断,也就是EXTI line1 记录app按键任务 HAL_NVIC_SetPriority(KEY_DOWN_EXTI_IRQn, 10, 0); } HAL_NVIC_EnableIRQ(KEY_DOWN_EXTI_IRQn) key_check_press.key_event ^= KEY_RIGHT; // 配置PC2的中断,也就是EXTI line2 删除按键中断任务 HAL_NVIC_SetPriority(KEY_RIGHT_EXTI_IRQn, 10, 0); } HAL_NVIC_EnableIRQ(KEY_RIGHT_EXTI_IRQn); } } // 配置PC3的中断,也就是EXTI line3如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数 HAL_NVIC_SetPriorityif(KEY_UP_EXTI_IRQn, 10, 0key_event && pFkey_cb); HAL_NVIC_EnableIRQ{ pFkey_cb(KEY_UP_EXTI_IRQnkey_event);
}
}
</syntaxhighlight>如下,是我们在初始化函数中打开的四个中断线。留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。<syntaxhighlight lang="c++" line="1" start="43155">// EXTI line0 中断函数void EXTI0_IRQHandlerKEY_RegisterCb(voidkey_cb cb)
{
HAL_GPIO_EXTI_IRQHandlerif(KEY_LEFT_Pincb != 0) { pFkey_cb = cb; }
}
</syntaxhighlight>
 
== 实验05-光敏二极管 ==
光敏二极管在不同的光照强度下,它的out引脚输出的电压不同。所以此实验,我们利用STM32L4的ADC功能,去采集光敏二极管的引脚输出电压,以此能够获取到当前环境的光照情况。
 
=== STM32L476 ADC简介 ===
STM32L476一共有3个ADC(ADC1、ADC2、ADC3),3个ADC都可以进行独立的工作,每个ADC都支持12位精度的采样。
// EXTI line1 中断函数void EXTI1_IRQHandler(void){ HAL_GPIO_EXTI_IRQHandler(KEY_DOWN_Pin);}每个ADC最多有19个多路复用的通道,每个通道的AD转换都是可以执行单一、连续扫描或者不连续扫描的。
// EXTI line2 中断函数void EXTI2_IRQHandler(void){ HAL_GPIO_EXTI_IRQHandler(KEY_RIGHT_Pin);}ADC的参考电压取决于VREF+引脚的输入电压,在开发板上,我们接好miniUSB供电,此引脚电压范围在3.2V~3.3V之间,代码中我们默认取3.3V为参考电压。
// EXTI line3 中断函数ADC引脚的输入电压范围:VREF- ≤ VIN ≤ VREF+ ,在开发板上,就是大于0V,小于3.3V。 在这个例程中,我们将以ADC1的通道16(也就是PB1引脚)来采集光敏二极管的输出电压。 === 硬件设计 ===选择STM32L4引脚PB1作为光敏二极管的ADC采集引脚。[[文件:NBDK-SCH-ADCLIGHT.png|边框|居中|无框|357x357像素]]=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到DBG端,SW2拨到MCU。# 使用Keil打开基础实验 05-光敏二极管实验工程。# 使用Xshell打开Jlink虚拟出的COM口# 下载程序,并完成功能测试。 === 实验验证 ===下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的ADC数据。红色字体部分,是正常的室内光照强度时采集的电压;绿色字体部分,是打开手机手电筒照射光敏二极管时采集的电压。可以看到明显的电压差值。[[文件:NBDK-XSHELL-ADCLIGHT.png|边框|居中|无框|759x759像素]]=== 源码详解 ===void EXTI3_IRQHandler(void)本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。{ HAL_GPIO_EXTI_IRQHandler(KEY_UP_Pin);==== stm32l4xx_hal_conf.h ====此文件位于“05-光敏二极管实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。}</syntaxhighlight>我们配置好上面所说的引脚外部中断后,一旦有按键被按下,则会触发中断,最终会跑到如下的HAL_GPIO_EXTI_Callback()函数中。我们在这个函数中,判断一下是哪一个中断线触发的中断,并且记录一下按键信息,以及触发的时间(记录触发的时间,是为了进行按键消抖,防止误操作)。此例程我们主要给大家展示STM32L4的ADC功能,所以我们宏定义中打开ADC相关的。<syntaxhighlight lang="c++" line="1" start="119103">void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash{#define HAL_PWR_MODULE_ENABLED // 电源 #define HAL_RCC_MODULE_ENABLED // 如果是UP键被触发,记录按键任务为KEY_UP,并记录当前时钟时钟#define HAL_CORTEX_MODULE_ENABLED if(GPIO_Pin == KEY_UP_Pin)// NVIC {#define HAL_GPIO_MODULE_ENABLED key_check_press.key_event = KEY_UP;// GPIO#define HAL_UART_MODULE_ENABLED key_check_press.start_tick = HAL_GetTick();// UART }#define HAL_DMA_MODULE_ENABLED // DMA #define HAL_ADC_MODULE_ENABLED //ADC</ 如果是LEFT键被触发,记录按键任务为KEY_LEFT,并记录当前时钟syntaxhighlight> if(GPIO_Pin === KEY_LEFT_Pin) { key_check_press.key_event = KEY_LEFT; key_check_pressmain.start_tick c = HAL_GetTick(); } // 如果是DOWN键被触发,记录按键任务为KEY_DOWN,并记录当前时钟 if(GPIO_Pin == KEY_DOWN_Pin) { key_check_press.key_event = KEY_DOWN; key_check_press.start_tick = HAL_GetTickmain函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config();函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 } // 如果是RIGHT键被触发,记录按键任务为KEY_RIGHT,并记录当前时钟接下来我们初始化了串口部分,目的是打印ADC采集电压值。 if(GPIO_Pin == KEY_RIGHT_Pin) {接下来是ADC引脚初始化。 key_check_press.key_event = KEY_RIGHT; key_check_press.start_tick = HAL_GetTick在最后的while(); }循环中,我们调用ADC值采集函数,每个500ms采集一次,并且将采集值转换成电压值,格式化打印到串口显示。}</syntaxhighlight>按键轮训函数,其实就是用来处理消抖的函数,我们根据从外部中断回调函数中的获取的时间(也就是中断触发的时间,上一个函数中记录的),对比现在实时的时间,判断是否超过20ms,如果超过20ms,则认为按键被按下。我们记录下按键信息,并且执行向应用层回调的函数。<syntaxhighlight lang="c++" line="1" start="17133">void KEY_Pollint main(void)
{
uint8_t key_event uint16_t ad_value = 0;  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟 HAL_Init();  // 如果有按键任务配置系统时钟(包含振荡器、系统时钟、总线时钟等等) ifSystemClock_Config(key_check_press.key_event); { // 获取当前时钟 减去 记录的按键触发时钟,如果大于消抖延时,则继续向下判断初始化串口USART1 if(HAL_GetTick MX_USART1_UART_Init() - key_check_press.start_tick >= KEY_DELAY_TICK ); { // 如果按键任务记录为KEY_UP初始化ADC1 if MX_ADC1_Init(key_check_press.key_event & KEY_UP); { // 获取当前KEY_UP引脚电平,如果是低电平,则认为UP按键被按下 if while (HAL_GPIO_ReadPin(KEY_UP_GPIO_Port,KEY_UP_Pin) == GPIO_PIN_RESET1) { key_event | HAL_Delay(500); ad_value = KEY_UPHAL_ADC_Read(); // 记录app按键任务获取ADC采集值 } key_check_press printf("Adc_value = %.2f\r\n",ad_value*3.key_event ^= KEY_UP3/4095); // 删除按键中断任务转换ADC采集为真实电压,并且打印到串口显示 } } <// 如果按键任务记录为KEY_LEFTsyntaxhighlight> if(key_check_press==== gyu_util.key_event & KEY_LEFT)c ====请参照实验01中的介绍。 { // 获取当前KEY_LEFT引脚电平,如果是低电平,则认为LEFT按键被按下基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。 if(HAL_GPIO_ReadPin(KEY_LEFT_GPIO_Port,KEY_LEFT_Pin) == GPIO_PIN_RESET) { key_event |= KEY_LEFT; // 记录app按键任务 } key_check_press= gyu_adc.key_event ^c ==== KEY_LEFT; // 删除按键中断任务 }ADC初始化函数,配置12位采样分辨率,配置2.5倍ADC时钟周期的采样频率。 // 如果按键任务记录为KEY_DOWN选择ADC通道16(CH16)作为此次的ADC引脚(也就是PB1)。<syntaxhighlight lang="c++" line="1" start="40"> ifvoid MX_ADC1_Init(key_check_press.key_event & KEY_DOWNvoid) { // 获取当前KEY_DOWN引脚电平,如果是低电平,则认为DOWN按键被按下定义常规ADC通道结构体 ADC_ChannelConfTypeDef sConfig; if(HAL_GPIO_ReadPin(KEY_DOWN_GPIO_Port,KEY_DOWN_Pin) == GPIO_PIN_RESET) { // 初始化ADC key_event | hadc1.Instance = KEY_DOWNADC1; // 记录app按键任务ADC寄存器地址,定义为ADC1 } key_check_press hadc1.Init.key_event ^ClockPrescaler = KEY_DOWNADC_CLOCK_ASYNC_DIV1; // 删除按键中断任务没有预分频器的ADC异步时钟 } hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 如果按键任务记录为KEY_RIGHT12位采样分辨率 if(key_check_press.key_event & KEY_RIGHT) { // 获取当前KEY_RIGHT引脚电平,如果是低电平,则认为RIGHT按键被按下初始化ADC,如果失败,则进入错误处理程序 if(HAL_GPIO_ReadPinHAL_ADC_Init(KEY_RIGHT_GPIO_Port,KEY_RIGHT_Pin&hadc1) !== GPIO_PIN_RESETHAL_OK) { key_event |= KEY_RIGHT; // 记录app按键任务 } key_check_press.key_event ^= KEY_RIGHT _Error_Handler(__FILE__, __LINE__); // 删除按键中断任务 } }
}
  // 配置ADC通道 sConfig.Channel = ADC_CHANNEL_16; //如果有记录给app的按键任务,代表真的有按钮按下,则执行回调函数配置为ADC通道16 sConfig.Rank = ADC_REGULAR_RANK_1; // 指定ADC规格组中的排名 sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5; // 采样时间配置为2.5个ADC时钟周期 sConfig.SingleDiff = ADC_SINGLE_ENDED; // ADC通道结束设置为单端 sConfig.OffsetNumber = ADC_OFFSET_NONE; // 禁用ADC偏移 sConfig.Offset = 0; // 定义从原始量中减去的偏移量 if(key_event HAL_ADC_ConfigChannel(&hadc1, & pFkey_cbsConfig)!= HAL_OK) // 配置ADC通道
{
pFkey_cb_Error_Handler(key_event__FILE__, __LINE__); // 如果出错,进入错误处理
}
 
}
</syntaxhighlight>留给应用层调用注册按键回调的函数,用于将轮询后确认的按键信息,传递给应用层使用。重新定义ADC的硬件引脚,可以看到,配置PB1为ADC引脚。<syntaxhighlight lang="c++" line="1" start="15577">void KEY_RegisterCbHAL_ADC_MspInit(key_cb cbADC_HandleTypeDef* adcHandle)
{
GPIO_InitTypeDef GPIO_InitStruct; if(cb !adcHandle->Instance= 0=ADC1)
{
pFkey_cb // 使能GPIOB引脚时钟(选择的ADC引脚为PB1) __HAL_RCC_GPIOB_CLK_ENABLE();  // 使能ADC时钟 __HAL_RCC_ADC_CLK_ENABLE(); // 初始化ADC引脚配置 GPIO_InitStruct.Pin = GPIO_PIN_1; // 选择引脚编号1 GPIO_InitStruct.Mode = cbGPIO_MODE_ANALOG_ADC_CONTROL; // 配置为ADC引脚 GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 初始化PB1引脚
}
}
</syntaxhighlight>用于获取ADC采集值的函数,经过一系列的API函数调用,我们获取到最终的adcValue。<syntaxhighlight lang="c++" line="1" start="104">uint16_t HAL_ADC_Read(void){ uint16_t ad_value = 0; // 启动ADC转换 HAL_ADC_Start(&hadc1); // 等待ADC转换完成 HAL_ADC_PollForConversion(&hadc1, 50); // 检查是否已经完成转换 if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC)) { // 获取采集到的ADC值 ad_value = HAL_ADC_GetValue(&hadc1); } return ad_value; // 返回ADC采集值}</syntaxhighlight>== 实验05实验06-光敏二极管 DAC模拟输出 ==光敏二极管在不同的光照强度下,它的out引脚输出的电压不同。所以此实验,我们利用STM32L4的ADC功能,去采集光敏二极管的引脚输出电压,以此能够获取到当前环境的光照情况。此实验我们将会配置DAC1的通道1,作为模拟电压的输出引脚,并且配置一个ADC引脚,采集DAC输出的电压,并将电压值格式化打印到串口显示。 === STM32L476 ADC简介 DAC简介 ===STM32L476一共有3个ADC(ADC1、ADC2、ADC3),3个ADC都可以进行独立的工作,每个ADC都支持12位精度的采样。DAC模块是一个12位电压输出数模转换器。 DAC可配置为8位或12位模式,并可与DMA控制器配合使用。在12位模式下,数据可以左对齐或右对齐。 DAC有两个输出通道,每个通道都有自己的转换器。在双DAC通道模式下,当两个通道组合在一起进行同步更新操作时,可以单独或同时完成转换。 DAC的主要特性如下:
每个ADC最多有19个多路复用的通道,每个通道的AD转换都是可以执行单一、连续扫描或者不连续扫描的。•两个DAC转换器:每个转换器一个输出通道
ADC的参考电压取决于VREF+引脚的输入电压,在开发板上,我们接好miniUSB供电,此引脚电压范围在3.2V~3.3V之间,代码中我们默认取3.3V为参考电压。•12位模式下的左或右数据对齐
ADC引脚的输入电压范围:VREF- ≤ VIN ≤ VREF+ ,在开发板上,就是大于0V,小于3.3V。•同步更新功能
在这个例程中,我们将以ADC1的通道16(也就是PB1引脚)来采集光敏二极管的输出电压。•噪声波和三角波生成
•双DAC通道,用于独立或同步转换 •每个通道的DMA功能,包括DMA欠载错误检测 •转换的外部触发器 •DAC输出通道缓冲/非缓冲模式 •缓冲偏移校准 •每个DAC输出可以与DAC_OUTx输出引脚断开 •DAC输出连接到片上外设 •采样和保持模式,用于在停止模式下进行低功耗操作 •输入电压参考,VREF + === 硬件设计 ===选择STM32L4引脚PB1作为光敏二极管的ADC采集引脚。选择STM32L4引脚PA4作为模拟输出(DAC)引脚,配置PA7作为ADC引脚。 此实验测试的时候,由于选择的PA4及PA7引脚被显示屏占用,所以我们需要拔下显示屏,然后使用杜邦线短接PA4及PA7引脚。[[文件:NBDK-SCH-ADCLIGHTDAC.png|边框|居中|无框|357x357像素333x333像素]]
=== 实验准备 ===
# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
# 使用miniUSB线,连接PC与开发板USB接口。
# 将SW1拨到DBG端,SW2拨到MCU。
# 使用Keil打开基础实验 0506-光敏二极管实验工程。DAC实验工程。
# 使用Xshell打开Jlink虚拟出的COM口
# 下载程序,并完成功能测试。
=== 实验验证 ===
下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的ADC数据。
红色字体部分,是正常的室内光照强度时采集的电压;绿色字体部分,是打开手机手电筒照射光敏二极管时采集的电压。可以看到明显的电压差值。我们默认配置DAC引脚输出2000(也就是1.6V)电压,可以看到ADC采集到的数据为1.56V,这个是ADC采集的偏移量导致的。[[文件:NBDK-XSHELL-ADCLIGHTDAC.png|边框|居中|无框|759x759像素]]
=== 源码详解 ===
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
==== stm32l4xx_hal_conf.h ====
此文件位于“05此文件位于“06-光敏二极管实验DAC实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。
此例程我们主要给大家展示STM32L4的ADC功能,所以我们宏定义中打开ADC相关的。此例程我们主要给大家展示STM32L4的DAC功能,所以我们宏定义中打开DAC相关的。<syntaxhighlight lang="c" line="1" start="103">
// 使能的宏
#define HAL_MODULE_ENABLED // 芯片
#define HAL_DMA_MODULE_ENABLED // DMA
#define HAL_ADC_MODULE_ENABLED // ADC
#define HAL_DAC_MODULE_ENABLED // DAC
</syntaxhighlight>
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来我们初始化了串口部分,目的是打印ADC采集电压值。接下来我们初始化了串口部分,目的是打印采集到DAC输出电压。
接下来是ADC引脚初始化。接下来分别初始化了ADC以及DAC引脚配置。
在最后的while()循环中,我们调用ADC值采集函数,每个500ms采集一次,并且将采集值转换成电压值,格式化打印到串口显示。循环中,我们设置DAC引脚的输出电压(默认设置输出1.6V),然后我们调用ADC值采集函数采集这个引脚电压,并且每隔500ms将采集值转换成电压值,格式化打印到串口显示。
<syntaxhighlight lang="c++" line="1" start="33">
// 初始化ADC1
MX_ADC1_Init();
 
// 初始化DAC1
MX_DAC1_Init();
//
{
HAL_Delay(500);
HAL_DAC_Set(2000); // 设置2000DAC输出值(1.6V) ad_value = HAL_ADC_Read(); // 获取ADC采集值 printf("adc = %d\r\n",ad_value); // 打印ADC采集值 printf("Adc_value = %.2f2fV\r\n",ad_value*3.3/4095); // 转换ADC采集为真实电压,并且打印到串口显示
}
}
</syntaxhighlight>
==== gyu_util.c ====
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。请参照实验01中的介绍。
主要包含了三个部分的初始化配置。基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。 1==== gyu_dac.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。c ==== 2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。 3.外设时钟配置,选择外设时钟来源。 为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。 基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。DAC初始化函数,配置DAC1通道1(也就是PA4引脚)为DAC输出引脚。<syntaxhighlight lang="c++" line="1" start="4937">void SystemClock_ConfigMX_DAC1_Init(void)
{
RCC_OscInitTypeDef RCC_OscInitStructhdac1.Instance = DAC1; // 配置为DAC1 if (HAL_DAC_Init(&hdac1) != HAL_OK) // 初始化DAC1 { _Error_Handler(__FILE__, __LINE__); } // 定义RCC内部DAC通道配置结构体定义 DAC_ChannelConfTypeDef sConfig; sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; /外部振荡器结构体/DAC模式 RCC_ClkInitTypeDef RCC_ClkInitStructsConfig.DAC_Trigger=DAC_TRIGGER_NONE; // 定义RCC系统,AHB和APB总线时钟配置结构体不使用触发功能 RCC_PeriphCLKInitTypeDef PeriphClkInitsConfig.DAC_OutputBuffer=DAC_OUTPUTBUFFER_DISABLE; // DAC1输出缓冲关闭 sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLE; // 定义RCC扩展时钟结构体不连接到片内外设
// 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);  // 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz // RCC时钟配置,出错则进入错误处理函数初始化通道CH1配置 if (HAL_RCC_OscConfigHAL_DAC_ConfigChannel(&RCC_OscInitStructhdac1, &sConfig, DAC_CHANNEL_1) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__);
}
HAL_DAC_Start(&hdac1,DAC_CHANNEL_1); // 初始化CPU,AHB和APB总线时钟开启DAC通道1 RCC_ClkInitStruct.ClockType }</syntaxhighlight>重新定义DAC的硬件引脚,可以看到,配置PA4为DAC引脚。<syntaxhighlight lang="c++" line="1" start= RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK"70"> |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2void HAL_DAC_MspInit(DAC_HandleTypeDef* hdac){ GPIO_InitTypeDef GPIO_InitStruct; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource if(hdac->Instance== RCC_SYSCLKSOURCE_PLLCLK; DAC1) // 配置系统时钟为PLLCLK输入,80MHz判断是否是DAC1 RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1{ __HAL_RCC_DAC1_CLK_ENABLE(); // AHB时钟为系统时钟1分频,80/1 = 80MHz使能DAC时钟 RCC_ClkInitStruct GPIO_InitStruct.APB1CLKDivider Pin = RCC_HCLK_DIV1GPIO_PIN_4; // APB1时钟为系统时钟1分频,80/1 = 80MHz选择PA4 RCC_ClkInitStruct GPIO_InitStruct.APB2CLKDivider Mode = RCC_HCLK_DIV1GPIO_MODE_ANALOG; // APB2时钟为系统时钟1分频,80/1 = 80MHz配置模拟模式 // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) ! GPIO_InitStruct.Pull = HAL_OK) GPIO_NOPULL; // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4) {无上下拉 _Error_HandlerHAL_GPIO_Init(__FILE__GPIOA, __LINE__&GPIO_InitStruct);// 初始化PA4引脚配置
}
}
</syntaxhighlight>设置DAC引脚输出电压,value为单元值,不是真正的电压值大小。
// 初始化外设时钟 PeriphClkInit模拟电压值(范围:0~4095,对应0~3.PeriphClockSelection 3V)。<syntaxhighlight lang= RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection "c++" line= RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz PeriphClkInit.Usart2ClockSelection "1" start= RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz"92"> PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHzvoid HAL_DAC_Set(uint16_t value) PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz{ PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSEHAL_DAC_SetValue(&hdac1,DAC_CHANNEL_1,DAC_ALIGN_12B_R,value); // 配置LPTIM1时钟为LSE,32.768KHz配置CH1 12位右对齐模拟输出 PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义} PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; /</ 配置PLLSAI1时钟为HSE,8MHzsyntaxhighlight> PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 实验07-温湿度采集 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }此实验给大家展示的是利用STM32L476的I2C外设功能,去获取sht20温湿度传感器采集的温湿度数据。并且将获取到的数据转换成真实的温湿度数据,格式化打印到串口显示。
// 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__);== STM32L476 I2C简介 === }I2C(内部集成电路)总线接口处理STM32L4和串行I2C总线之间的通信。 它提供多主机功能,并控制所有I2C总线特定的排序,协议,仲裁和定时。 它支持标准模式(Sm),快速模式(Fm)和快速模式加(Fm +)。兼容SMBus(系统管理总线)和PMBus(电源管理总线)。
// 配置系统定时器中断时间,配置为HCLK的千分频 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);I2C主要功能:
// 配置系统定时器,配置为HCLK HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);•I2C总线规范rev03兼容性:
// 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight>- 从模式和主模式
==== gyu_adc.c ====ADC初始化函数,配置12位采样分辨率,配置2.5倍ADC时钟周期的采样频率。- 多主机功能
选择ADC通道16(CH16)作为此次的ADC引脚(也就是PB1)。<syntaxhighlight lang="c++" line="1" start="40">void MX_ADC1_Init(void){ // 定义常规ADC通道结构体 ADC_ChannelConfTypeDef sConfig;- 标准模式(最高100 kHz)
// 初始化ADC hadc1.Instance = ADC1; // ADC寄存器地址,定义为ADC1 hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1; // 没有预分频器的ADC异步时钟 hadc1.Init.Resolution = ADC_RESOLUTION_12B; // 12位采样分辨率 // 初始化ADC,如果失败,则进入错误处理程序 if (HAL_ADC_Init(&hadc1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }- 快速模式(最高400 kHz)
// 配置ADC通道 sConfig.Channel = ADC_CHANNEL_16; // 配置为ADC通道16 sConfig.Rank = ADC_REGULAR_RANK_1; // 指定ADC规格组中的排名 sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5; // 采样时间配置为2.5个ADC时钟周期 sConfig.SingleDiff = ADC_SINGLE_ENDED; // ADC通道结束设置为单端 sConfig.OffsetNumber = ADC_OFFSET_NONE; // 禁用ADC偏移 sConfig.Offset = 0; // 定义从原始量中减去的偏移量 if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) // 配置ADC通道 { _Error_Handler(__FILE__, __LINE__); // 如果出错,进入错误处理 }- 快速模式加(最高1 MHz)
}- 7位和10位寻址模式 - 多个7位从机地址(2个地址,1个带可配置掩码) </syntaxhighlight>重新定义ADC的硬件引脚,可以看到,配置PB1为ADC引脚。<syntaxhighlight lang="c++" line="1" start="77">- 所有7位地址确认模式void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle){- 一般电话 GPIO_InitTypeDef GPIO_InitStruct; if(adcHandle->Instance==ADC1)可编程设置和保持时间 { // 使能GPIOB引脚时钟(选择的ADC引脚为PB1)- 易于使用的事件管理 __HAL_RCC_GPIOB_CLK_ENABLE();- 可选的时钟拉伸
// 使能ADC时钟 __HAL_RCC_ADC_CLK_ENABLE(); // 初始化ADC引脚配置 GPIO_InitStruct.Pin = GPIO_PIN_1; // 选择引脚编号1 GPIO_InitStruct.Mode = GPIO_MODE_ANALOG_ADC_CONTROL; // 配置为ADC引脚 GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); // 初始化PB1引脚 }}</syntaxhighlight>用于获取ADC采集值的函数,经过一系列的API函数调用,我们获取到最终的adcValue。<syntaxhighlight lang="c++" line="1" start="104">uint16_t HAL_ADC_Read(void){ uint16_t ad_value = 0; // 启动ADC转换 HAL_ADC_Start(&hadc1); // 等待ADC转换完成 HAL_ADC_PollForConversion(&hadc1, 50); // 检查是否已经完成转换 if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC)) { // 获取采集到的ADC值 ad_value = HAL_ADC_GetValue(&hadc1); } return ad_value; // 返回ADC采集值}</syntaxhighlight>== 实验06-DAC模拟输出 ==此实验我们将会配置DAC1的通道1,作为模拟电压的输出引脚,并且配置一个ADC引脚,采集DAC输出的电压,并将电压值格式化打印到串口显示。软件重置
=== STM32L476 DAC简介 ===DAC模块是一个12位电压输出数模转换器。 DAC可配置为8位或12位模式,并可与DMA控制器配合使用。在12位模式下,数据可以左对齐或右对齐。 DAC有两个输出通道,每个通道都有自己的转换器。在双DAC通道模式下,当两个通道组合在一起进行同步更新操作时,可以单独或同时完成转换。•具有DMA功能的1字节缓冲区
DAC的主要特性如下:•可编程模拟和数字噪声滤波器
•两个DAC转换器:每个转换器一个输出通道=== 硬件设计 ===选择STM32L4引脚PB13作为I2C SCL引脚,PB14作为I2C SDA引脚。[[文件:NBDK-SCH-SHT20.png|边框|居中|无框|353x353像素]]
•12位模式下的左或右数据对齐 •同步更新功能 •噪声波和三角波生成 •双DAC通道,用于独立或同步转换 •每个通道的DMA功能,包括DMA欠载错误检测 •转换的外部触发器 •DAC输出通道缓冲/非缓冲模式 •缓冲偏移校准 •每个DAC输出可以与DAC_OUTx输出引脚断开 •DAC输出连接到片上外设 •采样和保持模式,用于在停止模式下进行低功耗操作 •输入电压参考,VREF + === 硬件设计 ===选择STM32L4引脚PA4作为模拟输出(DAC)引脚,配置PA7作为ADC引脚。 此实验测试的时候,由于选择的PA4及PA7引脚被显示屏占用,所以我们需要拔下显示屏,然后使用杜邦线短接PA4及PA7引脚。[[文件:NBDK-SCH-DAC.png|边框|居中|无框|333x333像素]]=== 实验准备 ===
# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。
# 使用miniUSB线,连接PC与开发板USB接口。
# 将SW1拨到DBG端,SW2拨到MCU。
# 使用Keil打开基础实验 0607-DAC实验工程。温湿度实验工程。
# 使用Xshell打开Jlink虚拟出的COM口
# 下载程序,并完成功能测试。
=== 实验验证 ===
下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的ADC数据。下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的温湿度数据。
我们默认配置DAC引脚输出2000(也就是1在采集的过程中,我们将手指按在SHT20上,可以看到温度和湿度都在上升,例如温度,由开始的20.6V)电压,可以看到ADC采集到的数据为14°C上升到最终的26.56V,这个是ADC采集的偏移量导致的。7°C。[[文件:NBDK-XSHELL-DACSHT20.png|边框|居中|无框|759x759像素]]
=== 源码详解 ===
本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
==== stm32l4xx_hal_conf.h ====
此文件位于“06此文件位于“07-DAC实验温湿度实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。
此例程我们主要给大家展示STM32L4的DAC功能,所以我们宏定义中打开DAC相关的。此例程我们主要给大家展示STM32L4的I2C功能,所以我们宏定义中打开I2C相关的。<syntaxhighlight lang="c" line="1" start="103">
// 使能的宏
#define HAL_MODULE_ENABLED // 芯片
#define HAL_ADC_MODULE_ENABLED // ADC
#define HAL_DAC_MODULE_ENABLED // DAC
#define HAL_I2C_MODULE_ENABLED // I2C
</syntaxhighlight>
main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
接下来我们初始化了串口部分,目的是打印采集到DAC输出电压。接下来我们初始化了串口部分,目的是打印采集到的温湿度数据。
接下来分别初始化了ADC以及DAC引脚配置。接下来初始化I2C引脚。
在最后的while在while()循环中,我们设置DAC引脚的输出电压(默认设置输出1.6V),然后我们调用ADC值采集函数采集这个引脚电压,并且每隔500ms将采集值转换成电压值,格式化打印到串口显示。循环中,我们每隔500ms采集一次温湿度的值,并且将采集的温湿度值转化成真实值,格式化打印到串口显示。
<syntaxhighlight lang="c++" line="1" start="33">
int main(void)
{
uint16_t ad_value = 0;
 
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
// 重置所有外设、flash界面以及系统时钟
// 初始化串口USART1
MX_USART1_UART_Init();
  // 初始化ADC1 MX_ADC1_Init();  // 初始化DAC1初始化I2C2 MX_DAC1_InitMX_I2C2_Init();
//
{
HAL_Delay(500);
HAL_DAC_Set(2000); // 设置2000DAC输出值(1.6V) ad_value = HAL_ADC_Read(); // 获取ADC采集值 printf("adc Temp = %d.1f\r\n",ad_valueSHT20_Convert(SHT20_ReadTemp(),1)); // 打印ADC采集值 printf("Adc_value RH = %.2fV1f%%\r\n",ad_value*3.3/4095SHT20_Convert(SHT20_ReadRH(),0)); // 转换ADC采集为真实电压,并且打印到串口显示
}
}
</syntaxhighlight>
==== gyu_util.c ====
时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。请参照实验01中的介绍。
主要包含了三个部分的初始化配置。基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。==== gyu_i2c.c ====在讲解i2c代码之前,我们先给大家讲解一下参数Timing,这个值是通过计算得来的,在STM32芯片手册的P1238页有计算公式说明,我们这边偷懒,利用STM32CUBE里面的配置功能,对应SHT20的I2C参数要求。
1我们配置I2C时钟为100KHz,Rise Time 300ns,Fall Time 100ns。最终得出Timing值为0x10D05E82。[[文件:NBDK-DS-SHT20.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。png|边框|居中|无框|557x557像素]][[文件:NBDK-CUBE-SHT20.png|边框|居中|无框|643x643像素]]
2.时钟配置,选择系统、AHB总线及APB总线的时钟来源。 3.外设时钟配置,选择外设时钟来源。 为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。 基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。初始化I2C引脚,选择的I2C2。<syntaxhighlight lang="c++" line="1" start="4936">void SystemClock_ConfigMX_I2C2_Init(void)
{
RCC_OscInitTypeDef RCC_OscInitStructhi2c2.Instance = I2C2; // 定义RCC内部/外部振荡器结构体I2C寄存器基础地址,定义为I2C2的 RCC_ClkInitTypeDef RCC_ClkInitStructhi2c2.Init.Timing = 0x10D05E82; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体指定I2C_TIMINGR寄存器值,此值必须在I2C初始化之前配置
// 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIGif (HAL_I2C_Init(RCC_LSEDRIVE_LOW&hi2c2) != HAL_OK);  // 初始化CPU,AHB和APB总线时钟初始化I2C2 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE{ |RCC_OSCILLATORTYPE_LSE _Error_Handler(__FILE__, __LINE__); // 设置需要配置的振荡器为HSI、HSE、LSE如果初始化失败,则进入错误处理 // 配置HSE} RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz)} /</ 配置LSE RCC_OscInitStruct.LSEState syntaxhighlight>定义I2C2功能引脚,选择PB13为SCL引脚,PB14为SDA引脚。并且使能GPIOB以及I2C2的时钟。<syntaxhighlight lang= RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState "c++" line= RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue "1" start= 16; // 配置HSI为16MHz "55"> // 配置PLLvoid HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle) RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL{ RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz使能GPIOB引脚时钟,因为选择的I2C引脚均在PB上 RCC_OscInitStruct.PLL.PLLM = 1__HAL_RCC_GPIOB_CLK_ENABLE(); // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz定义GPIO结构 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2GPIO_InitTypeDef GPIO_InitStruct; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz // RCC时钟配置,出错则进入错误处理函数判断I2C是否选择的是I2C2 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) !i2cHandle->Instance== HAL_OKI2C2)
{
_Error_Handler// I2C2引脚配置 GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_14; // 选择PB13为SCL引脚,PB14为SDA引脚 GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 外设功能为开漏模式 GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 高速模式 GPIO_InitStruct.Alternate = GPIO_AF4_I2C2; // 外设引脚选择I2C2 HAL_GPIO_Init(__FILE__GPIOB, __LINE__&GPIO_InitStruct); // 初始化IO配置  // 使能I2C2时钟 __HAL_RCC_I2C2_CLK_ENABLE(); }}</syntaxhighlight>I2C发送数据的函数。<syntaxhighlight lang="c++" line="1" start="89">uint8_t HAL_I2C_Send(uint8_t addr , uint8_t *pData, uint16_t len){ // 判断是否存在数据,不存在返回HAL_ERROR if(len == 0 || pData == 0) { return HAL_ERROR;
}
// 初始化CPU,AHB和APB总线时钟发送数据,并返回发送状态 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2return HAL_I2C_Master_Transmit(&hi2c2,addr,pData,len,100); // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz} RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; <// AHB时钟为系统时钟1分频,80/1 syntaxhighlight>I2C接收数据的函数。<syntaxhighlight lang= 80MHz RCC_ClkInitStruct.APB1CLKDivider "c++" line= RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/"1 " start= 80MHz"111">uint8_t HAL_I2C_Read(uint8_t addr, uint8_t *pData, uint16_t len) RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz{ // RCC时钟配置,出错则进入错误处理函数判断是否存在数据,不存在返回HAL_ERROR if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) !len == HAL_OK) // HCLK0 || pData =80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)0) { _Error_Handler(__FILE__, __LINE__)return HAL_ERROR;
}
// 接收数据,并返回接收状态
return HAL_I2C_Master_Receive(&hi2c2,addr,pData,len,100);
}
</syntaxhighlight>
// 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz PeriphClkInit= gyu_sht20.Lpuart1ClockSelection c = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义 PeriphClkInit.PLLSAI1.PLLSAI1Source 读取SHT20温度的函数,最终返回是温度是采集值,不是真实的温度值。<syntaxhighlight lang= RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M "c++" line= "1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q " start= RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz"52"> PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数 if uint16_t SHT20_ReadTemp(HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OKvoid) { _Error_Handler(__FILE__, __LINE__) uint16_t temp = 0; }
// 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz发送“读取温度指令” if uint8_t cmd = SHT20_MEASURE_TEMP_CMD; HAL_I2C_Send(HAL_PWREx_ControlVoltageScalingSHT20_WRITE_ADDR,&cmd,1); // 获取温度采集值,3位数据分别为:Data(PWR_REGULATOR_VOLTAGE_SCALE1MSB) != HAL_OK、Data(LSB)、CheckSum uint8_t pDATA[3] = {0,0,0}; _Error_Handler HAL_I2C_Read(__FILE__SHT20_READ_ADDR, __LINE__pDATA,3); }  // 配置系统定时器中断时间,配置为HCLK的千分频计算出真实的采集值,保留14bit(MSB 8bit、LSB 高6bit) HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000)temp = pDATA[0]// 配置系统定时器,配置为HCLKtemp <<= 8; HAL_SYSTICK_CLKSourceConfigtemp += (SYSTICK_CLKSOURCE_HCLKpDATA[1] & 0xfc); // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0)返回温度采集值 HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0)return temp;
}
</syntaxhighlight> ==== gyu_dac.c ====DAC初始化函数,配置DAC1通道1(也就是PA4引脚)为DAC输出引脚。读取SHT20湿度的函数,最终返回是湿度是采集值,不是真实的湿度值。<syntaxhighlight lang="c++" line="1" start="3781">void MX_DAC1_Inituint16_t SHT20_ReadRH(void)
{
hdac1.Instance uint16_t rh = DAC10; // 配置为DAC1 if (HAL_DAC_Init(&hdac1) != HAL_OK) // 初始化DAC1 { _Error_Handler(__FILE__, __LINE__); }
// DAC通道配置结构体定义发送“读取湿度指令” DAC_ChannelConfTypeDef sConfig; sConfig.DAC_SampleAndHold = DAC_SAMPLEANDHOLD_DISABLE; uint8_t //DAC模式 sConfig.DAC_Triggercmd =DAC_TRIGGER_NONESHT20_MEASURE_RH_CMD; // 不使用触发功能 sConfig.DAC_OutputBuffer=DAC_OUTPUTBUFFER_DISABLE; // DAC1输出缓冲关闭 sConfig.DAC_ConnectOnChipPeripheral = DAC_CHIPCONNECT_DISABLEHAL_I2C_Send(SHT20_WRITE_ADDR,&cmd,1); //不连接到片内外设
// 初始化通道CH1配置获取湿度采集值,3位数据分别为:Data(MSB)、Data(LSB)、CheckSum if (HAL_DAC_ConfigChanneluint8_t pDATA[3] = {0,0,0}; HAL_I2C_Read(&hdac1SHT20_READ_ADDR, &sConfigpDATA, DAC_CHANNEL_13) !; // 计算出真实的采集值,保留12bit(MSB 8bit、LSB 高4bit) rh = HAL_OK)pDATA[0]; {rh <<= 8; _Error_Handler rh += (__FILE__, __LINE__pDATA[1] & 0xf0); }
HAL_DAC_Start(&hdac1,DAC_CHANNEL_1); //开启DAC通道1返回湿度采集值 return rh;
}
</syntaxhighlight>重新定义DAC的硬件引脚,可以看到,配置PA4为DAC引脚。SHT20软件复位函数,工程中没有使用此函数。<syntaxhighlight langline="c++1" linestart="1110" startlang="70c">void HAL_DAC_MspInitSHT20_SoftReset(DAC_HandleTypeDef* hdacvoid)
{
GPIO_InitTypeDef GPIO_InitStruct; if(hdac->Instance==DAC1) // 判断是否是DAC1发送SHT20软件复位指令 { __HAL_RCC_DAC1_CLK_ENABLE(); // 使能DAC时钟 GPIO_InitStruct.Pin uint8_t cmd = GPIO_PIN_4SHT20_MEASURE_RH_CMD; // 选择PA4 GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;// 配置模拟模式 GPIO_InitStruct.Pull = GPIO_NOPULL; // 无上下拉 HAL_GPIO_Init HAL_I2C_Send(GPIOASHT20_WRITE_ADDR, &GPIO_InitStructcmd,1); // 初始化PA4引脚配置 }
}
</syntaxhighlight>设置DAC引脚输出电压,value为单元值,不是真正的电压值大小。 模拟电压值(范围:0~4095,对应0~3.3V)。温湿度转换函数,用于将采集到的温湿度采集值,转化成真实的温湿度值。<syntaxhighlight lang="c++" line="1" start="92127">void HAL_DAC_Setfloat SHT20_Convert(uint16_t value,uint8_t isTemp)
{
HAL_DAC_SetValuefloat tmp = 0.0; // 判断本次需要转换的值是温度还是湿度 if(isTemp) { tmp = -46.85 + (175.72* value)/(1 << 16); // 温度值转换,公式:T = -46.85 + 175.72*(S/2^16) } else { tmp = -6 + (&hdac1,DAC_CHANNEL_1,DAC_ALIGN_12B_R,125.0 *value)/(1<<16); //湿度值转换,公式:RH = -6.00 + 125.00*(S/ 配置CH1 12位右对齐模拟输出2^16) } return tmp;
}
</syntaxhighlight>
== 实验07-温湿度采集 ==
此实验给大家展示的是利用STM32L476的I2C外设功能,去获取sht20温湿度传感器采集的温湿度数据。并且将获取到的数据转换成真实的温湿度数据,格式化打印到串口显示。
==实验08-RGB = STM32L476 I2C简介 ===I2C(内部集成电路)总线接口处理STM32L4和串行I2C总线之间的通信。 它提供多主机功能,并控制所有I2C总线特定的排序,协议,仲裁和定时。 它支持标准模式(Sm),快速模式(Fm)和快速模式加(Fm +)。兼容SMBus(系统管理总线)和PMBus(电源管理总线)。
I2C主要功能:== 实验09-红外接收 ==红外接收实验,是利用开发板上的红外接收传感器,去获取遥控器按下的信号,红外传感器获取到这个信号后,会转成一段PWM波形从它的DATA引脚输出。此时我们利用STM32的定时器捕获功能,就可以获取到这个PWM波形所携带的信息,以此判断遥控器按下的是哪个按键。
•I2C总线规范rev03兼容性:=== STM32L476 定时器捕获简介 ===以下部分为TIM2 / TIM3 / TIM4 / TIM5这四个定时器的介绍。
- 从模式和主模式1.通用定时器简介:
- 多主机功能通用定时器由一个由可编程预分频器驱动的16位或32位自动重载计数器组成。它们可用于各种目的,包括测量输入信号的脉冲长度(输入捕获)或生成输出波形(输出比较和PWM)。
- 标准模式(最高100 kHz)使用定时器预分频器和RCC时钟控制器预分频器,可以将脉冲长度和波形周期从几微秒调制到几毫秒。定时器完全独立,不共享任何资源。
- 快速模式(最高400 kHz)2.通用定时器功能
- 快速模式加(最高1 MHz)•16位(TIM3,TIM4)或32位(TIM2和TIM5)上,下,上/下自动重载计数器。
- 7位和10位寻址模式•16位可编程预分频器,用于分频(也“在运行中”)计数器时钟
- 多个7位从机地址(2个地址,1个带可配置掩码)频率由1到65535之间的任何因子组成。
- 所有7位地址确认模式•最多4个独立频道:
- 一般电话输入捕获
- 可编程设置和保持时间输出比较
- 易于使用的事件管理 PWM生成(边缘和中心对齐模式)
- 可选的时钟拉伸单脉冲模式输出
- 软件重置•同步电路,用外部信号控制定时器并互连
•具有DMA功能的1字节缓冲区几个计时器。
•可编程模拟和数字噪声滤波器•以下事件的中断/ DMA生成: - 更新:计数器溢出/下溢,计数器初始化(通过软件或 内部/外部触发器)
=== 硬件设计 ===选择STM32L4引脚PB13作为I2C SCL引脚,PB14作为I2C SDA引脚。[[文件:NBDK-SCH-SHT20.png|边框|居中|无框|353x353像素]]触发事件(计数器启动,停止,初始化或通过内部/外部触发计数)
=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到DBG端,SW2拨到MCU。# 使用Keil打开基础实验 07-温湿度实验工程。# 使用Xshell打开Jlink虚拟出的COM口# 下载程序,并完成功能测试。输入捕获
=== 实验验证 ===下载完成后,打开COM口,可以看到每隔500ms打印一次采集到的温湿度数据。- 输出比较
在采集的过程中,我们将手指按在SHT20上,可以看到温度和湿度都在上升,例如温度,由开始的20.4°C上升到最终的26.7°C。[[文件:NBDK-XSHELL-SHT20.png|边框|居中|无框|759x759像素]]=== 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。•支持增量(正交)编码器和霍尔传感器电路进行定位
==== stm32l4xx_hal_conf.h ====此文件位于“07-温湿度实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。目的
此例程我们主要给大家展示STM32L4的I2C功能,所以我们宏定义中打开I2C相关的。<syntaxhighlight lang="c" line="1" start="103">// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC•外部时钟或逐周期电流管理的触发输入
#define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_UART_MODULE_ENABLED // UART#define HAL_DMA_MODULE_ENABLED // DMA #define HAL_ADC_MODULE_ENABLED // ADC#define HAL_DAC_MODULE_ENABLED // DAC#define HAL_I2C_MODULE_ENABLED // I2C</syntaxhighlight>3.通用定时器捕获模式
==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。在输入捕捉模式下,捕捉/比较寄存器(TIMx_CCRx)用于在相应ICx信号检测到转换后锁存计数器的值。 发生捕获时,会设置相应的CCXIF标志(TIMx_SR寄存器),如果使能了中断或DMA请求,则可以发送它们。 如果在CCxIF标志已经为高电平时发生捕获,则设置过捕获标志CCxOF(TIMx_SR寄存器)。 CCxIF可以通过软件将其写入0或读取存储在TIMx_CCRx寄存器中的捕获数据来清除。 将其写入0时,CCxOF将被清除。
接下来我们初始化了串口部分,目的是打印采集到的温湿度数据。==== 遥控器协议说明 ====遥控器使用的协议,被称为NEC码,NEC的编码方式如下所示,这个部分大家需要理解清楚,方便后续代码的阅读。
接下来初始化I2C引脚。NEC码高低电平位定义如下:
在while()循环中,我们每隔500ms采集一次温湿度的值,并且将采集的温湿度值转化成真实值,格式化打印到串口显示。一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平),一个逻辑 1 传输需要 2.25ms(560us脉冲+1680us 低电平)。[[文件:NBDK-INTER-NEC1.jpg|边框|居中|无框|625x625像素]]
<syntaxhighlight lang="c++" line="1" start="33">int main(void){ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟 HAL_Init();NEC数据格式为:
// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config(); // 初始化串口USART1 MX_USART1_UART_Init(); // 初始化I2C2 MX_I2C2_Init(); // while (1) { HAL_Delay(500); printf("Temp = %.1f\r\n",SHT20_Convert(SHT20_ReadTemp(),1)); printf("RH = %.1f%%\r\n",SHT20_Convert(SHT20_ReadRH(),0)); }}</syntaxhighlight>==== gyu_util.c ====时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。引导码、用户地址码、用户地址反码、数据码、数据反码。
主要包含了三个部分的初始化配置。引导码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,用户地址码、用户地址反码、数据码、数据反码均是8 位数据格式。  
1.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。当按键被持续按下时,每隔108ms重新发送一次此数据,所以我们可以利用计时超过108ms的方式,来计算按键持续按下的次数(代码中是判断的110ms)。
2[[文件:NBDK-INTER-NEC.时钟配置,选择系统、AHB总线及APB总线的时钟来源。jpg|边框|居中|无框|720x720像素]]
3.外设时钟配置,选择外设时钟来源。''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 硬件设计 ===选择STM32L4引脚PC6用来捕获红外传感器HS0038的DATA引脚输出的PWM波。为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''[[文件:NBDK-SCH-IR.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。 基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。png|边框|居中|无框|393x393像素|''<syntaxhighlight langspan class="c++tlid-translation-gender-indicator translation-gender-indicator" line></span>'']]''<span class="1tlid-translation-gender-indicator translation-gender-indicator" start></span><span class="49tlid-translation-gender-indicator translation-gender-indicator">void SystemClock_Config(void){ RCC_OscInitTypeDef RCC_OscInitStruct; </span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 定义RCC内部span><span class="tlid-translation-gender-indicator translation-gender-indicator"></外部振荡器结构体 RCC_ClkInitTypeDef RCC_ClkInitStruct; span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 定义RCC扩展时钟结构体span>''=== 实验准备 === # 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。 // 配置LSE驱动器功能为低驱动能力# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到DBG端,SW2拨到MCU。# 使用Keil打开基础实验 09-红外线接收实验工程。# 使用Xshell打开Jlink虚拟出的COM口# 下载程序,并完成功能测试。 === 实验验证 ===下载完成后,我们按下遥控器上的任意按键,可以看到LCD上将显示如下,irBtnVal代表的是键值,irBtnCnt代表是按键被按下的次数,irBtnInfo代表按键的图标或者定义。 === 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==== stm32l4xx_hal_conf.h ==== __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);此文件位于“09-红外线接收实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。
// 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType 此例程我们主要给大家展示STM32L4的I2C功能,所以我们宏定义中打开I2C相关的。<syntaxhighlight lang= RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM "c" line= "1; // 配置PLL VCO输入分频为1,8/1 " start= 8MHz"103"> RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz使能的宏 RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; /#define HAL_MODULE_ENABLED / SAI时钟7分频,160/7 = 22.857143MHz芯片 RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; #define HAL_FLASH_MODULE_ENABLED // SDMMC、RNG、USB时钟2分频,160/2 = 80MHzFlash RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; #define HAL_PWR_MODULE_ENABLED // 系统主时钟分区2分频,160/2 = 80MHz电源 #define HAL_RCC_MODULE_ENABLED // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); } 时钟#define HAL_CORTEX_MODULE_ENABLED // 初始化CPU,AHB和APB总线时钟 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4) { _Error_Handler(__FILE__, __LINE__); }NVIC
// 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; #define HAL_GPIO_MODULE_ENABLED // 配置串口USART1时钟为PCLK2,80MHzGPIO PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; #define HAL_UART_MODULE_ENABLED // 配置串口USART2时钟为PCLK1,80MHzUART PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; #define HAL_DMA_MODULE_ENABLED // 配置LPUART时钟为HSI,16MHzDMA PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; #define HAL_ADC_MODULE_ENABLED // 配置I2C2时钟为PCLK1,80MHzADC PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; #define HAL_DAC_MODULE_ENABLED // 配置LPTIM1时钟为LSE,32.768KHzDAC PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; #define HAL_I2C_MODULE_ENABLED // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义I2C PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; <// 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }syntaxhighlight>
// 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1==== main.2V,系统频率高达80MHzc ==== if main函数,我们的例程由此处开始执行,首先调用HAL_Init(HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { _Error_Handler函数初始化我们的模块,接着调用SystemClock_Config(__FILE__, __LINE__); }函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
// 配置系统定时器中断时间,配置为HCLK的千分频 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);接下来我们初始化LCD的SPI控制引脚,LCD背光引脚,并且初始化LCD的图形控制界面。
// 配置系统定时器,配置为HCLK HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);然后我们在LCD上打印固定的遥控器按键显示格式,也就是"irBtnVal"、"irBtnCnt"、"irBtnInfo"这几个字符串。
// 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight>接下来我们初始化我们此实验的重点功能,也就是TIM3定时器。
==== gyu_i2c.c ====在讲解i2c代码之前,我们先给大家讲解一下参数Timing,这个值是通过计算得来的,在STM32芯片手册的P1238页有计算公式说明,我们这边偷懒,利用STM32CUBE里面的配置功能,对应SHT20的I2C参数要求。在while()循环中,我们轮询遥控器的按键信息,一旦有遥控器按下,则在LCD的对应位置,打印按键的信息。
我们配置I2C时钟为100KHz,Rise Time 300ns,Fall Time 100ns。最终得出Timing值为0x10D05E82。[[文件:NBDK-DS-SHT20.png|边框|居中|无框|557x557像素]][[文件:NBDK-CUBE-SHT20.png|边框|居中|无框|643x643像素]] 初始化I2C引脚,选择的I2C2。<syntaxhighlight lang="c++" line="1" start="3634">void MX_I2C2_Initint main(void)
{
hi2c2.Instance irInfo_t irkey = I2C2{0,0}; // I2C寄存器基础地址,定义为I2C2的 hi2c2.Init.Timing = 0x10D05E82; // 指定I2C_TIMINGR寄存器值,此值必须在I2C初始化之前配置
if (HAL_I2C_Init(&hi2c2) != HAL_OK) /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ 初始化I2C2 { _Error_Handler(__FILE__, __LINE__); // 如果初始化失败,则进入错误处理重置所有外设、flash界面以及系统时钟 }}</syntaxhighlight>定义I2C2功能引脚,选择PB13为SCL引脚,PB14为SDA引脚。并且使能GPIOB以及I2C2的时钟。<syntaxhighlight lang="c++" line="1" start="55">void HAL_I2C_MspInitHAL_Init(I2C_HandleTypeDef* i2cHandle);{ // 使能GPIOB引脚时钟,因为选择的I2C引脚均在PB上配置系统时钟(包含振荡器、系统时钟、总线时钟等等) __HAL_RCC_GPIOB_CLK_ENABLESystemClock_Config();
// 定义GPIO结构LCD SPI初始化 GPIO_InitTypeDef GPIO_InitStructLCD_GPIO_Init(); // LCD IO控制引脚(例如背光) MX_SPI1_Init(); // LCD SPI控制引脚
// 判断I2C是否选择的是I2C2图形界面初始化 ifGUI_Init(i2cHandle->Instance==I2C2) { ; // I2C2引脚配置GUI界面初始化 GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_14 GUI_Clear(); // 选择PB13为SCL引脚,PB14为SDA引脚清屏 GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 外设功能为开漏模式 GPIO_InitStruct.Pull = GPIO_PULLUP GUI_SetColor(GUI_Crimson); // 上拉红色字体 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH GUI_DispStringAt("irBtnVal:",24,24); // 高速模式打印字符串"irBtnVal:"到位置X->24,Y->24 GPIO_InitStruct.Alternate = GPIO_AF4_I2C2 GUI_DispStringAt("irBtnCnt:",24,72); // 外设引脚选择I2C2打印字符串"irBtnCnt:"到位置X->24,Y->96 HAL_GPIO_Init GUI_DispStringAt(GPIOB"irBtnInfo:",24, &GPIO_InitStruct120); // 初始化IO配置打印字符串"irBtnCnt:"到位置X->24,Y->120
// 使能I2C2时钟初始化TIM3 __HAL_RCC_I2C2_CLK_ENABLE MX_TIM3_Init(); }}</syntaxhighlight>I2C发送数据的函数。<syntaxhighlight lang="c++" line="1" start="89">uint8_t HAL_I2C_Send(uint8_t addr , uint8_t *pData, uint16_t len){ // 判断是否存在数据,不存在返回HAL_ERROR ifwhile(len == 0 || pData == 01)
{
return HAL_ERRORirkey = IRBNT_POLL(); // 轮训获取IR按键信息 } if(irkey.irBtnVal) // 如果按键信息存在 { GUI_DispHexAt(irkey.irBtnVal,144,24,2); // 发送数据,并返回发送状态打印按键值 return HAL_I2C_Master_Transmit GUI_DispHexAt(&hi2c2irkey.irBtnCnt,addr144,pData72,len2); // 打印按键计数 // 打印按键图标或名称 GUI_GotoXY(144,100120); // 指的光标位置} GUI_ClearArea(); // 清除指定位置数据< switch(irkey.irBtnVal) //syntaxhighlight>I2C接收数据的函数。<syntaxhighlight lang=判断按键值,打印相应图标或名称 { case REMOTE_BTN_SWITCH: GUI_DispString("'switch'"); break; case REMOTE_BTN_MENU: GUI_DispString("c++'menu'" line=); break; case REMOTE_BTN_MUTE: GUI_DispString("1'mute'" start=); break; case REMOTE_BTN_MODE: GUI_DispString("111'mode'">); break;uint8_t HAL_I2C_Read case REMOTE_BTN_PLUS: GUI_DispString(uint8_t addr, uint8_t *pData, uint16_t len"'+'"); break;{ case REMOTE_BTN_RETURN: GUI_DispString("'return'"); break; // 判断是否存在数据,不存在返回HAL_ERROR case REMOTE_BTN_REWIND: GUI_DispString("'|<<'"); break; if case REMOTE_BTN_PAUSE: GUI_DispString(len == 0 "'>|| pData == '"); break; case REMOTE_BTN_FASTFORWARD: GUI_DispString("'>>|'"); break; case REMOTE_BTN_0: GUI_DispString("'0'"); break; { case REMOTE_BTN_LESS: GUI_DispString("'-'"); break; case REMOTE_BTN_OK: GUI_DispString("'OK'"); break; case REMOTE_BTN_1: GUI_DispString("'1'"); break; case REMOTE_BTN_2: GUI_DispString("'2'"); break; case REMOTE_BTN_3: GUI_DispString("'3'"); break; case REMOTE_BTN_4: GUI_DispString("'4'"); break; return HAL_ERROR case REMOTE_BTN_5: GUI_DispString("'5'"); break; } case REMOTE_BTN_6: GUI_DispString("'6'"); break; case REMOTE_BTN_7: GUI_DispString("'7'"); break; // 接收数据,并返回接收状态 case REMOTE_BTN_8: GUI_DispString("'8'"); break; return HAL_I2C_Master_Receive case REMOTE_BTN_9: GUI_DispString(&hi2c2,addr,pData,len,100"'9'");break; } } }
}
</syntaxhighlight>
==== gyu_util.c ====
请参照实验01中的介绍。
 
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
==== gyu_irc ====
初始化定时器TIM3,首先配置TIM3的时钟为1MHz(也就是1us),我们设置它向上自动装载,并且设置自动装载值为10000,通过计算可以知道装满一次需要10ms。
==== gyu_sht20.c ====读取SHT20温度的函数,最终返回是温度是采集值,不是真实的温度值。<syntaxhighlight lang="c++" line="1" start="52">uint16_t SHT20_ReadTemp(void){ uint16_t temp = 0;然后我们需要设置输入捕获的参数,我们配置上升沿下降沿都捕获,并且设置8个时钟周期的滤波(防止误识别)。
最后使能TIM3的中断,并且开始捕获TIM3的通道1(也就是PC6引脚)。 <syntaxhighlight lang="c++" line="1" start="59">void MX_TIM3_Init(void){ htim3.Instance = TIM3; // 通用定时器3 htim3.Init.Prescaler = 80-1; // TIM3 80预分频器(APB2总线),80MHz / 80 = 1MHz(1us) htim3.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数器 htim3.Init.Period = 10000; // 发送“读取温度指令”自动装载值设为10000,装满一次 10000 * 1us = 10ms uint8_t cmd htim3.Init.ClockDivision = SHT20_MEASURE_TEMP_CMDTIM_CLOCKDIVISION_DIV1;// 不分频 HAL_I2C_Sendhtim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; // 自动加载使能 if (HAL_TIM_IC_Init(SHT20_WRITE_ADDR,&cmdhtim3) != HAL_OK) // 初始化TIM3,出错则进入错误处理函数 { _Error_Handler(__FILE__,1__LINE__); }
// 获取温度采集值,3位数据分别为:Data初始化TIM3输入捕获参数 TIM_IC_InitTypeDef sConfigIC; // 定义输入捕获结构体(IC:Input capture) sConfigIC.ICPolarity = TIM_ICPOLARITY_BOTHEDGE; // 上升沿下降沿都捕获 sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI; // 配置为TI1 sConfigIC.ICPrescaler = TIM_ICPSC_DIV1; // 不分频 sConfigIC.ICFilter = 0x03; // IC1F=0011 8个定时器时钟周期滤波 // 初始化TIM3 CH1通道,出错则进入错误处理函数 if (HAL_TIM_IC_ConfigChannel(MSB&htim3, &sConfigIC, TIM_CHANNEL_1)、Data(LSB!= HAL_OK)、CheckSum uint8_t pDATA[3] = {0,0,0}; HAL_I2C_Read _Error_Handler(SHT20_READ_ADDR__FILE__,pDATA,3__LINE__); }
HAL_TIM_Base_Start_IT(&htim3); // 计算出真实的采集值,保留14bit(MSB 8bit、LSB 高6bit) temp = pDATA[0]; temp <<= 8;使能更新中断(也就是TIM_IT_UPDATE) temp += HAL_TIM_IC_Start_IT(pDATA[1] & 0xfchtim3, TIM_CHANNEL_1); // 返回温度采集值 return temp;开始捕获TIM3 CH1
}
</syntaxhighlight>读取SHT20湿度的函数,最终返回是湿度是采集值,不是真实的湿度值。使能GPIOC以及TIM3的时钟,并且配置PC6为TIM3的通道1。<syntaxhighlight lang="c++" line="1" start="8196">uint16_t SHT20_ReadRHvoid HAL_TIM_IC_MspInit(voidTIM_HandleTypeDef *htim)
{
uint16_t rh = 0GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_TIM3_CLK_ENABLE(); // 使能TIM3时钟 __HAL_RCC_GPIOC_CLK_ENABLE(); // 开启GPIOC时钟
GPIO_Initure.Pin = GPIO_PIN_6; // PC6 GPIO_Initure.Mode = GPIO_MODE_AF_PP; // 推挽输出 GPIO_Initure.Pull = GPIO_PULLUP; // 上拉 GPIO_Initure.Speed = GPIO_SPEED_HIGH; // 发送“读取湿度指令”高速模式 uint8_t cmd GPIO_Initure.Alternate = SHT20_MEASURE_RH_CMDGPIO_AF2_TIM3;// PC6配置为TIM3通道1 HAL_I2C_SendHAL_GPIO_Init(SHT20_WRITE_ADDRGPIOC,&cmd,1GPIO_Initure);
// 获取湿度采集值,3位数据分别为:Data(MSB)、DataHAL_NVIC_SetPriority(LSB)、CheckSum uint8_t pDATA[3] = {0TIM3_IRQn,010,0}; HAL_I2C_Read(SHT20_READ_ADDR,pDATA,3); // 计算出真实的采集值,保留12bit(MSB 8bit、LSB 高4bit) rh = pDATA[0]; rh <<= 8;设置TIM3中断优先级 rh += HAL_NVIC_EnableIRQ(pDATA[1] & 0xf0TIM3_IRQn); // 返回湿度采集值 return rh;使能TIM3中断}</syntaxhighlight>SHT20软件复位函数,工程中没有使用此函数。定时器周期中断回调函数,TIM3的自动装载值装满一次,进入一次此回调(本工程配置的参数是10ms进入一次)。 我们判断是否已经接收到引导码(根据引导码标志位判断),一旦接收到引导码,我们认为已经开始了一次NEC数据的接收。 如果是接收到引导码之后,第一次进入此函数,那么我们使能记录遥控器按键值的标志位,也就是代表接收到了一次NEC数据(不管数据对错)。 如果进入的次数少于11次,则继续增加计数,当计数值等于11时(也就是从接收到引导码已经过去至少110ms时),我们认为一次NEC的数据获取已经完成,此时清除周期回调的计数值,并且删除引导码标志位(下次再进到这个函数时,只有新的引导码数据到来,才会进行新的数据处理)。<syntaxhighlight lang="c++" line="1" start="110" lang="c122">void SHT20_SoftResetHAL_TIM_PeriodElapsedCallback(voidTIM_HandleTypeDef *htim)
{
// 发送SHT20软件复位指令 uint8_t cmd = SHT20_MEASURE_RH_CMD; HAL_I2C_Sendif(SHT20_WRITE_ADDR,&cmd,1);}</syntaxhighlighthtim->温湿度转换函数,用于将采集到的温湿度采集值,转化成真实的温湿度值。<syntaxhighlight lang="c++" line="1" startInstance ="127">float SHT20_Convert(uint16_t value,uint8_t isTemp){ float tmp = 0.0; // 判断本次需要转换的值是温度还是湿度 if(isTempTIM3)
{
tmp if(irStatus & IR_STATUS_BootCode) // 如果接收到引导码 { irStatus &= -46.85 + ~IR_STATUS_Rising; // 删除上升沿标记(以防止本次出错,确保下次采集流程正确) if(175.72* valuetim3Cnt == 0) // 如果是第一次进入(计数为0) { irStatus |= IR_STATUS_BtnInfo; // 记录已经获取到IR按键信号(遥控器按键值) } if((1 <tim3Cnt & 0X0F) < 1611); // 温度值转换,公式:T = -46.85 进入回调少于11次 { tim3Cnt++ 175.72*(S; //2^16)计数值自加 } else // 超过11次,代表一次采集超时(不论是否成功) { tmp irStatus &= -6 + (125.0 *value)/(1<<16)~IR_STATUS_BootCode; // 湿度值转换,公式:RH 删除引导码标记 tim3Cnt = -6.00 + 125.00*(S0; //2^16)清除计数值 } }
}
return tmp;
}
</syntaxhighlight>处理TIM3 CH1(PC6引脚)捕获的数据,这边要记得初始化中我们使能了上升沿下降沿都捕获数据。
== 08-RGB实验 ==当有边沿捕获到来,我们判断此时PC6引脚的电平。
== 实验09-红外接收 ==红外接收实验,是利用开发板上的红外接收传感器,去获取遥控器按下的信号,红外传感器获取到这个信号后,会转成一段PWM波形从它的DATA引脚输出。此时我们利用STM32的定时器捕获功能,就可以获取到这个PWM波形所携带的信息,以此判断遥控器按下的是哪个按键。如果此时是高电平,则代表刚刚的是上升沿,此时我们使能上升沿标志位,并且清空定时器计数值。
如果此时是低电平,且上升沿标识位被置位,则我们根据获取到的计数值(也就是上一次高电平的持续时间)来判断本段PWM波代表的含义。<syntaxhighlight lang="c++" line="1" start="155">void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){ // 判断是否为TIM3 CH1捕获产生的回调 if((htim->Instance == TIM3) && (htim->Channel = STM32L476 定时器捕获简介 =HAL_TIM_ACTIVE_CHANNEL_1)) { if(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6)) // 获取PC6引脚电平,如果是高电平,则代表是上升沿捕获 { __HAL_TIM_SET_COUNTER(&htim3, 0); // 清除TIM3定时器计数值 irStatus |=IR_STATUS_Rising; // 标记上升沿捕获 } else //如果是低电平,则代表是下降沿触发 { tim3Val =HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1); // 读取TIM3 CH1定时器计数值以下部分为TIM2 if(irStatus & IR_STATUS_Rising) // TIM3 存在上升沿标记,我们比较定时器计数值 { if(tim3Val > 260 && tim3Val < 860) / TIM4 / TIM5这四个定时器的介绍。高电平持续560us代表bit 0,范围560us±300us { irRecData <<= 1.通用定时器简介:; // 左移一位 irRecData |= 0; // bit位赋值0 } else if(tim3Val > 1380 && tim3Val < 1980) // 高电平持续1680us代表bit 1,范围1680us±300us {通用定时器由一个由可编程预分频器驱动的16位或32位自动重载计数器组成。它们可用于各种目的,包括测量输入信号的脉冲长度(输入捕获)或生成输出波形(输出比较和PWM)。 irRecData <<= 1; // 左移一位 irRecData |= 1; // bit位赋值1使用定时器预分频器和RCC时钟控制器预分频器,可以将脉冲长度和波形周期从几微秒调制到几毫秒。定时器完全独立,不共享任何资源。 } else if(tim3Val > 2200 && tim3Val < 2800) // 高电平持续2500us代表本次按键结束,范围2500us±300us2.通用定时器功能 { irCnt++; // 按键次数新增1•16位(TIM3,TIM4)或32位(TIM2和TIM5)上,下,上 tim3Cnt = 0; //下自动重载计数器。清除计数值 }•16位可编程预分频器,用于分频(也“在运行中”)计数器时钟 else if(tim3Val > 4200 && tim3Val < 4800) // 高电平持续4500us代表新的按键,范围4500us±300us {频率由1到65535之间的任何因子组成。 irStatus |= IR_STATUS_BootCode; // 标记引导码 irCnt = 0; // 有新的按键到来,清除按键计数•最多4个独立频道: } }- 输入捕获 irStatus &= ~IR_STATUS_Rising; // 清除上升沿标记- 输出比较 } }- PWM生成(边缘和中心对齐模式)}</syntaxhighlight>轮询当前的按键信息。如果按键值标识存在,则代表有按键被按下,接着判断地址码以及数据数据正确,并将最终的按键数据添加到irinfo中留给应用层调用。<syntaxhighlight lang="c++" line="1" start="206">- 单脉冲模式输出irInfo_t IRBNT_POLL(void){•同步电路,用外部信号控制定时器并互连 irInfo_t irinfo = {0,0}; // 定义按键信息结构体 uint8_t bcode, dcode; // 定义引导码正反编码几个计时器。 uint8_t bvalue, dvalue; // 定义按键值正反编码 •以下事件的中断 if(irStatus & IR_STATUS_BtnInfo) / DMA生成:/ 如果获取到按键值 {- 更新:计数器溢出 bcode = irRecData >> 24; // 地址码 dcode = (irRecData >> 16) & 0xff; //下溢,计数器初始化(通过软件或地址码反编码
内部 if((bcode == (uint8_t)~dcode) && bcode == REMOTE_DEVICE_ID) /外部触发器)/ 判断地址码是否正确 { bvalue = irRecData >> 8; // 按键值 dvalue = irRecData; // 按键值反编码 if(bvalue == (uint8_t)~dvalue) // 判断按键值是否正确 {- 触发事件(计数器启动,停止,初始化或通过内部 irinfo.irBtnVal = bvalue; //外部触发计数)将按键值赋给irinfo.irBtnVal } }- 输入捕获 irinfo.irBtnCnt = irCnt; // 将按键此处赋给irinfo.irBtnCnt- 输出比较 }
•支持增量(正交)编码器和霍尔传感器电路进行定位 return irinfo; // 返回按键信息}</syntaxhighlight>
目的== 实验10-串口打印 ==串口打印实验,给大家展示的是如何配置STM32L476一个有效的硬件串口功能,并且顺带给大家介绍了如何去配置一个格式化打印函数printf()。
•外部时钟或逐周期电流管理的触发输入=== STM32L476 UART简介 ===USART主要功能:
3.通用定时器捕获模式•全双工异步通信
在输入捕捉模式下,捕捉•NRZ标准格式(标记/比较寄存器(TIMx_CCRx)用于在相应ICx信号检测到转换后锁存计数器的值。 发生捕获时,会设置相应的CCXIF标志(TIMx_SR寄存器),如果使能了中断或DMA请求,则可以发送它们。 如果在CCxIF标志已经为高电平时发生捕获,则设置过捕获标志CCxOF(TIMx_SR寄存器)。 CCxIF可以通过软件将其写入0或读取存储在TIMx_CCRx寄存器中的捕获数据来清除。 将其写入0时,CCxOF将被清除。空格)
==== 遥控器协议说明 ====遥控器使用的协议,被称为NEC码,NEC的编码方式如下所示,这个部分大家需要理解清楚,方便后续代码的阅读。•可配置的过采样方法16或8,以提供速度和速度之间的灵活性
NEC码高低电平位定义如下:时钟容差
一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平),一个逻辑 1 传输需要 2.25ms(560us脉冲+1680us 低电平)。[[文件:NBDK-INTER-NEC1.jpg|边框|居中|无框|625x625像素]]•通用可编程发送和接收波特率高达10 Mbit / s时
NEC数据格式为:时钟频率为80 MHz,过采样为8
引导码、用户地址码、用户地址反码、数据码、数据反码。•双时钟域允许: - USART功能和从停止模式唤醒
引导码由一个 9ms 的低电平和一个 4.5ms 的高电平组成,用户地址码、用户地址反码、数据码、数据反码均是8 位数据格式。  - 独立于PCLK重新编程的便捷波特率编程
当按键被持续按下时,每隔108ms重新发送一次此数据,所以我们可以利用计时超过108ms的方式,来计算按键持续按下的次数(代码中是判断的110ms)。•自动波特率检测
[[文件:NBDK-INTER-NEC.jpg|边框|居中|无框|720x720像素]]•可编程数据字长(7,8或9位)
''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 硬件设计 ===选择STM32L4引脚PC6用来捕获红外传感器HS0038的DATA引脚输出的PWM波。''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''[[文件:NBDK-SCH-IR.png|边框|居中|无框|393x393像素|''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span>'']]''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到DBG端,SW2拨到MCU。# 使用Keil打开基础实验 09-红外线接收实验工程。# 使用Xshell打开Jlink虚拟出的COM口# 下载程序,并完成功能测试。•可编程数据顺序,具有MSB优先或LSB优先移位
=== 实验验证 ===下载完成后,我们按下遥控器上的任意按键,可以看到LCD上将显示如下,irBtnVal代表的是键值,irBtnCnt代表是按键被按下的次数,irBtnInfo代表按键的图标或者定义。•可配置的停止位(1或2个停止位)
=== 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。•同步模式和时钟输出,用于同步通信
==== stm32l4xx_hal_conf.h ====此文件位于“09-红外线接收实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。•单线半双工通信
此例程我们主要给大家展示STM32L4的I2C功能,所以我们宏定义中打开I2C相关的。<syntaxhighlight lang="c" line="1" start="103">•使用DMA进行持续通信// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED /•使用集中式DMA将接收/ NVIC发送的字节缓冲在保留的SRAM中
#define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_UART_MODULE_ENABLED // UART#define HAL_DMA_MODULE_ENABLED // DMA #define HAL_ADC_MODULE_ENABLED // ADC#define HAL_DAC_MODULE_ENABLED // DAC#define HAL_I2C_MODULE_ENABLED // I2C</syntaxhighlight>•发送器和接收器的独立使能位
==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。•独立的信号极性控制,用于发送和接收
接下来我们初始化LCD的SPI控制引脚,LCD背光引脚,并且初始化LCD的图形控制界面。•可交换Tx / Rx引脚配置
然后我们在LCD上打印固定的遥控器按键显示格式,也就是"irBtnVal"、"irBtnCnt"、"irBtnInfo"这几个字符串。•调制解调器和RS-485收发器的硬件流控制
接下来我们初始化我们此实验的重点功能,也就是TIM3定时器。•通信控制/错误检测标志
在while()循环中,我们轮询遥控器的按键信息,一旦有遥控器按下,则在LCD的对应位置,打印按键的信息。•奇偶校验控制:
<syntaxhighlight lang="c++" line="1" start="34">- 传输奇偶校验位int main(void){- 检查接收数据字节的奇偶校验 irInfo_t irkey = {0,0}; •带有标志的14个中断源 /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟 HAL_Init();•多处理器通信
// 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config(); // LCD SPI初始化 LCD_GPIO_Init(); // LCD IO控制引脚(例如背光) MX_SPI1_Init(); // LCD SPI控制引脚 // 图形界面初始化 GUI_Init(); // GUI界面初始化 GUI_Clear(); // 清屏 GUI_SetColor(GUI_Crimson); // 红色字体 GUI_DispStringAt("irBtnVal:",24,24); // 打印字符串"irBtnVal:"到位置X->24,Y->24 GUI_DispStringAt("irBtnCnt:",24,72); // 打印字符串"irBtnCnt:"到位置X->24,Y->96 GUI_DispStringAt("irBtnInfo:",24,120); // 打印字符串"irBtnCnt:"到位置X->24,Y->120如果地址不匹配,USART进入静音模式。
// 初始化TIM3•从静音模式唤醒(通过空闲线路检测或地址标记检测) MX_TIM3_Init(); ''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ while(1) { irkey span><span class= IRBNT_POLL(); "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 轮训获取IR按键信息 if(irkey.irBtnVal) span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 如果按键信息存在 { GUI_DispHexAt(irkey.irBtnVal,144,24,2); span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 打印按键值 GUI_DispHexAt(irkey.irBtnCnt,144,72,2); span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 打印按键计数span>'' === 硬件设计 === // 打印按键图标或名称选择STM32L4引脚PA9和PA10作为串口,当我们将拨码开关SW1拨到USB一端时,此串口通过CH340芯片转成USB接口,用于向电脑上打印一些调试信息。 GUI_GotoXY(144,120); // 指的光标位置[[文件:NBDK-SCH-UART-USB.png|边框|居中|无框|707x707像素]] GUI_ClearArea(); ''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 清除指定位置数据 switch(irkey.irBtnVal) span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 判断按键值,打印相应图标或名称 { case REMOTE_BTN_SWITCH: GUI_DispString(span><span class="tlid-translation-gender-indicator translation-gender-indicator"'switch'></span><span class="); break; case REMOTE_BTN_MENU: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'menu'></span><span class="); break; case REMOTE_BTN_MUTE: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'mute'></span><span class="); break; case REMOTE_BTN_MODE: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'mode'></span><span class="); break; case REMOTE_BTN_PLUS: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'+'></span><span class="); break; case REMOTE_BTN_RETURN: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'return'></span><span class="); break; case REMOTE_BTN_REWIND: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'|></span><'span class="); break; case REMOTE_BTN_PAUSE: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'>||'</span><span class="); break; case REMOTE_BTN_FASTFORWARD: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'></span>|'<span class="tlid-translation-gender-indicator translation-gender-indicator"); break; case REMOTE_BTN_0: GUI_DispString(></span><span class="'0'tlid-translation-gender-indicator translation-gender-indicator"); break; case REMOTE_BTN_LESS: GUI_DispString(></span><span class="'tlid-translation-gender-indicator translation-gender-'indicator"></span><span class="); break; case REMOTE_BTN_OK: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'OK'></span><span class="); break; case REMOTE_BTN_1: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'1'></span><span class="); break; case REMOTE_BTN_2: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'2'></span><span class="); break; case REMOTE_BTN_3: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'3'></span><span class="); break; case REMOTE_BTN_4: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'4'></span><span class="); break; case REMOTE_BTN_5: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'5'></span><span class="); break; case REMOTE_BTN_6: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'6'></span><span class="); break; case REMOTE_BTN_7: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'7'></span><span class="); break; case REMOTE_BTN_8: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"'8'></span><span class="); break; case REMOTE_BTN_9: GUI_DispString(tlid-translation-gender-indicator translation-gender-indicator"></span>'9'"); break; }=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到USB端,SW2拨到MCU。 }# 使用Keil打开基础实验 10-串口打印实验工程。 }# 使用Xshell打开miniUSB虚拟出的COM口}# 下载程序,并完成功能测试。</syntaxhighlight>==== gyu_util.c =实验验证 ===时钟初始化函数,用于配置我们模块运行的系统时钟、AHB高性能总线时钟、APB外设总线时钟以及单个外设的时钟。下载完成后,我们打开miniUSB虚拟出的COM口,可以看到串口周期性的打印计数值。[[文件:NBDK-XSHELL-UARTPRINTF.png|边框|居中|无框|759x759像素]]
主要包含了三个部分的初始化配置。=== 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
1==== stm32l4xx_hal_conf.内部或者外部振荡器选择,也就是选择时钟信号的来源,是内部振荡,还是外部晶振。h ====此文件位于“10-串口打印实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。
2此例程我们主要给大家展示STM32L4的串口功能,所以我们宏定义中打开UART相关的。<syntaxhighlight lang="c" line="1" start="103">// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC #define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_DMA_MODULE_ENABLED // DMA#define HAL_UART_MODULE_ENABLED // UART</syntaxhighlight> ==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.时钟配置,选择系统、AHB总线及APB总线的时钟来源。c部分有详细说明。 3.外设时钟配置,选择外设时钟来源。接下来我们初始化串口UASRT1。
为了给大家比较全面的展示各个时钟,我们振荡器选择HSI(内部16MHz高频)、HSE(外部8MHz高频)以及LSE(外部32.768KHz低频)三个。选择HSE作为PLL(锁相回路)时钟源,配置PLLCLK为80MHz。配置系统时钟SYSCLK、AHB高性能总线、APB外设总线(APB1及APB2)为80MHz。另外我们还分别配置了ADC、UART以及I2C的外设时钟。在while()循环中,我们每隔100ms通过格式化输出"TimeCount = xx:xx:xx"。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。<syntaxhighlight lang="c++" line="1" start="4934">void SystemClock_Configint main(void)
{
RCC_OscInitTypeDef RCC_OscInitStructuint32_t hour = 0; // 定义RCC内部/外部振荡器结构体 RCC_ClkInitTypeDef RCC_ClkInitStructuint32_t minute = 0; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 // 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW)uint32_t second = 0;
// 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct* Reset of all peripherals, Initializes the Flash interface and the Systick.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE; /*/ 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE重置所有外设、flash界面以及系统时钟 RCC_OscInitStruct.HSEState = RCC_HSE_ONHAL_Init(); // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动)配置系统时钟(包含振荡器、系统时钟、总线时钟等等) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ONSystemClock_Config(); // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL初始化串口USART1 RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = 1MX_USART1_UART_Init(); // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz // RCC时钟配置,出错则进入错误处理函数 if while (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK1)
{
_Error_Handler// 模拟时钟计时,这边的1s实际只是100ms HAL_Delay(__FILE__, __LINE__100); } // 初始化CPU,AHB和APB总线时钟100ms延时 RCC_ClkInitStruct.ClockType printf("TimeCount = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2%02d:%02d:%02d\r\n",hour,minute,second); // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2格式化输出"TimeCount = xx:xx:xx" RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz时分秒计数 RCC_ClkInitStruct.AHBCLKDivider second++; if(second = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz60) RCC_ClkInitStruct.APB1CLKDivider { second = RCC_HCLK_DIV10; // APB1时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1 minute++; // APB2时钟为系统时钟1分频,80/1 = 80MHz // RCC时钟配置,出错则进入错误处理函数 } if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) !minute == HAL_OK60) // HCLK { minute =80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4)0; hour++; { } _Error_Handlerif(__FILE__, __LINE__hour == 24) { hour = 0; }
}
}
</syntaxhighlight>
==== gyu_util.c ====
请参照实验01中的介绍。
 
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
==== gyu_usartc ====
串口初始化函数,配置串口协议:波特率115200,数据位8位,停止位1位,无校验位,无流控制。
// 初始化外设时钟 PeriphClkInit.PeriphClockSelection <syntaxhighlight lang="c++" line="1" start= RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2"37"> |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1void MX_USART1_UART_Init(void) |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC{ PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz配置串口参数 PeriphClkInithuart1.Usart2ClockSelection Instance = RCC_USART2CLKSOURCE_PCLK1USART1; // 配置串口USART2时钟为PCLK1,80MHzUART寄存器基础地址,定义为USART1的 PeriphClkInithuart1.Lpuart1ClockSelection Init.BaudRate = RCC_LPUART1CLKSOURCE_HSI115200; // 配置LPUART时钟为HSI,16MHz串口波特率为115200 PeriphClkInithuart1.I2c2ClockSelection Init.WordLength = RCC_I2C2CLKSOURCE_PCLK1UART_WORDLENGTH_8B; // 配置I2C2时钟为PCLK1,80MHz串口数据位为8位 PeriphClkInithuart1.Init.Lptim1ClockSelection StopBits = RCC_LPTIM1CLKSOURCE_LSEUART_STOPBITS_1; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义串口停止位为1位 PeriphClkInithuart1.PLLSAI1Init.PLLSAI1Source Parity = RCC_PLLSOURCE_HSEUART_PARITY_NONE; // 配置PLLSAI1时钟为HSE,8MHz串口无校验位 PeriphClkInithuart1.PLLSAI1Init.PLLSAI1M Mode = 1UART_MODE_TX_RX; // 配置PLLSAI1分频为1串口模式,TX和RX作用 PeriphClkInithuart1.PLLSAI1Init.PLLSAI1N HwFlowCtl = 8UART_HWCONTROL_NONE; // 配置PLLSAI1倍增为8串口无流控制 PeriphClkInithuart1.PLLSAI1Init.PLLSAI1P OverSampling = RCC_PLLP_DIV7UART_OVERSAMPLING_16; // SAI时钟7分频,64/7 = 9.142857MHz16位过采样 PeriphClkInithuart1.PLLSAI1Init.PLLSAI1Q OneBitSampling = RCC_PLLQ_DIV2UART_ONE_BIT_SAMPLE_DISABLE; / / SDMMC、RNG、USB时钟2分频,64/2 = 32MHz1位过采样禁能 PeriphClkInithuart1.PLLSAI1AdvancedInit.PLLSAI1R AdvFeatureInit = RCC_PLLR_DIV2UART_ADVFEATURE_NO_INIT; // 系统主时钟分区2分频,64/2 = 32MHz 没有串口高级功能初始化 PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数串口初始化 if (HAL_RCCEx_PeriphCLKConfigHAL_UART_Init(&PeriphClkInithuart1) != HAL_OK)
{
_Error_Handler(__FILE__, __LINE__); // 如果初始化失败,进入错误处理任务
}
}</syntaxhighlight>配置串口硬件,使能GPIOA以及USART1的时钟,配置PA9和PA10为串口的TX及RX引脚。<syntaxhighlight lang="c++" line="1" start="67">void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle){ // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz定义GPIO结构体 GPIO_InitTypeDef GPIO_InitStruct; // 判断选择的是否为USART1 if (HAL_PWREx_ControlVoltageScalinguartHandle->Instance==USART1) { // 使能GPIOA引脚时钟(因为选择的TX和RX分别为PA9和PA10) __HAL_RCC_GPIOA_CLK_ENABLE(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK; // 使能USART1时钟 __HAL_RCC_USART1_CLK_ENABLE(); { _Error_Handler// GPIO配置 GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10; // 选择USART1的TX和RX引脚(TX:PA9,RX:PA10) GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 推挽输出 GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;// 引脚频率5-80MHz GPIO_InitStruct.Alternate = GPIO_AF7_USART1; // 配置为USART1 HAL_GPIO_Init(__FILE__GPIOA, __LINE__&GPIO_InitStruct); // 初始化引脚
}
}
</syntaxhighlight>配置fputc()函数,用于格式化打印,当我们进行了如下代码配置,就可以调用printf()函数去格式化打印调试信息。<syntaxhighlight lang="c++" line="1" start="99">
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
PUTCHAR_PROTOTYPE{ // 配置系统定时器中断时间,配置为HCLK的千分频配置格式化输出到串口USART1 HAL_SYSTICK_ConfigHAL_UART_Transmit(HAL_RCC_GetHCLKFreq&huart1, (uint8_t *)/1000&ch, 1, 0xFFFF); return ch;}
<// 配置系统定时器,配置为HCLKsyntaxhighlight> == 实验11-串口中断 ==  HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);== 实验12-串口DMA ==
// 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight>== 实验13-TFT显示屏 ==
==实验14-二维码显示 == gyu_irc ====初始化定时器TIM3,首先配置TIM3的时钟为1MHz(也就是1us),我们设置它向上自动装载,并且设置自动装载值为10000,通过计算可以知道装满一次需要10ms。
然后我们需要设置输入捕获的参数,我们配置上升沿下降沿都捕获,并且设置8个时钟周期的滤波(防止误识别)。== 实验15-RNG随机发生器 ==RNG实验给大家展示一下随机数的生成,在这个实验中,除了main.c以及gyu_rng.c两个关键文件外,我们还需要注意一下gyu_util.c中系统时钟配置,具体的原因,请大家查看下面的工程介绍。
最后使能TIM3的中断,并且开始捕获TIM3的通道1(也就是PC6引脚)。=== STM32L476 随机发生器简介 ===随机发生器定义:
RNG是一个真正的随机数发生器,它基于模拟噪声源连续提供32位熵样本。它可以被应用程序用作活动熵源,以构建符合NIST的确定性随机比特生成器(DRBG)。 RNG真随机数发生器已根据德国AIS-31标准进行了验证。{{Note|text=熵:热力学中表征物质状态的参量之一,用符号S表示,其物理意义是体系混乱程度的度量。|type=info}}真随机数发生器生成条件:* RNG时钟rng_clk = 48 MHz* AHB时钟rng_hclk = 60 MHz{{Note|text=由于真随机数发生器生成条件的要求,本次实验的gyu_util.c文件中有关系统时钟的配置,和之前的实验有所不同,请大家仔细查看一下该文件。|type=warning}}[[文件:NBDK-DS-RNG.png|边框|居中|无框|806x806像素]]''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><syntaxhighlight langspan class="c++tlid-translation-gender-indicator translation-gender-indicator" line></span><span class="1tlid-translation-gender-indicator translation-gender-indicator" start></span><span class="59tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''void MX_TIM3_Init(void)=== 硬件设计 ===使用STM32L476内部RNG随机数发生器。{ htim3.Instance ''<span class= TIM3; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 通用定时器3 htim3.Init.Prescaler span><span class= 80"tlid-translation-gender-indicator translation-gender-1; indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ TIM3 80预分频器(APB2总线),80MHz span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 80 span><span class= 1MHz(1us) htim3.Init.CounterMode "tlid-translation-gender-indicator translation-gender-indicator"></span><span class= TIM_COUNTERMODE_UP; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 向上计数器 htim3.Init.Period span><span class= 10000; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 自动装载值设为10000,装满一次 10000 * 1us span><span class= 10ms htim3.Init.ClockDivision "tlid-translation-gender-indicator translation-gender-indicator"></span><span class= TIM_CLOCKDIVISION_DIV1;"tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 不分频 htim3.Init.AutoReloadPreload span><span class= TIM_AUTORELOAD_PRELOAD_ENABLE; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 自动加载使能 if (HAL_TIM_IC_Init(&htim3) !span><span class= HAL_OK) "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 初始化TIM3,出错则进入错误处理函数 { _Error_Handler(__FILE__, __LINE__); } span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 初始化TIM3输入捕获参数 TIM_IC_InitTypeDef sConfigIC; span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 定义输入捕获结构体(IC:Input capture) sConfigIC.ICPolarity span><span class= TIM_ICPOLARITY_BOTHEDGE; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 上升沿下降沿都捕获 sConfigIC.ICSelection span><span class= TIM_ICSELECTION_DIRECTTI; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 配置为TI1 sConfigIC.ICPrescaler span><span class= TIM_ICPSC_DIV1; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 不分频 sConfigIC.ICFilter span><span class= 0x03; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ IC1Fspan><span class=0011 8个定时器时钟周期滤波 "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 初始化TIM3 CH1通道,出错则进入错误处理函数 if (HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1) !span><span class= HAL_OK) { _Error_Handler(__FILE__, __LINE__); } HAL_TIM_Base_Start_IT(&htim3); "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 使能更新中断(也就是TIM_IT_UPDATE) HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1); span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 开始捕获TIM3 CH1}span><span class="tlid-translation-gender-indicator translation-gender-indicator"></syntaxhighlightspan>使能GPIOC以及TIM3的时钟,并且配置PC6为TIM3的通道1。<syntaxhighlight langspan class="c++tlid-translation-gender-indicator translation-gender-indicator" line></span><span class="1tlid-translation-gender-indicator translation-gender-indicator" start></span><span class="96tlid-translation-gender-indicator translation-gender-indicator">void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim){ GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_TIM3_CLK_ENABLE(); </span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 使能TIM3时钟 __HAL_RCC_GPIOC_CLK_ENABLE(); span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 开启GPIOC时钟 GPIO_Initure.Pin span><span class= GPIO_PIN_6; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ PC6 GPIO_Initure.Mode span><span class= GPIO_MODE_AF_PP; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 推挽输出 GPIO_Initure.Pull span><span class= GPIO_PULLUP; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 上拉 GPIO_Initure.Speed span><span class= GPIO_SPEED_HIGH; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ 高速模式 GPIO_Initure.Alternate span><span class= GPIO_AF2_TIM3; "tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></ PC6配置为TIM3通道1span>''=== 实验准备 === HAL_GPIO_Init(GPIOC, &GPIO_Initure);# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。 # 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到USB端,SW2拨到MCU。# 使用Keil打开基础实验 15-RNG随机发生器实验工程。# 使用Xshell打开miniUSB虚拟出的COM口。# 下载程序,并完成功能测试。 HAL_NVIC_SetPriority(TIM3_IRQn, 10, 0); // 设置TIM3中断优先级 HAL_NVIC_EnableIRQ(TIM3_IRQn); // 使能TIM3中断=== 实验验证 ===}下载完成后,我们打开miniUSB虚拟出的COM口,每按下一次S1(btn_up),STM32L476都会向串口打印一个随机数。</syntaxhighlight>定时器周期中断回调函数,TIM3的自动装载值装满一次,进入一次此回调(本工程配置的参数是10ms进入一次)。[[文件:NBDK-XSHELL-RNG.png|边框|居中|无框|759x759像素]]
我们判断是否已经接收到引导码(根据引导码标志位判断),一旦接收到引导码,我们认为已经开始了一次NEC数据的接收。=== 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
如果是接收到引导码之后,第一次进入此函数,那么我们使能记录遥控器按键值的标志位,也就是代表接收到了一次NEC数据(不管数据对错)。==== stm32l4xx_hal_conf.h ====此文件位于“15-RNG随机发生器实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。
如果进入的次数少于11次,则继续增加计数,当计数值等于11时(也就是从接收到引导码已经过去至少110ms时),我们认为一次NEC的数据获取已经完成,此时清除周期回调的计数值,并且删除引导码标志位(下次再进到这个函数时,只有新的引导码数据到来,才会进行新的数据处理)。此例程我们主要给大家展示STM32L4的随机发生器生成随机数的功能,所以我们宏定义中打开RNG相关的。<syntaxhighlight lang="c" line="1" start="103">#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC #define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_DMA_MODULE_ENABLED // DMA#define HAL_UART_MODULE_ENABLED // UART#define HAL_SPI_MODULE_ENABLED // SPI#define HAL_RNG_MODULE_ENABLED // RNG随机发生器</syntaxhighlight> ==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 接下来我们初始化串口UASRT1、按键、以及TFT显示屏。 接下来是我们这个工程的关键,初始化RNG部分。 在while()循环中,我们调用KEY_Poll()函数去轮询是否有按键被按下。 <syntaxhighlight lang="c++" line="1" start="12241">int main(void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3) { if(irStatus & IR_STATUS_BootCode) /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ 如果接收到引导码 { irStatus &= ~IR_STATUS_Rising; // 删除上升沿标记(以防止本次出错,确保下次采集流程正确)重置所有外设、flash界面以及系统时钟 if HAL_Init(tim3Cnt == 0) // 如果是第一次进入(计数为0) { irStatus |= IR_STATUS_BtnInfo; // 记录已经获取到IR按键信号(遥控器按键值) } if((tim3Cnt & 0X0F) < 11) // 进入回调少于11次 { tim3Cnt++; // 计数值自加 } else // 超过11次,代表一次采集超时(不论是否成功) { irStatus &= ~IR_STATUS_BootCode;// 删除引导码标记 tim3Cnt = 0; // 清除计数值 } } }}</syntaxhighlight>处理TIM3 CH1(PC6引脚)捕获的数据,这边要记得初始化中我们使能了上升沿下降沿都捕获数据。
当有边沿捕获到来,我们判断此时PC6引脚的电平。 // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config(); // 初始化USART1 MX_USART1_UART_Init(); // 初始化按键引脚 MX_KEY_Init(); // 注册按钮回调函数 KEY_RegisterCb(AppKey_cb); // LCD SPI初始化 LCD_GPIO_Init(); // LCD IO控制引脚(例如背光) MX_SPI1_Init(); // LCD SPI控制引脚 // 图形界面初始化如果此时是高电平,则代表刚刚的是上升沿,此时我们使能上升沿标志位,并且清空定时器计数值。 GUI_Init(); // GUI界面初始化 GUI_Clear(); // 清屏如果此时是低电平,且上升沿标识位被置位,则我们根据获取到的计数值(也就是上一次高电平的持续时间)来判断本段PWM波代表的含义。 // 打印logo到位置X->0,Y->0 GUI_DrawBitmap(&bmLogo,0,0); // 随机发生器初始化 MX_RNG_Init(); // while(1) { KEY_Poll(); // 按键轮训,监测是否有按键被按下 }}</syntaxhighlight>在按键回调函数中,可以看到,每次S1(key_up)按键被按下,都是调用RNG_Get()函数去获取一次随机数。<syntaxhighlight lang="c++" line="1" start="15589">void HAL_TIM_IC_CaptureCallbackAppKey_cb(TIM_HandleTypeDef *htimuint8_t key)
{
// 判断是否为TIM3 CH1捕获产生的回调如果有相应按键被按下,则串口打印调试信息 if((htim->Instance == TIM3) key && (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)KEY_UP)
{
ifRNG_Get(HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_6)) // 获取PC6引脚电平,如果是高电平,则代表是上升沿捕获 { __HAL_TIM_SET_COUNTER(&htim3, 0); // 清除TIM3定时器计数值获取随机数 irStatus |= IR_STATUS_Rising; // 标记上升沿捕获 } } else <//如果是低电平,则代表是下降沿触发syntaxhighlight> { tim3Val = HAL_TIM_ReadCapturedValue(&htim3, TIM_CHANNEL_1); // 读取TIM3 CH1定时器计数值 if(irStatus & IR_STATUS_Rising) // 存在上升沿标记,我们比较定时器计数值 { if(tim3Val > 260 && tim3Val < 860) // 高电平持续560us代表bit 0,范围560us±300us { irRecData <<= 1; // 左移一位 irRecData |= 0; // bit位赋值0 } else if(tim3Val > 1380 && tim3Val < 1980) // 高电平持续1680us代表bit 1,范围1680us±300us { irRecData <<= 1; // 左移一位 irRecData |gyu_util.c = 1; // bit位赋值1 } else if(tim3Val > 2200 && tim3Val < 2800) // 高电平持续2500us代表本次按键结束,范围2500us±300us { irCnt++; // 按键次数新增1 tim3Cnt = 0; // 清除计数值 } else if(tim3Val > 4200 && tim3Val < 4800) // 高电平持续4500us代表新的按键,范围4500us±300us { irStatus |= IR_STATUS_BootCode; // 标记引导码 irCnt = 0; // 有新的按键到来,清除按键计数 }此实验因为真随机数发生器的条件限制,我们配置的时钟和之前的例程有稍许不同。 } 首先是我们的AHB时钟,此实验配置为60MHz,之前的实验都是80MHz。 irStatus &= ~IR_STATUS_Rising; // 清除上升沿标记 } }}</syntaxhighlight>轮询当前的按键信息。如果按键值标识存在,则代表有按键被按下,接着判断地址码以及数据数据正确,并将最终的按键数据添加到irinfo中留给应用层调用。其次是此实验我们使用到了MSI(并且配置为RCC_MSIRANGE_11,也就是48MHz),在下面的外设功能配置中,我们选择此48MHz时钟作为RNG的时钟。<syntaxhighlight lang="c++" line="1" start="20649">irInfo_t IRBNT_POLLvoid SystemClock_Config(void){ irInfo_t irinfo = {0,0}RCC_OscInitTypeDef RCC_OscInitStruct; //定义RCC内部/ 定义按键信息结构体外部振荡器结构体 uint8_t bcode, dcodeRCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义引导码正反编码定义RCC系统,AHB和APB总线时钟配置结构体 uint8_t bvalue, dvalueRCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义按键值正反编码定义RCC扩展时钟结构体
if// 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(irStatus & IR_STATUS_BtnInfoRCC_LSEDRIVE_LOW) ;  // 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE|RCC_OSCILLATORTYPE_MSI; // 设置需要配置的振荡器为HSI、HSE、LSE、MSI // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 如果获取到按键值激活LSE时钟(32.768KHz,低驱动) {// 配置HSI bcode RCC_OscInitStruct.HSIState = irRecData >> 24RCC_HSI_ON; // 地址码激活HSI时钟 dcode RCC_OscInitStruct.HSICalibrationValue = (irRecData >> 16) & 0xff; // 配置HSI为16MHz // 配置MSI RCC_OscInitStruct.MSIState = RCC_MSI_ON; // 激活MSI时钟(内部高频,最高可配置48MHz) RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; // 配置为48MHz // 地址码反编码配置PLL RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL if((bcode RCC_OscInitStruct.PLL.PLLSource =RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = (uint8_t)~dcode) && bcode 1; // 配置PLL VCO输入分频为1,8/1 =8MHz RCC_OscInitStruct.PLL.PLLN = REMOTE_DEVICE_ID) 15; // 判断地址码是否正确配置PLL VCO输入倍增为20,8MHz*15 = 120MHz { RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,120/7 = 17.14MHz bvalue RCC_OscInitStruct.PLL.PLLQ = irRecData >> 8RCC_PLLQ_DIV2; //SDMMC、RNG、USB时钟2分频,120/ 按键值2 = 60MHz dvalue RCC_OscInitStruct.PLL.PLLR = irRecDataRCC_PLLR_DIV2; //系统主时钟分区2分频,120/ 按键值反编码2 = 60MHz // RCC时钟配置,出错则进入错误处理函数 if(bvalue =HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { _Error_Handler(uint8_t__FILE__, __LINE__)~dvalue) ; } // 判断按键值是否正确初始化CPU,AHB和APB总线时钟 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK { |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 irinfo RCC_ClkInitStruct.irBtnVal SYSCLKSource = bvalueRCC_SYSCLKSOURCE_PLLCLK; // 将按键值赋给irinfo配置系统时钟为PLLCLK输入,60MHz RCC_ClkInitStruct.irBtnValAHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,60/1 = 60MHz } RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,60/1 = 60MHz } RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,60/1 = 60MHz // RCC时钟配置,出错则进入错误处理函数 irinfo.irBtnCnt if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != irCnt; HAL_OK) // 将按键此处赋给irinfoHCLK=60MHz,Vcore=3.irBtnCnt3V,所以选择SW2(FLASH_LATENCY_2) { _Error_Handler(__FILE__, __LINE__); }
return irinfo// 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC |RCC_PERIPHCLK_RNG;// 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC、RNG PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,60MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,60MHz PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,60MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 返回按键信息配置ADC时钟为PLLSAI1,现在为60MHz,下面会重新定义} PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1< PeriphClkInit.PLLSAI1.PLLSAI1N = 8; /syntaxhighlight>/ 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q =RCC_PLLQ_DIV2; // SDMMC、USB时钟2分频,64/2 = 实验10-串口打印 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R =RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 =32MHz 串口打印实验,给大家展示的是如何配置STM32L476一个有效的硬件串口功能,并且顺带给大家介绍了如何去配置一个格式化打印函数printf PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz PeriphClkInit.RngClockSelection = RCC_RNGCLKSOURCE_MSI; // 外设时钟配置,出错则进入错误处理函数 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }
// 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) !=== STM32L476 UART简介 ===HAL_OK) { _Error_Handler(__FILE__, __LINE__);USART主要功能: }
•全双工异步通信 // 配置系统定时器中断时间,配置为HCLK的千分频 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
•NRZ标准格式(标记 /空格)/ 配置系统定时器,配置为HCLK HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
•可配置的过采样方法16或8,以提供速度和速度之间的灵活性 // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight>
时钟容差==== gyu_rng.c ====初始化RNG。
•通用可编程发送和接收波特率高达10 Mbit <syntaxhighlight lang="c++" line="1" start="43">void MX_RNG_Init(void){ hrng.Instance = RNG; if (HAL_RNG_Init(&hrng) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }}</syntaxhighlight>使能RNG时钟,并且配置和使能RNG中断。<syntaxhighlight lang="c++" line="1" start="60">void HAL_RNG_MspInit(RNG_HandleTypeDef* hrng){ if(hrng->Instance==RNG) { // 使能RNG时钟 __HAL_RCC_RNG_CLK_ENABLE(); // 使能NVIC中断及优先级 HAL_NVIC_SetPriority(RNG_IRQn, 10, 0); HAL_NVIC_EnableIRQ(RNG_IRQn); }}</ s时syntaxhighlight>随机数的获取函数,在此处打开中断。此处中断开启,会触发HAL_RNG_ErrorCallback()或者HAL_RNG_ReadyDataCallback()回调函数,当有随机数成功生成时,返回HAL_RNG_ReadyDataCallback()回调。<syntaxhighlight lang="c++" line="1" start="81">void RNG_Get(void)时钟频率为80 MHz,过采样为8{ HAL_RNG_GenerateRandomNumber_IT(&hrng); // 随机数获取函数(开启中断)•双时钟域允许:}</syntaxhighlight>真随机数成功生成的回调函数,参数random32bit就是生成的随机数,我们利用printf函数将随机数打印到串口。<syntaxhighlight lang="c++" line="1" start="108">- USART功能和从停止模式唤醒void HAL_RNG_ReadyDataCallback(RNG_HandleTypeDef* hrng, uint32_t random32bit){- 独立于PCLK重新编程的便捷波特率编程 printf("%u\r\n",random32bit); // 将随机数打印到串口}•自动波特率检测</syntaxhighlight>
•可编程数据字长(7,8或9位)== 实验16-RTC实时时钟 ===== STM32L476 RTC时钟简介 ===
•可编程数据顺序,具有MSB优先或LSB优先移位''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 硬件设计 ===选择STM32L4
•可配置的停止位(1或2个停止位)''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到USB端,SW2拨到MCU。# 使用Keil打开基础实验 10-串口打印实验工程。# 使用Xshell打开miniUSB虚拟出的COM口# 下载程序,并完成功能测试。
•同步模式和时钟输出,用于同步通信=== 实验验证 ===下载完成后,可以看到TFT屏幕上打印当前的实时时间、日期以及星期,且10s后会触发闹钟(表现为TFT打印"Alarm"、蜂鸣器哔一声)。
•单线半双工通信=== 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。
•使用DMA进行持续通信==== stm32l4xx_hal_conf.h ====此文件位于“10-串口打印实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。
•使用集中式DMA将接收此例程我们主要给大家展示STM32L4的实时时钟功能,所以我们宏定义中打开RTC相关的。<syntaxhighlight lang="c" line="1" start="103">/发送的字节缓冲在保留的SRAM中/ 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC
•发送器和接收器的独立使能位#define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_DMA_MODULE_ENABLED // DMA#define HAL_UART_MODULE_ENABLED // UART#define HAL_SPI_MODULE_ENABLED // SPI#define HAL_RTC_MODULE_ENABLED // RTC实时时钟</syntaxhighlight>
•独立的信号极性控制,用于发送和接收==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。
•可交换Tx / Rx引脚配置 •调制解调器和RS-485收发器的硬件流控制 •通信控制/错误检测标志 •奇偶校验控制: - 传输奇偶校验位 - 检查接收数据字节的奇偶校验 •带有标志的14个中断源 •多处理器通信 如果地址不匹配,USART进入静音模式。 •从静音模式唤醒(通过空闲线路检测或地址标记检测)''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 硬件设计 ===选择STM32L4引脚PA9和PA10作为串口,当我们将拨码开关SW1拨到USB一端时,此串口通过CH340芯片转成USB接口,用于向电脑上打印一些调试信息。[[文件:NBDK-SCH-UART-USB.png|边框|居中|无框|707x707像素]]''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到USB端,SW2拨到MCU。# 使用Keil打开基础实验 10-串口打印实验工程。# 使用Xshell打开miniUSB虚拟出的COM口# 下载程序,并完成功能测试。 === 实验验证 ===下载完成后,我们打开miniUSB虚拟出的COM口,可以看到串口周期性的打印计数值。[[文件:NBDK-XSHELL-UARTPRINTF.png|边框|居中|无框|759x759像素]] === 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==== stm32l4xx_hal_conf.h ====此文件位于“10-串口打印实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。 此例程我们主要给大家展示STM32L4的串口功能,所以我们宏定义中打开UART相关的。<syntaxhighlight lang="c" line="1" start="103">// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC #define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_DMA_MODULE_ENABLED // DMA#define HAL_UART_MODULE_ENABLED // UART</syntaxhighlight> ==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 接下来我们初始化串口UASRT1。 在while()循环中,我们每隔100ms通过格式化输出"TimeCount = xx:xx:xx"。 <syntaxhighlight lang="c++" line="1" start="34">int main(void){ uint32_t hour = 0; uint32_t minute = 0; uint32_t second = 0;  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟 HAL_Init();  // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config(); // 初始化串口USART1 MX_USART1_UART_Init(); // while (1) { // 模拟时钟计时,这边的1s实际只是100ms HAL_Delay(100); // 100ms延时 printf("TimeCount = %02d:%02d:%02d\r\n",hour,minute,second); // 格式化输出"TimeCount = xx:xx:xx"  // 时分秒计数 second++; if(second == 60) { second = 0; minute++; } if(minute == 60) { minute = 0; hour++; } if(hour == 24) { hour = 0; } }}</syntaxhighlight>==== 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的外设时钟。 基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。<syntaxhighlight lang="c++" line="1" start="49">void SystemClock_Config(void){ RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 // 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);  // 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE; // 设置需要配置的振荡器为HSI、HSE、LSE // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置PLL RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 20; // 配置PLL VCO输入倍增为20,8MHz*20 = 160MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,160/7 = 22.857143MHz RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,160/2 = 80MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,160/2 = 80MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); } // 初始化CPU,AHB和APB总线时钟 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,80MHz RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,80/1 = 80MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,80/1 = 80MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK) // HCLK=80MHz,Vcore=3.3V,所以选择SW4(FLASH_LATENCY_4) { _Error_Handler(__FILE__, __LINE__); }  // 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC; // 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,80MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,80MHz PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,80MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为80MHz,下面会重新定义 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz // 外设时钟配置,出错则进入错误处理函数 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }  // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }  // 配置系统定时器中断时间,配置为HCLK的千分频 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);  // 配置系统定时器,配置为HCLK HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);  // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight> ==== gyu_usartc ====串口初始化函数,配置串口协议:波特率115200,数据位8位,停止位1位,无校验位,无流控制。 <syntaxhighlight lang="c++" line="1" start="37">void MX_USART1_UART_Init(void){ // 配置串口参数 huart1.Instance = USART1; // UART寄存器基础地址,定义为USART1的 huart1.Init.BaudRate = 115200; // 串口波特率为115200 huart1.Init.WordLength = UART_WORDLENGTH_8B; // 串口数据位为8位 huart1.Init.StopBits = UART_STOPBITS_1; // 串口停止位为1位 huart1.Init.Parity = UART_PARITY_NONE; // 串口无校验位 huart1.Init.Mode = UART_MODE_TX_RX; // 串口模式,TX和RX作用 huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; // 串口无流控制 huart1.Init.OverSampling = UART_OVERSAMPLING_16; // 16位过采样 huart1.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE; // 1位过采样禁能 huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT; // 没有串口高级功能初始化 // 串口初始化 if (HAL_UART_Init(&huart1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); // 如果初始化失败,进入错误处理任务 } }</syntaxhighlight>配置串口硬件,使能GPIOA以及USART1的时钟,配置PA9和PA10为串口的TX及RX引脚。<syntaxhighlight lang="c++" line="1" start="67">void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle){ // 定义GPIO结构体 GPIO_InitTypeDef GPIO_InitStruct; // 判断选择的是否为USART1 if(uartHandle->Instance==USART1) { // 使能GPIOA引脚时钟(因为选择的TX和RX分别为PA9和PA10) __HAL_RCC_GPIOA_CLK_ENABLE(); // 使能USART1时钟 __HAL_RCC_USART1_CLK_ENABLE(); // GPIO配置 GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10; // 选择USART1的TX和RX引脚(TX:PA9,RX:PA10) GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 推挽输出 GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;// 引脚频率5-80MHz GPIO_InitStruct.Alternate = GPIO_AF7_USART1; // 配置为USART1 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 初始化引脚 }}</syntaxhighlight>配置fputc()函数,用于格式化打印,当我们进行了如下代码配置,就可以调用printf()函数去格式化打印调试信息。<syntaxhighlight lang="c++" line="1" start="99">#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f) PUTCHAR_PROTOTYPE{ // 配置格式化输出到串口USART1 HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch;} </syntaxhighlight> == 实验11-串口中断 == == 实验12-串口DMA == == 实验13-TFT显示屏 == == 实验14-二维码显示 == == 实验15-RNG随机发生器 ==RNG实验给大家展示一下随机数的生成,在这个实验中,除了main.c以及gyu_rng.c两个关键文件外,我们还需要注意一下gyu_util.c中系统时钟配置,具体的原因,请大家查看下面的工程介绍。 === STM32L476 随机发生器简介 ===随机发生器定义: RNG是一个真正的随机数发生器,它基于模拟噪声源连续提供32位熵样本。它可以被应用程序用作活动熵源,以构建符合NIST的确定性随机比特生成器(DRBG)。 RNG真随机数发生器已根据德国AIS-31标准进行了验证。{{Note|text=熵:热力学中表征物质状态的参量之一,用符号S表示,其物理意义是体系混乱程度的度量。|type=info}}真随机数发生器生成条件:* RNG时钟rng_clk = 48 MHz* AHB时钟rng_hclk = 60 MHz{{Note|text=由于真随机数发生器生成条件的要求,本次实验的gyu_util.c文件中有关系统时钟的配置,和之前的实验有所不同,请大家仔细查看一下该文件。|type=warning}}[[文件:NBDK-DS-RNG.png|边框|居中|无框|806x806像素]]''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 硬件设计 ===使用STM32L476内部RNG随机数发生器。 ''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到USB端,SW2拨到MCU。# 使用Keil打开基础实验 15-RNG随机发生器实验工程。# 使用Xshell打开miniUSB虚拟出的COM口。# 下载程序,并完成功能测试。 === 实验验证 ===下载完成后,我们打开miniUSB虚拟出的COM口,每按下一次S1(btn_up),STM32L476都会向串口打印一个随机数。[[文件:NBDK-XSHELL-RNG.png|边框|居中|无框|759x759像素]] === 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==== stm32l4xx_hal_conf.h ====此文件位于“15-RNG随机发生器实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。 此例程我们主要给大家展示STM32L4的随机发生器生成随机数的功能,所以我们宏定义中打开RNG相关的。<syntaxhighlight lang="c" line="1" start="103">#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC #define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_DMA_MODULE_ENABLED // DMA#define HAL_UART_MODULE_ENABLED // UART#define HAL_SPI_MODULE_ENABLED // SPI#define HAL_RNG_MODULE_ENABLED // RNG随机发生器</syntaxhighlight> ==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。 接下来我们初始化串口UASRT1、按键、以及TFT显示屏。 接下来是我们这个工程的关键,初始化RNG部分。 在while()循环中,我们调用KEY_Poll()函数去轮询是否有按键被按下。 <syntaxhighlight lang="c++" line="1" start="41">int main(void){ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ // 重置所有外设、flash界面以及系统时钟 HAL_Init();  // 配置系统时钟(包含振荡器、系统时钟、总线时钟等等) SystemClock_Config(); // 初始化USART1 MX_USART1_UART_Init(); // 初始化按键引脚 MX_KEY_Init(); // 注册按钮回调函数 KEY_RegisterCb(AppKey_cb); // LCD SPI初始化 LCD_GPIO_Init(); // LCD IO控制引脚(例如背光) MX_SPI1_Init(); // LCD SPI控制引脚 // 图形界面初始化 GUI_Init(); // GUI界面初始化 GUI_Clear(); // 清屏 // 打印logo到位置X->0,Y->0 GUI_DrawBitmap(&bmLogo,0,0); // 随机发生器初始化 MX_RNG_Init(); // while(1) { KEY_Poll(); // 按键轮训,监测是否有按键被按下 }}</syntaxhighlight>在按键回调函数中,可以看到,每次S1(key_up)按键被按下,都是调用RNG_Get()函数去获取一次随机数。<syntaxhighlight lang="c++" line="1" start="89">void AppKey_cb(uint8_t key){ // 如果有相应按键被按下,则串口打印调试信息 if(key & KEY_UP) { RNG_Get(); // 获取随机数 }}</syntaxhighlight> ==== gyu_util.c ====此实验因为真随机数发生器的条件限制,我们配置的时钟和之前的例程有稍许不同。 首先是我们的AHB时钟,此实验配置为60MHz,之前的实验都是80MHz。 其次是此实验我们使用到了MSI(并且配置为RCC_MSIRANGE_11,也就是48MHz),在下面的外设功能配置中,我们选择此48MHz时钟作为RNG的时钟。<syntaxhighlight lang="c++" line="1" start="49">void SystemClock_Config(void){ RCC_OscInitTypeDef RCC_OscInitStruct; // 定义RCC内部/外部振荡器结构体 RCC_ClkInitTypeDef RCC_ClkInitStruct; // 定义RCC系统,AHB和APB总线时钟配置结构体 RCC_PeriphCLKInitTypeDef PeriphClkInit; // 定义RCC扩展时钟结构体 // 配置LSE驱动器功能为低驱动能力 __HAL_RCC_LSEDRIVE_CONFIG(RCC_LSEDRIVE_LOW);  // 初始化CPU,AHB和APB总线时钟 RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI|RCC_OSCILLATORTYPE_HSE |RCC_OSCILLATORTYPE_LSE|RCC_OSCILLATORTYPE_MSI; // 设置需要配置的振荡器为HSI、HSE、LSE、MSI // 配置HSE RCC_OscInitStruct.HSEState = RCC_HSE_ON; // 激活HSE时钟(开发板外部为8MHz) // 配置LSE RCC_OscInitStruct.LSEState = RCC_LSE_ON; // 激活LSE时钟(32.768KHz,低驱动) // 配置HSI RCC_OscInitStruct.HSIState = RCC_HSI_ON; // 激活HSI时钟 RCC_OscInitStruct.HSICalibrationValue = 16; // 配置HSI为16MHz // 配置MSI RCC_OscInitStruct.MSIState = RCC_MSI_ON; // 激活MSI时钟(内部高频,最高可配置48MHz) RCC_OscInitStruct.MSIClockRange = RCC_MSIRANGE_11; // 配置为48MHz // 配置PLL RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; // 打开PLL RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; // 选择HSE时钟作为PLL入口时钟源,8MHz RCC_OscInitStruct.PLL.PLLM = 1; // 配置PLL VCO输入分频为1,8/1 = 8MHz RCC_OscInitStruct.PLL.PLLN = 15; // 配置PLL VCO输入倍增为20,8MHz*15 = 120MHz RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV7; // SAI时钟7分频,120/7 = 17.14MHz RCC_OscInitStruct.PLL.PLLQ = RCC_PLLQ_DIV2; // SDMMC、RNG、USB时钟2分频,120/2 = 60MHz RCC_OscInitStruct.PLL.PLLR = RCC_PLLR_DIV2; // 系统主时钟分区2分频,120/2 = 60MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); } // 初始化CPU,AHB和APB总线时钟 RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; // 需要配置的时钟HCLK、SYSCLK、PCLK1、PCLK2 RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; // 配置系统时钟为PLLCLK输入,60MHz RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // AHB时钟为系统时钟1分频,60/1 = 60MHz RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; // APB1时钟为系统时钟1分频,60/1 = 60MHz RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; // APB2时钟为系统时钟1分频,60/1 = 60MHz // RCC时钟配置,出错则进入错误处理函数 if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) // HCLK=60MHz,Vcore=3.3V,所以选择SW2(FLASH_LATENCY_2) { _Error_Handler(__FILE__, __LINE__); }  // 初始化外设时钟 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART1|RCC_PERIPHCLK_USART2 |RCC_PERIPHCLK_LPUART1|RCC_PERIPHCLK_LPTIM1 |RCC_PERIPHCLK_I2C2|RCC_PERIPHCLK_ADC |RCC_PERIPHCLK_RNG;// 需要初始化的外设时钟:USART1、USART2、LPUART1、LPTIM1、I2C2、ADC、RNG PeriphClkInit.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; // 配置串口USART1时钟为PCLK2,60MHz PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; // 配置串口USART2时钟为PCLK1,60MHz PeriphClkInit.Lpuart1ClockSelection = RCC_LPUART1CLKSOURCE_HSI; // 配置LPUART时钟为HSI,16MHz PeriphClkInit.I2c2ClockSelection = RCC_I2C2CLKSOURCE_PCLK1; // 配置I2C2时钟为PCLK1,60MHz PeriphClkInit.Lptim1ClockSelection = RCC_LPTIM1CLKSOURCE_LSE; // 配置LPTIM1时钟为LSE,32.768KHz PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_PLLSAI1; // 配置ADC时钟为PLLSAI1,现在为60MHz,下面会重新定义 PeriphClkInit.PLLSAI1.PLLSAI1Source = RCC_PLLSOURCE_HSE; // 配置PLLSAI1时钟为HSE,8MHz PeriphClkInit.PLLSAI1.PLLSAI1M = 1; // 配置PLLSAI1分频为1 PeriphClkInit.PLLSAI1.PLLSAI1N = 8; // 配置PLLSAI1倍增为8 PeriphClkInit.PLLSAI1.PLLSAI1P = RCC_PLLP_DIV7; // SAI时钟7分频,64/7 = 9.142857MHz PeriphClkInit.PLLSAI1.PLLSAI1Q = RCC_PLLQ_DIV2; // SDMMC、USB时钟2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1R = RCC_PLLR_DIV2; // 系统主时钟分区2分频,64/2 = 32MHz PeriphClkInit.PLLSAI1.PLLSAI1ClockOut = RCC_PLLSAI1_ADC1CLK; // 配置PLLSAI1输出为ADC1时钟,也就是配置ADC1时钟,32MHz PeriphClkInit.RngClockSelection = RCC_RNGCLKSOURCE_MSI; // 外设时钟配置,出错则进入错误处理函数 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }  // 配置内部主稳压器输出电压,配置为稳压器输出电压范围1模式,也就是:典型输出电压为1.2V,系统频率高达80MHz if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }  // 配置系统定时器中断时间,配置为HCLK的千分频 HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);  // 配置系统定时器,配置为HCLK HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);  // 系统定时器中断配置,设置系统定时器中断优先级最高(为0),且子优先级最高(为0) HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);}</syntaxhighlight> ==== gyu_rng.c ====初始化RNG。 <syntaxhighlight lang="c++" line="1" start="43">void MX_RNG_Init(void){ hrng.Instance = RNG; if (HAL_RNG_Init(&hrng) != HAL_OK) { _Error_Handler(__FILE__, __LINE__); }}</syntaxhighlight>使能RNG时钟,并且配置和使能RNG中断。<syntaxhighlight lang="c++" line="1" start="60">void HAL_RNG_MspInit(RNG_HandleTypeDef* hrng){ if(hrng->Instance==RNG) { // 使能RNG时钟 __HAL_RCC_RNG_CLK_ENABLE(); // 使能NVIC中断及优先级 HAL_NVIC_SetPriority(RNG_IRQn, 10, 0); HAL_NVIC_EnableIRQ(RNG_IRQn); }}</syntaxhighlight>随机数的获取函数,在此处打开中断。此处中断开启,会触发HAL_RNG_ErrorCallback()或者HAL_RNG_ReadyDataCallback()回调函数,当有随机数成功生成时,返回HAL_RNG_ReadyDataCallback()回调。<syntaxhighlight lang="c++" line="1" start="81">void RNG_Get(void){ HAL_RNG_GenerateRandomNumber_IT(&hrng); // 随机数获取函数(开启中断)}</syntaxhighlight>真随机数成功生成的回调函数,参数random32bit就是生成的随机数,我们利用printf函数将随机数打印到串口。<syntaxhighlight lang="c++" line="1" start="108">void HAL_RNG_ReadyDataCallback(RNG_HandleTypeDef* hrng, uint32_t random32bit){ printf("%u\r\n",random32bit); // 将随机数打印到串口}</syntaxhighlight> == 16-RTC实时时钟实验 ===== STM32L476 RTC时钟简介 === ''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 硬件设计 ===选择STM32L4 ''<span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span><span class="tlid-translation-gender-indicator translation-gender-indicator"></span>''=== 实验准备 ===# 使用miniUSB线及10pin排线,通过Jlink仿真器连接PC端和开发板。# 使用miniUSB线,连接PC与开发板USB接口。# 将SW1拨到USB端,SW2拨到MCU。# 使用Keil打开基础实验 10-串口打印实验工程。# 使用Xshell打开miniUSB虚拟出的COM口# 下载程序,并完成功能测试。 === 实验验证 ===下载完成后,可以看到TFT屏幕上打印当前的实时时间、日期以及星期,且10s后会触发闹钟(表现为TFT打印"Alarm"、蜂鸣器哔一声)。 === 源码详解 ===本节中的源码说明,仅针对此例程中的重要功能,详细的源码介绍请大家参照代码后的注释。 ==== stm32l4xx_hal_conf.h ====此文件位于“10-串口打印实验\Inc”路径中,主要用途是选择使能此例程使用到的库文件。 此例程我们主要给大家展示STM32L4的实时时钟功能,所以我们宏定义中打开RTC相关的。<syntaxhighlight lang="c" line="1" start="103">// 使能的宏#define HAL_MODULE_ENABLED // 芯片#define HAL_FLASH_MODULE_ENABLED // Flash#define HAL_PWR_MODULE_ENABLED // 电源#define HAL_RCC_MODULE_ENABLED // 时钟#define HAL_CORTEX_MODULE_ENABLED // NVIC #define HAL_GPIO_MODULE_ENABLED // GPIO#define HAL_DMA_MODULE_ENABLED // DMA#define HAL_UART_MODULE_ENABLED // UART#define HAL_SPI_MODULE_ENABLED // SPI#define HAL_RTC_MODULE_ENABLED // RTC实时时钟</syntaxhighlight>接下来我们初始化TFT彩屏相关的SPI控制接口,以及GUI图形界面初始化。并且格式化打印一些内容,字体显示为红色的部分。
==== main.c ====main函数,我们的例程由此处开始执行,首先调用HAL_Init()函数初始化我们的模块,接着调用SystemClock_Config()函数初始化此例程用到的时钟,具体有哪些时钟被初始化,在gyu_util.c部分有详细说明。最后我们初始化RTC时钟,并且调用实现好的有关RTC的函数,去配置当前的时钟、日期、星期、以及闹钟。
接下来我们初始化TFT彩屏相关的SPI控制接口,以及GUI图形界面初始化。并且格式化打印一些内容,字体显示为红色的部分。在while()循环当中,持续获取当前的时钟、日期等相关信息,并且打印到TFT彩屏上显示。
最后我们初始化RTC时钟,并且调用实现好的RTC当检测到闹钟信息,则在TFT彩屏上打印"Alarm"字样指示闹钟。
<syntaxhighlight lang="c++" line="1" start="44">
</syntaxhighlight>
==== gyu_util.c ====
请参照实验1中的介绍。请参照实验01中的介绍。
基础实验中的其他例程,大部分都是使用的相同的时钟配置函数,有特殊的时钟使用,将会在对应例程的源码详解中做针对性说明。
510
个编辑