当前位置: 首页 > news >正文

扬州建设网站淘宝客网站主题模板

扬州建设网站,淘宝客网站主题模板,新开的店怎么弄定位,外贸公司有哪些岗位目录 前言 GPIO#xff08;通用输入输出引脚#xff09; 推挽输出模式 浮空输入和上拉输入模式 GPIO其他模式以及内部电路原理 输出驱动器 输入驱动器 中断 外部中断#xff08;EXTI#xff09; 深入中断#xff08;内部机制及原理#xff09; 外部中断/事件控…目录 前言 GPIO通用输入输出引脚 推挽输出模式 浮空输入和上拉输入模式 GPIO其他模式以及内部电路原理 输出驱动器 输入驱动器 中断 外部中断EXTI 深入中断内部机制及原理 外部中断/事件控制器 NVIC嵌套向量中断控制器 串口 轮询模式 中断模式 DMA模式与收发不定长数据 DMA模式 接收不定长数据 补充回调函数的模板以及解决传输过半中断 I2C 标准I2C模式读取AHT20传感器 aht20.c main.c I2C中断模式读取AHT20传感器 aht20.c main.c I2CDMA模式 时钟树 CubeMX配置 定时器TIM 基本定时功能 知识点补充 定时器更新中断 外部时钟与循迹模块 外部时钟模式2 外部时钟模式1 定时器从模式 复位模式 Reset Mode 门模式 Gated Mode 触发模式 Trigger Mode 知识点补充定时器上电自动触发一次中断 输入捕获超声波测距 超声波测距模块 输入捕获 输出比较PWM脉冲宽度调制 PWM 输出比较模式 旋转编码器 ADC模拟-数字转换技术 单次转换模式 连续转换模式 ADC多通道采集功能 单次转换模式 连续转换模式 RTC实时时钟 HAL库RTC库 自己实现RTC库函数 前言 这是一份专注于STM32 HAL库的实战学习笔记你将通过它掌握两大核心技能         手把手教你通过CubeMX配置常用外设摆脱对标准库的依赖         像查字典一样随时检索HAL库函数用法快速解决开发中的配置难题。 无论你是         ● 从标准库转向HAL库的开发者         ● 需要快速搭建原型的竞赛选手         ● 希望建立规范化开发流程的工程师 这里都会提供开箱即用的代码模板和直击痛点的配置要点。话不多说让我们直接开启HAL库的学习之旅本教程会持续更新希望大家关注支持一下。 GPIO通用输入输出引脚 其作用就是在STM32的控制之下读取外部电路的电压值或者向外输出一定的电压值它一共有8大使用模式我们先来学习最简单也是最常用的推挽输出。 推挽输出模式 可以在STM32的控制下向外部输出0V的低电平或者3.3V的高电平假设我们要控制单片机上的PA7引脚来点亮连接在这个引脚上的LED灯配置如下 打开CubeMX配置PA7引脚为 GPIO_Output 这样PA7就设置成了输出模式 我们再细化一下设置点击左侧的System Core再点击GPIO就能看到我们刚刚设置的PA7引脚 再点击PA7就能进行详细的设置 HAL库为我们提供了控制IO电平的函数 /* GPIOx:要设置的GPIO的分组* GPIO_Pin:要设置的GPIO的编号* PinState:GPIO状态(GPIO_PIN_SET/GPIO_PIN_RESET)*/ HAL_GPIO_WritePin(GPIOx, GPIO_Pin, PinState); 浮空输入和上拉输入模式 本小节学习如何使用GPIO的输入功能实现按键控制小灯在下节再对GPIO的几种模式进行稍微深入一点的探索。 本小节要实现的效果为按住KEY1不松开时LED1亮起松开KEY1时LED1熄灭按一下KEY2LED2亮起再按一下KEY2LED2熄灭。KEY1的硬件原理图如下 外部自带了一个上拉电阻平时按键松开PB12直接通过一个电阻连接到了3.3V电源上一会我们会将PB12设置成GPIO八大模式之一的浮空输入模式浮空输入模式下的GPIO口内部处于高阻态相当于芯片内部有一个巨大的电阻根据电阻分压原理10K电阻的压降几乎为0V因此PB12处就几乎是3.3V。懂了这个原理其他场景下也是这样分析的。 我们开始CubeMX的配置假设LED1和2分别为PA7和PB0按键引脚为PB12将PB12设置成 GPIO_Input 到这里就可以直接生成代码了不需要进行更加详细的设置设置成输入模式后默认是浮空输入模式 HAL库为我们提供了读取引脚电平的函数 /* 返回值:GPIO_PIN_SET/GPIO_PIN_RESET*/ GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin); 我们先来实现KEY1控制LED1的代码十分简单在main函数的while循环里实现 while (1) {if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_RESET){HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);}else{HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);} } 我们再来看看KEY2的硬件原理图 可见KEY2并没有外置的上拉电阻而上拉下拉的操作也非常常见因而STM32为我们在芯片内部提前准备好了上拉与下拉电阻我们回到CubeMX来实现KEY2控制LED2的效果KEY2的引脚为PB13 点击PB13进入详细设置设置成Pull-up启用上拉输入模式之后我们就来写代码实现KEY2每按一次LED2的电平就翻转的效果 while (1) { if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) GPIO_PIN_RESET){//软件消抖HAL_Delay(10);//10ms后如果任然检测到按键被按下,就执行相应操作if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) GPIO_PIN_RESET){HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);//每按下一次按键只能翻转一次电平,因此按键没松开之前要一直卡在这里,避免多次触发while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_13) GPIO_PIN_RESET);}} } GPIO其他模式以及内部电路原理 前面我们学习了GPIO的推挽输出、浮空输入、上拉输入以及下拉输入原理和上拉输入类似尚未见过的还有开漏输出、复用推挽输出、复用开漏输出以及模拟输入。本小节来了解一下它们在芯片内部的电路原理并通过电路来推理各个模式的应用场景本小节围绕下面这张图展开 输出驱动器 我们先来看输出驱动器可以看到被输出控制模块控制的两个MOS其作用可以简化为两个被控制的开关我们假设IO口连接了一个小灯小灯另一端是GND小灯工作电压为3.3V则我们可以使用推挽输出模式此模式下P-MOS和N-MOS协同工作当我们控制IO口输出高低电平时对应的MOS将会激活连接VDD3.3V或者VSS0V从而对外输出高电平或低电平 但总有一些更高或者更低的电压驱动推挽输出最大只能输出3.3V的电压例如小灯的工作电压为5V此时就需要用到开漏输出此模式下只有N-MOS工作P-MOS一直处于断开的状态当使用HAL库的函数控制IO口输出高电平时N-MOS断开整个IO口内部处于高阻态或者说断路并不对外输出特定的电平信号若控制IO口输出低电平则N-MOS激活IO口与VSS连接小灯两端都是0V若将小灯另一端连接到5V这时候就能通过开漏输出模式控制小灯的亮灭 需要注意的是这里我们需要用支持5V容忍的IO口否则会使上方保护二极管长期导通将5V引入电源中造成损坏。 对于输出控制模块它有两个控制指令的来源一个是我们使用HAL库的函数控制的输出寄存器而另一个则是我们后面将会学习的片上外设例如串口模块、I2C模块等因而根据控制来源的不同STM32将两种输出模式又细分成了普通的推挽输出和开漏输出以及复用推挽输出与复用开漏输出。 输入驱动器 来到输入驱动器外部输入的电流从IO口引脚进入后首先经过一对上拉下拉电阻可配置成前面说的三种输入模式浮空输入、上拉输入、下拉输入 后面的肖特基触发器用于稳定电平处理后的电平信号被写入输入数据寄存器等待我们使用HAL库的函数对寄存器进行读取这就是最基本的GPIO口读取高低电平的原理。注意到这条线路中有两个分支第一条分支通向了模拟输入浮空、上拉、下拉这三种输入模式都是仅读取了高低电平也就是数字信号而模拟输入则是读取输入电压的具体数值因此在触发器前产生分支将电压引入了模拟输入相关的片上外设将会在以后的ADC相关知识了解到 另一条支线则是在触发器后接入了例如串口模块等需要数字输入的片上外设这里要注意的是输入部分的不同分支可以同时读取触发器的输出因此不会出现复用上拉输入等模式而是在片上外设上也使用普通的输入模式即可。 中断 STM32要随时准备着去处理一些我们为其规定的各种突发事件处理完成后还要继续执行之前正在执行的任务而这些可以打断正常工作流程去处理的任务我们就将其称之为中断。 对于STM32芯片来说可以产生中断的事件多种多样例如指令出错、定时器结束、串口接收到数据、GPIO电平变化等等。。。本小节先来学习检测GPIO口电平变化的中断称之为外部中断EXTI 外部中断EXTI 假设我们有这样的需求LED1以4秒为周期循环闪烁亮两秒灭两秒当KEY1按下时LED2要翻转亮灭状态。 在配置CubeMX之前我们先想想为什么需要用中断的方式来检测按键是否被按下         如果用之前的方式将按键引脚配置成普通的输入模式由于还没有学到定时器要让LED1周期闪烁肯定要用到 HAL_Delay(); 这个函数它会延迟等待我们设定的时间后再执行下面的代码因此当我们按下按键时程序大概率在执行延时函数那我们按下按键就是在做无用功因此我希望当我按下按键时能快速响应就引入了中断。 来到CubeMX界面配置KEY1的PB12引脚为 GPIO_EXTI12 也就是第12号外部中断线具体概念在下一小节会讲还没完来到System Core下的GPIO对PB12进行详细设置 点击GPIO mode的选项框会有六个选项其中前三个与中断有关 很显然如果有上拉电阻的话当按键被按下时电平从高变成低那我们选择下降沿触发中断然后我们还要点击NVIC也就是中断控制器 勾选上开启中断向量EXTI15_10NVIC和中断向量的细节我们会在下一小节讲解: 生成代码后打开 stm32f1xx_it.c 后缀it表示它是与interrupt中断相关的文件此文件的最底部有一个CubeMX帮我们自动生成的函数 它就是我们按下按键触发中断后STM32会调用执行的中断处理函数因此我们在这个函数中翻转LED2的亮灭就能实现我们想要的效果。 main.c while(1) {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_RESET);HAL_Delay(2000);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_7, GPIO_PIN_SET);HAL_Delay(2000); } stm32f1xx_it.c void EXTI15_10_IRQHandler(void) {/* USER CODE BEGIN EXTI15_10_IRQn 0 */HAL_Delay(10);if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_RESET)HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);/* USER CODE END EXTI15_10_IRQn 0 */HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);/* USER CODE BEGIN EXTI15_10_IRQn 1 *//* USER CODE END EXTI15_10_IRQn 1 */ } 延时10ms是为了进行软件消抖按键按下或者抬起时的抖动都会有下降沿从而触发中断所以我们先延时10ms等待抖动过去10ms后再来判断引脚电平是否为低电平来判断按键是因为按下时的下降沿触发还是因为抬起时抖动中的下降沿触发确认是按键按下的下降沿触发后翻转LED2的电平。 注意此时还不能编译运行程序会跑飞因为这里涉及到了发生多个中断时的优先级问题这里的 HAL_Delay(); 函数需要依赖一个叫做 System tick timer系统滴答的中断但此中断的优先级比我们所触发的中断的优先级要低也就是说要等我们的中断处理函数执行完后才能执行 HAL_Delay(); 那就尬住了解决方法很简单回到CubeMX 让 System tick timer 的优先级数字小于我们的 EXTI15_10 即可数字越小优先级越高优先级也会在下一小节讲到也就是说当我们的中断处理函数触发的时候调用的HAL_Delay可以先执行执行完后再回来执行我们的中断处理函数。这里要补充的是正规的项目中直接在中断中实现按键逻辑尤其是在中断中调用延时函数HAL_Delay是一种不被推荐的做法因为我们需要尽可能的保证中断任务尽快执行完成以将中断对正常流程的影响降到最低。 深入中断内部机制及原理 外部中断/事件控制器 前面我们提到了NVIC、EXTI、中断向量、中断优先级本小节就详细讲讲。从上小节接触到的EXTI外部中断入手外部的电平信号进入到GPIO口后来到了输入驱动器经过上拉下拉电阻后经过施密特触发器转化最后抵达输入数据寄存器或者片上外设再接下来电平信号还会抵达这么一个结构 这样的结构在STM32F1系列芯片中共有19个这19个外部中断控制器共用一套寄存器但连线是独立的共有19组连线每个外部中断都对应其中一组线路前16个也就是EXTI0~EXTI15分别对应与其编号相同的GPIO口就是说PA0、PB0、PC0、PD0进入的电平信号都可以进入EXTI0以此类推 左下角的脉冲发生器和事件屏蔽寄存器与中断无关而是与事件Event相关的结构事件信号会送达相应的外设由外设自行处理本小节先忽略得到下面的简化图我们先看高亮这一部分 边沿检测电路可以检测输入的电平信号中有没有发生高低电平的转换我们之前在CubeMX选择是上升沿还是下降沿触发中断就是在配置上面两个寄存器当检测到我们设置的模式时就会向后传递一个高电平信号然后经过一个或门。注意这里的的软件中断事件寄存器让我们可以通过程序模拟产生一个中断一般不需要这里先忽略 请求挂起寄存器是需要注意的点其接收到高电平信号后会将对应位置1并将此位输出到一个与门因此中断屏蔽寄存器就起到了决定性的作用只有它的对应位置上为1输出高电平请求挂起寄存器的信号才能通过与门进入到NVIC中断屏蔽寄存器的开启在CubeMX将引脚设置成GPIO_EXTIx时就自动帮我们在代码中生成了高电平一直向后就来到了中断最高城——NVIC。 NVIC嵌套向量中断控制器 其主要作用就是掌管这样一张中断向量表 在所有外部中断线中只有EXTI0~4有自己的中断向量EXTI5~9共享中断向量EXTI9_510~15同理因此我们之前的PB12外部中断它的中断向量就是EXTI15_10然后执行相对应的函数。这里需要注意的是NVIC会一直检测某个中断线是否被激活为了防止中断处理函数重复执行需要将请求挂起寄存器的对应位清除为0但我们上一节写的代码并没有这么做这是因为 当有多个中断同时触发就要引入中断优先级分为抢占优先级和响应优先级数字越小优先级越高 若两个都相同就按照它们在中断向量表中的顺序决定 由上可得响应优先级仅在两中断同时发生时起到辅助作用抢占优先级才是大哥STM32为每个中断向量准备了4个二进制位来存储中断优先级信息 在CubeMX中我们可以自由选择这4位中几位用来设置抢占优先级几位用来设置响应优先级默认4位都用来设置抢占优先级这时优先级可以设置的范围为0~15。 此处插播一个小知识点如果中断模式下需要上拉或下拉在CubeMX中也可以直接配置 串口 本小节开始学习单片机中最常见的串口——TTL串口仅需两根数据线就可完成两个设备的双向通信连接方式如下注意要共地 轮询模式 我们先学习串口三大模式中最基本的模式——轮询模式它会阻塞程序的执行直到完成发送或接收或者等待超时接收时需要接收固定长度的数据。 接下来进入实战环节日常开发中我们常使用串口与电脑进行通信本小节实现电脑向STM32发送指令指令的样式定长为两个字节STM32收到指令后需回复电脑当前收到的指令话不多说打开CubeMX 点击左侧的Connectivity就可以看到USARTF1C8T6的芯片有三个串口本小节以USART2为例点击后选择异步模式Asynchronous可以看到PA2和PA3分别被设置成了USART2的TX引脚和RX引脚 通信两设备要使用相同的波特率才能正常通信后面三个通常保持默认即可生成代码后我们来实现效果 串口轮询模式的发送函数和接收函数为 /* 第一个参数是串口的句柄,要使用哪个串口进行发送* 最后一个参数是超时时间,你愿意等待多久无论是否发送成功,单位为ms* 填写HAL_MAX_DELAY表示愿意永久等待*/ HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);/* 参数用法和Transmit相同*/ HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); main.c //使用strlen函数获得字符串长度需要包含此头文件 #include string.huint8_t receiveData[2];/*省略,直接来看while循环*/ while (1) {HAL_UART_Receive(huart2, receiveData, 2, HAL_MAX_DELAY);HAL_UART_Transmit(huart2, receiveData, strlen(receiveData), 100); } 中断模式 本小节学习中断模式解决程序等待问题在轮询模式中CPU需要不断去查询发送数据寄存器中的数据是否已经移送到发送移位寄存器移了的话就赶紧把下一个数据塞进TDR没移就不断查询直到把要发送的数据全部发完或者用时超过设定的超时时间轮询模式下的接收也是类似CPU会一直查询接收数据寄存器RDR中是否有新数据可以读。很明显CPU会一直查询和搬运无暇顾及其他任务即堵塞。 中断模式下CPU将数据塞入寄存器后就可以执行其他代码了当发送移位寄存器中的数据发送出去后会触发发送数据寄存器空中断把CPU叫回来再塞入数据以此反复我们来到CubeMX打开System Core下的NVIC然后打开USART2的中断功能就这么简单 生成代码来看看如何使用中断模式发送数据使用中断发送数据的函数与轮询模式十分类似只是加上了_IT后缀。函数如下 /* 中断模式下的串口发送十分简单,不再需要设置超时时间*/ HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);/* 中断模式下的串口接收有些特殊* 因为其不会堵塞程序的执行,往往出现上次的数据还没接收完,又开始执行串口中断接收* 因此将中断模式下的接收放在while循环前面,把它当作启动函数*/ HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);怎样知道何时数据接收完成          使用中断处理函数。来到我们之前提到过的 stm32f1xx_it.c 文件在最底部就可以找到USART2的中断处理函数 USART2_IRQHandler 这里需要注意的是我们的逻辑代码不能再写在IRQHandler里了这是因为每个USART只有一个中断向量除了我们要用到的接收数据寄存器非空中断还有发送数据寄存器空中断等等都共用了此中断处理函数因此需要判断是什么原因引起的中断这在 HAL_UART_IRQHandler 函数里为我们封装好了我们需要找到对应的回调函数 进入上面的函数往下找到带__weak前缀的函数我们可以重写它实现自己的逻辑代码找到这个函数往往根据函数名就知道它的功能 /* 接收完成回调函数* 当接收到我们想要的字节数后会调用该函数*/ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {/*处理数据等操作*//* !!!退出回调函数之前重新启动接收中断,为下一次的接收做准备*/HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); } DMA模式与收发不定长数据 中断模式学习完了不知道你有没有发现接收到的数据仍然是固定长度的只不过CPU的利用率变高了本小节来学习效率最高的DMA模式以及如何使用扩展函数实现收发不定长的数据。 DMA模式 中断模式下发送和接收的数据寄存器空和非空都会产生中断把CPU叫回来搬运数据我们还能再压榨一下性能给CPU找一个小助手让它在寄存器和内存之间搬运数据它就是DMA直接内存访问只要我们创建一个DMA通道告诉DMA将数据从哪里搬到哪里等全部搬运完成DMA会通过中断提醒我们例如我们只需要为串口的接收和发送创建两条DMA通道就可以让DMA帮着在串口的寄存器与内存变量之间搬运数据了来到CubeMX看看如何创建DMA通道 来到USART2的配置界面可以看到还有其他选项卡一眼就找到了DMA的配置界面 点击DMA Settings点击Add按钮就可以添加一个DMA通道然后为新的通道选择功能 我们先为USART2的发送添加一个DMA通道 上面这些都是默认生成的我并没有改动数据地址是否自增要结合具体情况来选择寄存器只有一字节的长度因此地址不需要自增而从内存变量中是依次搬运要发送的数据因此内存地址选择了自增。为USART2的RX添加一个DMA通道同理 生成代码后来看看DMA模式下的串口发送函数只需要将后缀的_IT改成_DMA即可回调函数仍然是 HAL_UART_RxCpltCallback 仍然要注意退出回调函数时重新启动DMA接收 /* DMA模式下的串口发送函数*/ HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);/* DMA模式下的串口接收函数* 同样的,第一次调用该函数在while循环之前调用一次,之后在回调函数退出之前调用*/ HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);接收不定长数据 主要靠的就是串口空闲idle中断此中断的触发条件与接收的字节数无关只有当RX引脚上无后续数据进入时触发因而认为串口空闲中断发生时就是一帧数据包接收完成了在此时对数据进行分析处理即可。 只需要调用以下函数即可可以看到有3个版本选择有DMA后缀的 HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint16_t *RxLen, uint32_t Timeout); HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);/* 这里只讲DMA模式* 前两个参数和之前一样* 第三个参数不是想要接收的数据长度,而是一次能接收的最大数据长度!!!* 一般填写接收数组的长度,避免接收数据太长导致数组越界*/ HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); 例如我定义了一个数组uint8_t receiveData[50]; 那么第三个参数可以传入 sizeof(receiveData)还要注意的是ReceiveToIdle的回调函数并不是我们之前用的RxCpltCallback了来到stm32f1xx_hal_uart.c文件找到 HAL_UARTEx_RxEventCallback 回调函数 /* 与之前的回调函数不同的是多了一个入参Size* 可以通过Size得知本次接收到了多少个字节的数据*/ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size); 补充回调函数的模板以及解决传输过半中断 //拿上面刚讲到的举例 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {//每次进入回调函数之前判断是哪个句柄?假设是USART2触发的中断if(huart huart2){/*自己的逻辑代码,我这里将接收到了任意个字节的数据原封不动发回给PC*/HAL_UART_Transmit_DMA(huart2, receiveData, Size);//在每次退出之前和初始化的时候别忘了这句HAL_UARTEx_ReceiveToIdle_DMA(huart2, (uint8_t *)receiveData, sizeof(receiveData));/* 关闭传输过半中断* 第一个参数是DMA通道的指针地址* 第二个参数是要关闭的中断:选择传输过半中断* 别忘了在while循环之前(初始化)也禁止一下,避免第一次就触发了传输过半中断*/__HAL_DMA_DISABLE_IT(hdma_usart2_rx, DMA_IT_HT);} } 这里需要注意的是退出回调函数时启动下一次接收使用普通模式的 HAL_UARTEx_ReceiveToIdle 和中断模式的 HAL_UARTEx_ReceiveToIdle_IT 都是可以的但是DMA模式除了串口空闲中断以外还有一个传输过半中断就是说接收的数据量达到最大值的一半时会触发这个回调函数未察觉到是因为最大接收的字节数设置的比较大而发送数据的时候比较短自然不会触发使用一个很大的接收数组自然能解决但治标不治本因此需要关闭DMA传输过半中断。 需要在usart.h里手动定义 extern DMA_HandleTypeDef hdma_usart2_rx; 否则使用 __HAL_DMA_DISABLE_IT 函数时第一个参数会报错显示未定义需要我们手动定义。 I2C I2C和串口不同只有一条线可以用来传递数据SDA另一条用于提供同步时钟脉冲的时钟线SCL并且为半双工通信同一时刻只能进行一个方向的通信因此采用主从模式一台为主机另一台多台为从机 标准I2C模式读取AHT20传感器 具体的原理不详细展开了我们直接来使用CubeMX实现I2C读取温湿度传感器AHT20DHT20点击左侧的Connectivity里的I2C1将其配置成标准的I2C模式 关于I2C的中断和DMA模式会在下小节讲到这次的代码需要为AHT20写一下驱动文件来到Project Manager项目管理点开Code Generator代码生成器勾选上为每个外设生成一对.c/.h文件 生成代码后来到keil新建aht20.c和aht20.h文件然后根据传感器读取流程来直接写代码 这里需要注意的是AHT20模块的设备地址其实为0x38I2C通信一般使用7位地址码那么它的地址0111000按理说是0x38但是I2C通信每次发送都是一字节的数据规定从机地址向左移动一位因此是0x70那为什么一开始要发送0x71呢这是因为协议规定主机是要写从机最后一位就是0读数据最后一位就是1不过HAL库对这一位的设置在函数里帮我们自动处理因此我们默认从机地址为8位且最后一位为0。 初始化后就可以开始读取数据了步骤如下 读到的六字节数据如下温度数据和湿度数据各占两个半字节因此后续还需要拼接 最后对读到的数据根据公式进行转化就可以获得温度和湿度 普通的I2C发送和接收函数 /* 普通I2C接收函数* 第一个参数是I2C句柄* 第二个参数是从机地址,8位地址,最后一位为0,函数内部会自动处理最后一位* 第三个参数是要发送的数据的指针* 第四个参数是要发送的字节数* 第五个参数是超时时间*/ HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);/*参数同上*/ HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); aht20.c #include aht20.h#define AHT20_ADDRESS 0x70 // AHT20 I2C设备地址/*** brief AHT20传感器初始化函数* note 初始化前需保证传感器已上电至少40ms用于稳定内部状态* 如果传感器未校准发送校准命令0xBE 0x08 0x00*/ void AHT20_Init(void) {uint8_t readBuffer; // 用于读取状态寄存器的缓冲区// 等待传感器上电稳定数据手册要求至少40msHAL_Delay(40);// 读取状态寄存器1字节HAL_I2C_Master_Receive(hi2c1, AHT20_ADDRESS, readBuffer, 1, HAL_MAX_DELAY);// 检查校准状态状态寄存器bit30x08为1表示已校准if((readBuffer 0x08) 0x00) {// 发送初始化校准命令0xBE命令0x08参数10x00参数2uint8_t sendBuffer[3] {0xBE, 0x08, 0x00};HAL_I2C_Master_Transmit(hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_DELAY);} }/*** brief 读取AHT20传感器的温湿度数据* param *Tem : 输出温度值单位摄氏度* param *Hum : 输出湿度值单位百分比RH* note 发送触发测量命令0xAC 0x33 0x00后需等待至少75ms* 原始数据为20位需按公式转换为实际物理量*/ void AHT20_Read(float *Tem, float *Hum) {uint8_t sendBuffer[3] {0xAC, 0x33, 0x00}; // 触发测量命令uint8_t readBuffer[6]; // 数据接收缓冲区状态5字节数据// 发送测量命令HAL_I2C_Master_Transmit(hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_DELAY);// 等待测量完成数据手册要求至少75msHAL_Delay(75);// 读取6字节数据1字节状态 5字节测量数据HAL_I2C_Master_Receive(hi2c1, AHT20_ADDRESS, readBuffer, 6, HAL_MAX_DELAY);// 检查状态寄存器bit70x800表示数据就绪if((readBuffer[0] 0x80) 0x00){uint32_t data 0;/*---------------- 湿度计算 ----------------*/// 将readBuffer[1]、readBuffer[2]、readBuffer[3]高4位组合成20位数据data ((uint32_t)readBuffer[3] 4) // 取第3字节高4位低位((uint32_t)readBuffer[2] 4) // 第2字节左移4位中间((uint32_t)readBuffer[1] 12); // 第1字节左移12位高位// 转换为百分比Hum (data / 2^20) * 100*Hum data * 100.0f / (1 20);/*---------------- 温度计算 ----------------*/// 将readBuffer[3]低4位、readBuffer[4]、readBuffer[5]组合成20位数据data (((uint32_t)readBuffer[3] 0x0F) 16) // 取第3字节低4位并左移16位((uint32_t)readBuffer[4] 8) // 第4字节左移8位((uint32_t)readBuffer[5]); // 第5字节直接使用// 转换为摄氏度Tem (data / 2^20) * 200 - 50*Tem data * 200.0f / (1 20) - 50;} } 头文件里声明这两个函数就行了接下来通过串口来打印一下温湿度信息 main.c /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include aht20.h #include stdio.h #include string.h /* USER CODE END Includes */int main(void) {/* USER CODE BEGIN 2 */AHT20_Init();float tem,hum;char message[50];/* USER CODE END 2 */while (1){AHT20_Read(tem, hum);sprintf(message, 温度: %.1f ℃, 湿度: %.1f %%\r\n,tem, hum);HAL_UART_Transmit(huart2, (uint8_t *)message, strlen(message), HAL_MAX_DELAY);HAL_Delay(1000);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */} } 使用sprintf函数拼接一下字符串就能够通过串口打印出来了 I2C中断模式读取AHT20传感器 使用起来I2C的中断与DMA模式其实也跟串口的这两种模式用法一样来到CubeMX 中断模式下的I2C发送和接收函数以及发送和接收完成回调函数如下 //I2C中断模式发送函数 HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);//I2C中断模式接收函数 HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size);//I2C主机发送完成回调函数 void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c);//I2C主机接收完成回调函数 void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c); 由于是中断模式发送和接收自然不用超时时间而是引入了回调函数的机制和串口一样现在就用中断模式的函数改写一下读取温湿度的函数只在原来的函数后面加上_IT后缀是远远不够的对数据的处理得放在回调函数里实现因此使用状态机编程来实现 aht20.c #include aht20.h#define AHT20_ADDRESS 0x70uint8_t readBuffer[6];void AHT20_Init(void) {uint8_t readBuffer;HAL_Delay(40);HAL_I2C_Master_Receive(hi2c1, AHT20_ADDRESS, readBuffer, 1, HAL_MAX_DELAY);if((readBuffer 0x08) 0x00){uint8_t sendBuffer[3] {0xBE, 0x08, 0x00};HAL_I2C_Master_Transmit(hi2c1, AHT20_ADDRESS, sendBuffer, 3, HAL_MAX_DELAY);} }//把发送测量指令、数据读取、数据解析都拆开来分步骤执行 void AHT20_Measure(void) {static uint8_t sendBuffer[3] {0xAC, 0x33, 0x00};HAL_I2C_Master_Transmit_IT(hi2c1, AHT20_ADDRESS, sendBuffer, 3); }void AHT20_Get(void) {HAL_I2C_Master_Receive_IT(hi2c1, AHT20_ADDRESS, readBuffer, 6); }void AHT20_Analysis(float *Tem, float *Hum) {if((readBuffer[0] 0x80) 0x00){uint32_t data 0;data ((uint32_t)readBuffer[3] 4) ((uint32_t)readBuffer[2] 4) ((uint32_t)readBuffer[1] 12);*Hum data * 100.0f / (1 20);data (((uint32_t)readBuffer[3] 0x0F) 16) ((uint32_t)readBuffer[4] 8) ((uint32_t)readBuffer[5]);*Tem data * 200.0f / (1 20) -50;} }main.c #include aht20.h #include stdio.h #include string.h//0初始状态 发送测量指令 1正在发送测量指令 2测量指令发送完成 等待75ms后读取AHT20 3读取中 4读取完成 解析数据后恢复为初始状态 uint8_t aht20state 0;//I2C主机发送完成回调函数 void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {if(hi2c hi2c1){aht20state 2;} }//I2C主机接收完成回调函数 void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c) {if(hi2c hi2c1){aht20state 4;} }int main(void) {/* USER CODE BEGIN 2 */AHT20_Init();float tem,hum;char message[50];while (1){if(aht20state 0){AHT20_Measure();aht20state 1;}else if(aht20state 2){HAL_Delay(75);AHT20_Get();aht20state 3;}else if(aht20state 4){AHT20_Analysis(tem, hum);sprintf(message, 温度%.1f ℃, 湿度%.1f %%\r\n,tem, hum);HAL_UART_Transmit(huart2, (uint8_t *)message, strlen(message), HAL_MAX_DELAY);HAL_Delay(1000);aht20state 0;}} } I2CDMA模式 十分简单回到CubeMX为I2C的TX和RX分别添加一条DMA通道即可参数保持默认即可 DMA模式和中断模式代码几乎一样只不过将发送和接收函数的后缀改成带DMA的即可发送和接收完成调用的回调函数和中断模式一样。 时钟树 多数情况下只需要在CubeMX里做这几步即可 CubeMX配置 进入RCC设置将其高速时钟源HSE设置为晶振Crystal/Ceramic Resonator 随后来到时钟设置Clock Configuration将HCLK修改为最高频率72MHz不同芯片型号可能不同敲下回车即可 定时器TIM 只需记住一句话定时器就是计数器定时器除了实现普通的定时功能还可以实现捕获脉冲宽度、计算PWM占空比、输出PWM波形、编码器计数等等各种功能... 在STM32的F10系列芯片中最多有8个定时器可以使用其中TIM6、7为基本定时器它们只有简单的定时功能以及一个触发DAC的功能TIM2~5称为通用定时器TIM1、8称为高级控制定时器一般用于电机领域。 基本定时功能 本小节先从最简单的定时功能入手如何进行计时 很简单只要有一个时钟源经过预分频器后再将脉冲送给计数器想要让预分频器进行n分频只需要将预分频器设置为n-1即可。 知道了如何计时那如何实现定时呢 这就要靠自动重装载寄存器了它的作用就是实时监控计数器的值是否与自己的值相同当计数器记到与自己相同的值时便将计数器的值重置为0并且可以触发一次定时器更新中断通知STM32定时时间到了。如果我们想定时m个脉冲就需要设置自动重装载寄存器的值为m-1。 来到CubeMX看看如何配置先打开串口使用什么模式随便用于发送数据然后设置RCC的高速外部时钟源为晶振打开时钟设置HCLK为72MHz这些都是之前设置过的这里不演示了这里需要补充一张图由下图可知STM32的外设接在了哪根线上这对于以后计算定时时间很关键 可以看到修改HCLK为72MHz后不同APB线的时钟频率 回到CubeMX点击Timers就可以看到当前芯片的所有定时器我们使用的F103C8T6芯片只有四个定时器不过仍然具有基本定时器的功能毕竟是通过基本定时器扩展出来的这里以TIM4为例可以看到很多复杂的功能但大多数和基本定时器无关只需要勾选Internal Clock内部时钟作为时钟源即可 如果是TIM2则是将Clock Source时钟源选择为Internal Clock 回到TIM4来到下面的详细配置假设我想定时1秒如何实现前面的图可知TIM4连接在APB1线上它的定时器时钟线为72MHz那么我可设置预分频器的值让72MHz变成10000Hz然后设置自动重装载值计数一万次就实现了定时一秒的效果 注意这里的两个值因为是从0开始因此-1按照上面这样设置计数器里的值从0数到9999的时候就会从0开始重新计数。我们先来了解一下定时器相关的函数然后来写代码 /* 基本定时器启动函数* 参数传入定时器句柄*/ HAL_TIM_Base_Start(TIM_HandleTypeDef *htim);/* 这个宏定义可以获得计数器的值* 参数也是定时器句柄*/ __HAL_TIM_GetCounter(TIM_HandleTypeDef *htim); 代码比较简单延时100msHAL_Delay函数有个小bug想要准确延时100ms参数需要填99后发送一次计数器值直接看结果 除了上面那个宏还有下面这几个宏 /* 设置计数器的值* 第一个参数为句柄,第二个参数为值*/ __HAL_TIM_SetCounter/* 获得重装载值* 只有一个参数,传入句柄*/ __HAL_TIM_GetAutoreload/* 设置重装载值* 句柄值*/ __HAL_TIM_SetAutoreload/* 设置预分频值* 句柄值*/ __HAL_TIM_SET_PRESCALER 知识点补充 预分频寄存器有个叫影子寄存器的结构真正工作的其实是影子寄存器我们通过 __HAL_TIM_SET_PRESCALER 宏将新的预分频值给到预分频寄存器后直到计数器的值归零后才会把新的预分频值给它的影子寄存器 自动重装载寄存器也有个它的影子寄存器我们设置完后也是在下个周期计数器的值归零后生效这样的好处是不会影响当前的计数周期。不过重装载寄存器可以自己控制是否开启影子寄存器机制默认是关闭影子寄存器的来到CubeMX这个选项设置成Enable就是打开 要注意的是如果我们没启动重装载寄存器的影子寄存器机制那么当我们将重装载寄存器的数值调小时可能会小于当前计数器的值这样的话计数器会数到最大值65535才会触发更新回到0开始下一次计数。 定时器更新中断 之前只打印计数值没做事回到CubeMX勾上这个就行了 启动函数以及回调函数如下 HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);//定时器周期结束回调函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);/* 模板如下 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if(htim htim4){} } */ 小小总结一下只要记住这张图就好了后续基于这个结构不断扩展 外部时钟与循迹模块 上小节基本定时功能的时钟源是来自时钟数上的APB时钟线它在STM32内部因此使用的是内部时钟我们还能对外部的信号来自GPIO口上的信号进行计数。 STM32并没有直接将GPIO口接入进来F103芯片上每个定时器本芯片只有TIM1~4有4个输入通道其输入的信号称为TI1到TI4TIM Input其中TI3和TI4并没有接入到触发控制器因此先忽略STM32为每个通道都配套了输入滤波器和边沿检测器边沿检测器可以检测输入信号的边沿并输出一个脉冲可检测上升沿、下降沿、双边沿对于通道1TI1来说其边沿检测器输出的脉冲信号有两个TI1FP1和TI1FP2通道2TI2也有两个TI2FP1和TI2FP2其中TI1FP2和TI2FP1后面才会用到因此先忽略TI1FP1和TI2FP2来到了触发控制器前另外通道1TI1上还有一根称为TI1_ED的信号也来到触发控制器前TI1FP1和TI2FP2可以选择三种边沿触发方式TI1_ED只能由双边沿触发产生脉冲这三个信号会连接到触发器即只能选择一个作为输入信号触发器又接入到触发控制器中一个叫做从模式选择器的结构除了来自通道1和通道2的这三根信号触发器还有一个独立的外部时钟信号外部触发器 ETRExternal TriggerETR经过极性选择、边沿检测、预分频以及输入滤波其中边沿检测只能检测上升沿但是极性选择可以将电平的极性翻转因此极性选择和边沿检测配合就能检测上升沿和下降沿ETR最终进入到触发器。被触发器选择后进入从模式控制器的这条路我们称之为外部时钟模式1而ETR除了能进入触发器外还能独立直接进入触发控制器注意不是从模式控制器这条独享的路称之为外部时钟模式2为何要如此设置后面会讲到。 外部时钟模式2 现在来到实战环节需要记录流水线传送带转动多远的距离以及传送带此时的速度因此引入了循迹模块/黑白线模块红外反射光电开关 蓝色小灯泡能发射红外光黑色小灯泡能接收红外光黑色小灯泡检测到红外强度后会通过AO引脚以模拟量的形式输出出来接收红外反射越强电压越低但这不是我我们需要的模块上还有一个变阻器上图左边的蓝色正方体可以借此调整此模块的检测阈值灵敏度即调整橙色线 当红外光强度大于阈值即电压小于橙线时模块上的小灯会亮起同时模块上的DO引脚会输出低电平红外光小于阈值时相反灯灭DO输出高电平 因此模块面前有可以反射红外光的物体时DO引脚输出低电平当模块面前没有物体或者有黑色物体吸收了红外光时DO引脚输出高电平所以假如我们在传送带边缘铺上一圈黑白间隔条纹运动起来时模块的DO引脚会输出方波信号借此就可以实现想要的效果来到CubeMX这次先不要设置HCLK为72MHz而是保持在8MHz打开Timers这次我们使用TIM2这是因为由于该芯片引脚有限TIM3、4没有引出外部触发器ETR的引脚进入TIM2的设置选择时钟源Clock Source为ETR2 下面的各个参数都保持默认即可滤波器很快会讲到预分频的存在是因为最终输入到触发器的ETR信号最快只能是内部时钟APB1/APB2频率的1/4因此通过这个分频器可以把速度降下来但是我们本次的输入信号非常慢手动用黑白条纹纸来模拟传送带运动因此不分频然后我们就可以来写代码了结果用串口输出还是OLED屏幕显示随意 main.c #include stdio.hint main(void) {/*虽然是外部时钟来触发定时器,但本质还是让定时器进行基础的计数功能*/HAL_TIM_Base_Start(htim2);int cnt 0;while(1){/*使用这个宏获得计数器值*/cnt __HAL_TIM_GetCounter(htim2);/*串口或屏幕显示,这里不演示了*/} } 可以看到成功计数了但是幅度不对这是因为黑白交接的边缘会出现抖动因此需要滤波 回到CubeMX详细参数的Clock Filter就是设置滤波器的绝大多数情况直接填15就行了 修改后再试试效果就正常了 现在来实现一开始要的效果测传送带的速度需要在CubeMX设置自动重装载值为10并且开启中断我直接给出代码作为参考重点是学习外部时钟 /* 头文件及宏定义 */ #include main.h #include oled.h // OLED显示库 #include tim.h // 定时器配置#define Period 10 // 定时器自动重装值(ARR)需与MX配置中的Counter Period一致 #define Width 1.5 // 每产生一次脉冲对应的物理宽度单位cm/* 全局变量声明 */ volatile uint32_t loop -1; // 定时器中断计数器初始化为-1以抵消首次中断// volatile确保中断和主循环中变量的同步 uint32_t lastCounter 0; // 上一次的脉冲计数值用于计算速度 uint32_t lastTime 0; // 上一次计算速度的时间戳单位ms char message[20] ; // OLED显示缓存/* 中断回调函数 */ // 定时器溢出中断回调函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if (htim htim2) { // 仅处理TIM2的中断loop; // 每次中断表示定时器溢出一次计满Period} }/* 主程序初始化 */ int main(void) {HAL_Init(); // 初始化HAL库SystemClock_Config(); // 配置系统时钟MX_TIM2_Init(); // 初始化TIM2定时器HAL_Delay(20); // 短暂延时确保硬件稳定OLED_Init(); // 初始化OLED屏幕// 启动定时器中断TIM2HAL_TIM_Base_Start_IT(htim2);/* 主循环 */while (1) {OLED_NewFrame(); // 开始OLED新帧绘制// 计算总脉冲数 溢出次数 * Period 当前计数值uint32_t counter loop * Period __HAL_TIM_GET_COUNTER(htim2);// 显示脉冲计数sprintf(message, Counter: %lu, counter);OLED_PrintString(0, 0, message, font16x16, OLED_COLOR_NORMAL);// 计算速度基于时间差uint32_t currentTime HAL_GetTick(); // 获取当前时间戳msfloat deltaTime (currentTime - lastTime) / 1000.0f; // 转换为秒if (deltaTime 0.05f) { // 至少间隔50ms计算一次避免除零错误float speed (counter - lastCounter) * Width / deltaTime;lastCounter counter; // 更新上一次计数值lastTime currentTime; // 更新上一次时间戳// 显示速度sprintf(message, Speed: %.1fcm/s, speed);OLED_PrintString(0, 20, message, font16x16, OLED_COLOR_NORMAL);}OLED_ShowFrame(); // 刷新OLED显示HAL_Delay(50); // 主循环延时降低CPU负载} } 外部时钟模式1 外部时钟模式1如何使用回到CubeMX我们知道外部时钟模式1的触发器是接入到触发控制器中一个叫做从模式控制器的东西上因此我们要使用从模式才能使用外部时钟模式1 关掉TIM2的时钟源Clock Source选项选择从模式为外部时钟模式1External Clock Mode 1然后就可以选择使用触发器的哪一条路输入了Trigger Source 选择ETR1 选择TI1_ED 只能配置滤波器了还记得吗TI1_ED只能是双边沿触发的产物 不管白到黑还是黑到白都会计数 选择TI1FP1 与TI1_ED相比多了一个可以选择什么边沿触发上、下、双 定时器从模式 上小节说到了要想使用外部时钟模式1就需要用到从模式将从模式设置为外部时钟模式1本小节来探究一下设置从模式时的另外三个选项是什么 外部时钟模式1的功能是给定时器提供计数的信号而这三种模式的功能则是控制定时器的工作状态。 复位模式 Reset Mode 它可以对定时器的计数状态进行复位假设从模式被配置成复位模式由于从模式控制器不再是外部时钟模式1而是复位模式也就不能为定时器提供计数信号所以我们需要使用另外的时钟源如果单纯的计时可以使用内部时钟想对外部信号进行计数也可以使用外部触发器ETR通过外部时钟模式2接入定时器这就是为何ETR可以独辟蹊径跳过触发器直接接到触发控制器的原因即在从模式控制器被占用时还可以从ETR引入外部信号。         回到正题假设我们这是使用内部时钟源预分频器设置为0自动重装载寄存器设置为5另外从模式的触发源选择为TIFP1上升沿触发那么在定时器正常的计数过程中0~5如果TI1输入了一个带上升沿的信号就会从TI1FP1输出一个脉冲进入到从模式控制器此时从模式控制器就会执行复位模式的功能将定时器进行复位所谓复位操作其实与自动重装载一样也是进行更新事件计数器清零、设置对应的影子寄存器、触发定时器更新中断如果开启了中断。 来到CubeMX按照我们上面所说的来配置内部时钟源的频率为8MHz我并没有修改HCLK为72MHz因此定时事件是用8MHz来计算的每5s发生一次中断 我们来写代码 #include string.h #include stdio.hvoid HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if(htim htim2){/*触发了定时器更新中断后打印字符串自动重装载*/} }int main(void) {HAL_TIM_Base_Start_IT(htim2);int cnt 0;while (1){cnt __HAL_TIM_GetCounter(htim2);/*打印cnt,此处省略*/HAL_Delay(500);} } 可以看到正常是计数到接近4999会触发一次更新中断在计数过程中使用循迹模块引入一个上升沿就可以看到才技术到1300多就触发了更新中断 这里引入一个问题如何区分是复位模式还是自动重装载其实就像定时器更新中断有一个更新中断标志位一样从模式控制器在接收到触发信号后还会设置一个触发器中断标志 回到代码中的回调函数优化一下 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if(htim htim2){//自行判断触发中断标志位if(__HAL_TIM_GET_FLAG(htim, TIM_FLAG_TRIGGER) ! RESET){//手动清除标志位__HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_TRIGGER);/*触发了触发器中断后打印字符串从模式触发*/}else{/*触发了定时器更新中断后打印字符串自动重装载*/}} } 可以看到由从模式的复位模式而引发的更新中断会打印从模式触发 门模式 Gated Mode 保持上面的设置都不变只不过从模式设置成门模式当输入信号为高电平严格讲是检测到上升沿时门就打开时钟信号可以进入到定时器定时器正常计数当输入信号为低电平检测到下降沿时门就关闭定时器暂停计数输入通道的边沿检测器就可以改变高低电平对门的开关控制。 来到CubeMX将从模式改为门模式其他无需修改这里需要注意的是门模式下控制信号出现上升或下降沿从模式控制器就会暂停或继续定时器计数这两个边沿的时刻会将触发器中断标志位置1但和复位模式不同只是将标志位置1并不复位计数器值也不触发定时器更新中断不调用回调函数因此代码这样写 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {if(htim htim2){/*触发了定时器更新中断后打印字符串自动重装载*/} }int main(void) {HAL_TIM_Base_Start_IT(htim2);int cnt 0;while (1){if(__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_TRIGGER) ! RESET){__HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_TRIGGER);/*触发了触发器中断后打印字符串从模式触发*/}cnt __HAL_TIM_GetCounter(htim2);/*每500ms打印cnt*/HAL_Delay(500);} } 当循迹模块输出低电平时由高变低从模式触发并且一直输出一个数证明计数器暂停了 变为高电平后计数继续并且也将触发中断标志位置1 触发模式 Trigger Mode 它的作用就是检测到设定的边沿后让定时器开始计数不需要改代码和门模式的代码一样直接来看效果 可以看到定时器没有默认启动计数所以一直输出0 循迹模块输出上升沿后从模式的触发模式触发开始计数 后面再输入上升沿后虽然也会打印从模式触发但定时器不会停止 因此触发模式仅能启动定时器计数并不能停止所以有时触发模式会配合单脉冲模式一起使用 所谓单脉冲模式就是定时器不再循环计数而是计数到自动重装载值后便停止计数代码不需要改直接看现象 上升沿启动技术后5s触发自动重装载然后计数值清零上升沿再触发计数循环往复。 知识点补充定时器上电自动触发一次中断 注意到每次程序重新启动或者复位时都会打印一次自动重装载也就是触发了定时器中断进入了回调函数 这是因为定时器初始化函数 MX_TIM2_Init(); 在初始化时会将定时器更新中断标志位置1后面调用启动函数自然就会触发中断如果影响到了你可以这样做 int main(void) {MX_TIM2_Init();/* USER CODE BEGIN 2 *//*这两个函数任选一个即可,内部实现是一样的*/__HAL_TIM_CLEAR_FLAG(htim2, TIM_FLAG_UPDATE);__HAL_TIM_CLEAR_IT(htim2, TIM_IT_UPDATE);//在调用定时器启动函数之前清除更新中断标志位即可HAL_TIM_Base_Start_IT(htim2);while(1){/*-----------*/} } 输入捕获超声波测距 超声波测距模块 该模块是测距常用的传感器注意供电电压其原理是发出一定频率的超声波遇见被测物体后反射回来如何测距只需要将发送时刻 - 接收时刻x 声速 / 2 即可以HC-SR04模块为例它有一个控制端Trig以及一个输出端Echo当需要测量时只需要通过GPIO口向Trig引脚发送一个脉冲信号接收到脉冲信号后就会将Echo拉高并发出超声波当接收到反射回来的超声波后模块会将Echo拉低我们测量Echo引脚高电平的持续时间就是超声波往返所消耗的时间。 操控Trig非常简单 测量Echo高电平持续时间也很简单 但这肯定不是本节课要使用的方法测量高电平读取时间浪费了太多CPU资源。 输入捕获 一句话概括其功能当定时器输入通道上检测到上升沿或者下降沿时立刻将此时计数器的数值记录到捕获寄存器中以待程序稍后读取。 我们来了解一下它的机制见下面这张图隐掉一些与输入捕获无关的线路时钟源保留内部时钟源输入捕获的关键就是捕获寄存器对于通用和高级定时器来说每个输入通道都有它自己的捕获寄存器TI1FP1经过一个预分频器后连接到捕获寄存器1上预分频器可以进行/2 /4 /8分频TI2FP2同理 假设我们启动通道1TI1的输入捕获模式设定为上升沿捕获若TI1捕获到上升沿捕获寄存器1会立刻将计数器的值复制到自身这很好理解并且如果我们还为此输入捕获开启了中断就还会触发输入捕获中断通知程序尽快读取那么只要再获取到下降沿出现时定时器的时刻两者相减就可以知道时间了不过一个通道的输入捕获只能捕获上升或下降沿         能不能将信号同时输入到通道1和通道2然后一个捕获上升沿一个捕获下降沿难道要在模块的Echo引脚引出两根线         别人早帮我们考虑好了STM32又从TI1和TI2上分别引出一条线连接到对方这两根线就是我们之前按下不表的TI1FP2和TI2FP1如果信号从TI1的GPIO口引入则输入通道1TI1叫做输入捕获的直接模式TI2叫做间接模式反之同理 一直被我们忽视的TI3和TI4也是一对拥有和TI1与TI2一模一样的结构唯一的区别是TI3FP3和TI4FP4没有接入到从模式控制器中从FP后面的数字就可以看出谁和谁是一对的总体如下 话不多说来到CubeMX超声波模块的Trig引脚我接到了PA11Echo引脚接到了PA10首先将PA11设置成GPIO的推挽输出 点击PA10发现它对于TIM1的通道3 因而来到TIM1 然后来到下面的详细设置在此之前我设置了HCLK为72MHz高速外部时钟HSE为晶振为了方便计算定时器的分频值为72分频 我们需要用到定时器的输入捕获中断来到NVIC选项卡开启TIM1捕获/比较中断 搞定后生成代码然后我们先来认识输入捕获相关的函数和回调函数 /* 输入捕获启动函数* 第一个参数是定时器句柄* 第二个参数是定时器的哪个输入通道?* 输入通道:TIM_CHANNEL_1~4*/ HAL_TIM_IC_Start(TIM_HandleTypeDef *htim, uint32_t Channel);//中断模式的启动函数 HAL_TIM_IC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel);//定时器输入捕获回调函数 给出模板 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {//养成习惯判断是哪个定时器触发的回调函数//这里还要判断是哪个通道触发的输入捕获回调函数//htim的Channel属性,每次进入中断回调之前都会被重新赋值//赋值:HAL_TIM_ACTIVE_CHANNEL_1~4if(htim htim1 htim-Channel HAL_TIM_ACTIVE_CHANNEL_4){} } 特别注意的是htim句柄的Channel属性和启动函数的Channel不同  main.c /* USER CODE BEGIN 0 */ int upEdge 0; int downEdge 0; float distance 0;void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {if(htim htim1 htim-Channel HAL_TIM_ACTIVE_CHANNEL_4){upEdge HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_3);downEdge HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_4);//1us计数值1//(downEdge - upEdge)就是具体多少us//声速340m/s转化为0.034cm/us//因为是往返,所以÷2//最后的单位是cmdistance ((downEdge - upEdge) * 0.034) / 2;} } /* USER CODE END 0 */int main(void) {//没有用到定时器更新中断,因此不需要IT后缀HAL_TIM_Base_Start(htim1);//启动输入捕获的函数//通道3捕获到上升沿,还不急着读HAL_TIM_IC_Start(htim1, TIM_CHANNEL_3);//通道4捕获到下降沿后,就要读了,因此带IT后缀HAL_TIM_IC_Start_IT(htim1, TIM_CHANNEL_4);while (1){HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_SET);HAL_Delay(1);HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET);//模块启动后将计数器的值清零,避免捕获完上升沿后直接重装载了__HAL_TIM_SetCounter(htim1, 0);/*等待模块测量*/HAL_Delay(20);/*通过串口或者OLED屏幕显示出来*///防止测量过快HAL_Delay(500);} } 输出比较PWM脉冲宽度调制 PWM PWM信号就是方波一组高低电平所占用的时间为周期周期倒数为频率占空比为高电平在一个周期里所占的比例方波的占空比为50% PWM就是一种用数字信号尽可能地模式模拟信号的技术 那么如何输出PWM波用输出比较模式 输出比较模式 回到定时器框图时钟源还是选择内部时钟源并隐掉不相干的线路在输出比较模式下捕获寄存器摇身一变改名成了比较寄存器用于输入脉冲信号的GPIO口也变为了输出脉冲信号这里以通道1为例在输出比较模式下我们要首先向比较寄存器中写一个数值然后定时器会一直比较计数器值与比较寄存器数值的大小关系根据此大小关系来决定输出有效电平还是无效电平有这么几种模式 冻结模式 这种模式下输出通道的GPIO口不理会比较结果维持旧的输出状态 强制有效 不理会比较结果强制输出通道输出有效电平 强制无效 与上面相反强制输出无效电平 匹配时有效 当计数器值与比较寄存器完全相等时输出有效电平如果已经是有效电平就继续维持 匹配时无效 相等时输出无效电平 匹配时翻转 顾名思义这个模式就可以输出占空比50%的PWM波 如果要输出任意占空比的PWM信号要使用专门为此设计的PWM模式此模式也有两种 但这是基于计数器向上计数模式下还有向下计数和中央对齐模式但只做了解即可一般都是用向上计数模式还有我一直说的有效电平和无效电平STM32在输出模式最后加了一个输出控制器它的作用就是设置输出模式输出有效电平时对应GPIO口为何种电平一般都设置成高电平为有效电平低电平为无效电平。 一言以蔽之PWM模式时生成PWM信号的关键设定一个自动重装载值让计数器从0数到自动重装载值周而复始也就确定了PWM的周期和频率而给某个通道的比较寄存器设定一个值PWM模式让此通道在计数器小于比较寄存器时输出一种电平大于时输出另一种电平通过调节比较寄存器值的大小也就能控制PWM的占空比。 接下来就来到CubeMX产生PWM信号并不断调节占空比实现呼吸灯的效果我的三色LED分别接在了PA6、PA7、PB0引脚它们分别对应着TIM3的通道1、2、3因此来到TIM3的配置界面时钟源选择内部时钟通道1的功能设置为输出比较Output Compare CH1下方的详细配置可以选择是冻结、匹配时有效、强制有效等等没啥用的模式 来到本小节重点通道1设置成PWM模式 详细设置如下在此之前设置HCLK为72MHz因此APB1的时钟线也是72MHzTIM3连接在APB1上因此用72MHz来计算 一切就绪后生成代码先来看看PWM的相关函数 /* PWM启动函数* 第一个参数是定时器句柄* 第二个参数是哪个通道:TIM_CHANNEL_1~4*/ HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);/*PWM停止函数*/ HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);/* 设置比较寄存器的值* 前两个参数就不讲了* 第三个参数为值*/ __HAL_TIM_SetCompare(htimx, TIM_CHANNEL_x, x); 来看看实现呼吸灯的代码 int main(void) {//这样PWM就被开启了,按照我们的设置,0.1ms为周期,每秒生成10000此占空比为50%的PWM波HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);while (1){for(int i 0; i 100; i){__HAL_TIM_SetCompare(htim3, TIM_CHANNEL_1, i);HAL_Delay(10);}for(int i 99; i 0; i--){__HAL_TIM_SetCompare(htim3, TIM_CHANNEL_1, i);HAL_Delay(10);}} } 占空比比较寄存器的值/自动重装载寄存器的值1  旋转编码器 最常见的就是增量型旋转编码器 一般有A、B两相输出信号顺时针旋转时B相方波领先A相90度即A相上升沿时B相高电平A相下降沿时B相低电平逆时针旋转反过来 我们就可以通过计数A相或者B相上升沿或下降沿的数量获得编码器旋转角度还能根据A相边沿时B相的电平情况得知旋转方向。 对于增量式旋转编码器的使用一个简单的思路是将A、B相信号接入到GPIO口后将A相GPIO口设置为上升沿或下降沿触发中断在中断回调函数里读取B相GPIO口的电平来判断旋转方向并且根据旋转方向对计数值1或者-1来记录脉冲数量知道每个脉冲的旋转角度就可以知道转了多少角度。 上面那种方法固然可以但如果旋转速度过快就会频繁调用回调函数其实通用/高级定时器为增量型编码器准备了专门的编码器接口只要将A、B两相信号同时输入进去就可以实现正转时计数器自增反转时计数器自减这里要注意编码器接口对上升沿和下降沿都敏感所以对于一组脉冲会计数两次。那编码器接口是哪里呢其实就是我们早已了解的TI1FP1和TI2FP2 来到CubeMX在此之前先看看旋转编码器的原理图 A相和B相分别接到了PA8和PA9TIM1的通道1和通道2编码器不止能旋转还能按下引脚为PB15 来到TIM1因为是记编码器的脉冲数所以不需要设置时钟源找到组合通道Combined Channels设置选择为编码器模式Encoder Mode 来到下面的详细设置注意这里的Encoder Mode可选TI1或TI2也可以两个通道都计数但记住我之前说的编码器接口对上升沿和下降沿都敏感如果选择两个通道都计数那么每次脉冲计数值会4 如果要使用编码器的按键功能记得看原理图有没有上拉电阻如果没有要用GPIO的上拉输入好了我们就来实现旋转编码器调节LED灯的亮度功能假设我的小灯接在了PA6即TIM3的通道1来复习一下PWM模式的设置TIM3使用内部时钟源通道1使用PWM模式预分频为72-1自动重装载值为100-1其他保持默认比较寄存器的值在代码里写这里可以先不动 完成后来写代码先认识一下编码器模式的函数 /*编码器启动函数*/ /*对于我们这种有A、B两相的编码器,第二个参数填TIM_CHANNEL_ALL表示启动所有通道*/ HAL_TIM_Encoder_Start(TIM_HandleTypeDef *htim, uint32_t Channel); 我们先看看编码器旋转和计数器数值之前的改变关系代码如下 int main(void) {int cnt 0;HAL_TIM_Encoder_Start(htim1, TIM_CHANNEL_ALL);while (1){cnt __HAL_TIM_GetCounter(htim1);/*用串口或OLED屏幕显示出来*/HAL_Delay(100);} } 初始值为0顺时针旋转后值变成了65534继续顺时针旋转数值会不断变小且步长为2逆时针旋转数值变大65534后变回0然后不断变大想要更符合常理回到CubeMX很简单 对于计两次数的问题直接进行一个2分频就行了预分频值为2-1 计数改变和旋转方向的问题我们改变TI2的极性就可以了让TI2的波形翻转 问题完美解决来实现旋转编码器控制LED亮灭程度吧 int main(void) {int cnt 0;HAL_TIM_Encoder_Start(htim1, TIM_CHANNEL_ALL);HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);while (1){cnt __HAL_TIM_GetCounter(htim1);//如果计数值大于60000,肯定反转小于0,因此反转限制为0if(cnt 60000){cnt 0;__HAL_TIM_SetCounter(htim1, 0);}//正转也限制为100,因为要根据计数值来设置占空比else if(cnt 100){cnt 100;__HAL_TIM_SetCounter(htim1, 100);}//自动重装载值为99,占空比为cnt/(991)__HAL_TIM_SetCompare(htim3, TIM_CHANNEL_1, cnt);/*用串口或OLED屏幕显示cnt即可*/HAL_Delay(100);} } ADC模拟-数字转换技术 STM32使用是逐次逼近法SAR通过不断进行二分比较最终确定电压值的方法STM32F103是12位分辨率的ADC即最终结果是以12个二进制位存储0~4095其中0代表0V4095代表参考电压的值一般为3.3V得到测量后的12位二进制数据后将其除以4095再乘以参考电压3.3V便可以知道测得的电压值。 在STM32中有16个GPIO口可以进行电压值的采样工作称其为16个ADC采样通道不过STM32F103C8T6只有前10个通道另外还有两个内部通道用于采集STM32内部提供的电压值一个连接在芯片内部的温度传感器上一个连接在内部参考电压上。 有两个用于转换的ADC结构ADC1和ADC2每个ADC中有注入组和规则组注入组我们先忽略规则组可以理解为普通组它就像一个用于注册排队的表格我们将某个ADC通道注册在上面当我们触发ADC时ADC就会对此通道进行采样、转换转换结果放入规则通道数据寄存器中等待程序读取我们甚至可以注册多个通道让ADC依次进行转换这个在下小节来讲 话不多说来到CubeMX假设我们有一个滑动变阻器原理图如下我们来测量它的电压值 点击PA5发现它可以设置为ADC1或ADC2的通道5我们选择ADC1 单次转换模式 找到ADC1的设置打开通道5其他先不动 发现时钟设置里出现了错误这是因为ADC1、2都是依靠APB2的时钟线并且频率不宜过快在F103上不要超过14MHz因此将ADC专用的分频器改为/6 先来认识ADC的相关函数 /* ADC启动函数*/ HAL_ADC_Start(ADC_HandleTypeDef* hadc);/* 读取ADC值*/ HAL_ADC_GetValue(ADC_HandleTypeDef* hadc);/* 等待转换完成的函数* 一直轮询检查是否转换完成* 第二个参数是超时时间,一般传入HAL_MAX_DELAY表示无限等待*/ HAL_ADC_PollForConversion(ADC_HandleTypeDef* hadc, uint32_t Timeout);/* ADC校准函数* 建议在每次上电后执行一次校准*/ HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef* hadc); 好了来写代码十分简单 int main(void) {int value 0;float voltage 0.0;HAL_ADCEx_Calibration_Start(hadc1);while (1){HAL_ADC_Start(hadc1);HAL_ADC_PollForConversion(hadc1, HAL_MAX_DELAY);value HAL_ADC_GetValue(hadc1);voltage (value / 4095.0) * 3.3;/*串口或屏幕显示出来*/HAL_Delay(500);} } 上面的程序是靠HAL_ADC_Start函数触发一次采样与转换等待转换完成后读取测量结果其实STM32的ADC还为我们提供了一种连续转换功能。 连续转换模式 开启此功能后我们只需要触发一次ADCADC在完成一次转换后会马上进行下一次的采样转换工作持续不断更新寄存器里的测量结果随时读取即可 开启方法也很简单来到CubeMX开启持续转换模式即可 来看看代码有什么不同把启动函数和等待转换完成函数放在循环前即可 结果如下 ADC多通道采集功能 本小节来同时采集电位器滑动变阻器、NTC热敏电阻、芯片内部温度、芯片内部参考电压。我们之前说过ADC的规则组好像一个用来注册排队的表格当时我们在ADC1的规则组上只注册了通道5检测电位器的电压值当我们启动一次ADC规则组就会对通道5进行一次采样转换转换完成后结果放进规则通道数据寄存器中并且将ADC状态寄存器的转换结束标志位置1。当我们调用HAL_ADC_GetValue函数读取寄存器时转换结束标志位会被自动置0以待下一次转换如果我们还开启了连续转换功能就可以只启动一次ADCADC就不断对通道5进行采样转换。 本小节我们会将通道5通道4内部温度传感器通道内部参考电压通道都注册到规则组上来并且开启ADC的扫描模式这样每次触发ADC测量时ADC就会先采样转换通道5将结果放在规则通道数据寄存器中紧接着采样转换通道4转换完通道4结果也放入规则通道数据寄存器中紧接着转换内部温度传感器通道同样的最后转换内部参考电压通道通过ADC的扫描模式我们就可以在一个ADC上连续测量多个通道的电压值 但有一个问题是每个通道的数据转换完成后程序可能还没来得及读取下一个通道的转换就完成了导致旧数据被覆盖而且程序也不好区分当前从数据寄存器中取出的数据到底是哪个通道的转换结果因此需要用到DMA我们可以告诉DMA我们有一个大小为4的数组要将规则组中的数据依次搬运到此数组中这样每次转换完成转换结束标志位被置1时转换结束事件就会通知DMA进行搬运如果我们给ADC开了连续转换模式还可以给DMA设置循环模式这样的话ADC连续不断地依次对四个通道进行转换DMA也同时依次循环搬运转换数据到数组中形成配合。 单次转换模式 话不多说来到CubeMXNTC电阻的原理图如下接在了PA4引脚也是ADC1的通道4 来到ADC1的设置打开这四个通道 来到下面的详细设置 内部参考电压的存在是因为我们不能保证参考电压一定稳定在3.3V所以STM32在内部提供了一个一直输出1.2V的内部参考电压然后我们测得内部参考电压通道的ADC值与1.2V进行计算就能得出真正的参考电压 根据芯片手册内部参考电压的采样时间典型值是5.1us按照我们设置的ADC时钟周期为12MHz5.1us就是61.2个周期因此采样周期我都设置成了71.5个周期。为了使用DMA搬运来到DMA选项卡 保持默认即可每次传输的数据宽度为半字即16位我们是12位分辨率的ADC因此16位足够了一切就绪生成代码我们先打印这四个通道的ADC值看看行不行的通 #include stdio.huint16_t values[4]; char mes[50] ;//ADC转换完成回调函数 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {if(hadc hadc1){sprintf(mes, %d %d %d %d, values[0], values[1], values[2], values[3]);//串口打印} }int main(void) {//校准HAL_ADCEx_Calibration_Start(hadc1);while (1){//第三个参数是要搬运多少次HAL_ADC_Start_DMA(hadc1, (uint32_t *)values, 4);/*假设这里有其他任务*/HAL_Delay(500);} } 成功打印 除了在循环里触发转换然后等待中断回调我们也可以像之前一样让其持续转换转换完一遍后马上进行下一轮转换 连续转换模式 回到CubeMX将连续转换模式开启 还记得组合拳吗来到DMA选项卡将DMA模式改为Circular循环模式 这样内存地址在自增时到了最后一个后就会回到开头就能和持续模式配合起来生成代码试试因为在一直转换我们可以随时取数据所以就不需要中断回调函数了 #include stdio.hint main(void) {//校准HAL_ADCEx_Calibration_Start(hadc1);//第三个参数是要搬运多少次HAL_ADC_Start_DMA(hadc1, (uint32_t *)values, 4);uint16_t values[4];char mes[50] ;while (1){sprintf(mes, %d %d %d %d, values[0], values[1], values[2], values[3]);//串口打印HAL_Delay(500);} } 运行结果是一样的。 RTC实时时钟 RTC的结构非常像一个简化版的定时器核心当然是一个计数器与定时器的16位计数器只能从0计数到65535不同RTC的计数器是32位的可以从0技术到4294967295RTC的计数器前还有个RTC预分频器可以将时钟源的时钟信号分频为1Hz的信号我们往往会采用unix时间戳的形式在RTC计数器中记录时间unix时间戳是一种在计算机领域通用的时间表示方式其记录的是从1970年1月1日0时0分0秒到当前时刻的秒数一些使用有符号32位整数记录unix时间戳的系统其仅能计数到2038年随后就会溢出不过我们的RTC计数器无需符号位可以从1970年记录到2106年。 我们通过将3.3V接入VDD引脚为其供电以执行我们的代码程序而VDD掉电后程序便无法继续执行。那RTC时钟如何保持掉电后继续走时呢STM32还为我们提供了VBAT引脚即使VDD已经掉电只要我们继续为VBAT引脚供电STM32就可以维持芯片上一块叫做后备区域的地方继续运行后备区域内的功能比较简单耗电很少因而我们就可以在PCB电路板上板载一颗纽扣电池为其供电而RTC就在这后备区域中。 接入到RTC的时钟源有三种LSE低速外部时钟、LSI低速内部时钟、HSE高速外部时钟的128分频。但只有LSE可以在VDD掉电后继续提供时钟信号因而通常选择低速外部时钟LSE作为RTC时钟信号。其频率为32.768KHz后经分频器分频为1Hz。另外STM32的RTC上还有简单的闹钟功能往闹钟寄存器上设置一个时间戳计数器与闹钟寄存器数值相等时触发对应的闹钟中断。 除了RTC时钟后备区域内另一个主要功能叫做备份寄存器在我们使用的STM32F103C8T6芯片中有10个16位的备份寄存器可以用于存储数据这些寄存器在VDD掉电后依旧靠VBAT维持因而可以掉电不丢失并且配有入侵检测功能产品可以设计使得外壳被开启后给予入侵检测引脚信号即使是设备外部供电已经断掉备份寄存器中的数据也能被清除适合防止重要数据。此外由于均是VBAT供电VBAT掉电后RTC与备份寄存器中的数据均会丢失因而备份寄存器也可以用来检测RTC时钟是否被设置过或者时间是否因为VBAT掉电而丢失。 为了兼容F407、H743等具有独立日期寄存器的芯片HAL库的RTC时钟部分将日期与时间分开处理导致未将日期数据记录在RTC计数器中从而使得日期数据会在VDD掉电后丢失另外默认生成的代码每次都会初始化RTC初始化过程中会使得RTC停止运行一小会导致每次程序重启时RTC时间会变慢一点因而本小节我们会写一个自己的RTC时钟库方便日后使用。 话不多说来到CubeMX将Debug模式设置为Serial WireRCC里设置高速外部时钟HSE源和低速外部时钟LSE源为晶振 点开Timer就可以看到RTC的设置只需要勾选Activate Clock Source就可以将其开启 如果勾选Activate Calendar那么在下方的详细设置还可以直接设置一个时间年月日时分秒不过我们要自己写库因此不需要RTC OUT可以将RTC的信号输出到GPIO口上Tamper是刚刚提到的入侵检测功能的开关看到下面的详细设置第一个是数据格式我们自己写库因此无需理会Auto Predivider Calculation自动计算预分频器要开启如果关闭了在下方的Asynchronous Predivider value需要我们自己计算填写预分频器的值Output是与上面的RTC OUT功能配合的用于选择将哪种信号输出到GPIO口无需理会。 来到时钟设置将主频设置为72MHz左上角是对RTC时钟的设置为了RTC的断电走时我们需要将其修改到LSE低速外部时钟 一切就绪后生成代码在工程文件夹的Core目录下的Inc文件夹和Src文件夹里新建myrtc.h和myrtc.c文件然后在keil5里添加文件 先不急着直接写代码我们要在HAL库的基础上进行改进因此先来看看HAL库是怎么写的 HAL库RTC库 来到stm32f1xx_hal_rtc.c文件在704行找到HAL_RTC_SetTime函数在下方还有一个设置日期的函数HAL_RTC_SetDate因而HAL_RTC_SetTime函数不涉及年月日数据在其代码中也可以印证这点 其将时分秒数据单位均换算为秒并相加随后在内部调用了RTC_WriteTimeCounter函数这就是用来设置RTC的计数器的函数 我们自己的库也要设置RTC的计数器因此点击这个函数的定义看看其内部实现 为什么要进入和退出初始模式根据芯片手册得知 必须要设置RTC_CRL寄存器中的CNF位才能对RTC的计数器进行写操作并且如果之前的写操作未完成就不能进行下一次写操作可以对RTC_CR寄存器中的RTOFF状态位进行查询来确认上一次写操作是否完成。 因此我们自己的RTC库要写设置时间函数的话首先是将年月日时分秒信息转化为unix时间戳然后调用RTC_WriteTimeCounter函数设置RTC的计数器为此时间戳这个函数被static关键字声明了因此在我们的.c/.h文件里无法调用后面我们直接复制过来就行了。 自己实现RTC库函数 先将进入与退出初始模式的函数复制到myrtc.c中然后是RTC_WriteTimeCounter写计数器的函数顺便将其上面的读计数器的函数RTC_ReadTimeCounter也复制过来在myrtc.h里引用stm32f1xx_hal.h和rtc.h解决报错现在我们来写自己的时间设置函数仿照HAL库的写法 我们的返回值也为HAL_StatusTypeDef第一个参数由于我们在myrtc.h中包含了rtc.h这里边就声明了这个指针因此我们直接使用就行不需要传入指针句柄后面两个参数是HAL库自己定义的时间结构体以及时间格式我们使用C语言提供的用于表示日期和时间的结构体tm比较好在myrtc.h中包含time.htm结构体内有这些成员变量 我们要做的事很简单将传进来的tm类型的参数转换为32位的unix时间戳然后将其设置到RTC计数器就好了即RTC_WriteTimeCounter函数关于将tm类型转换为unix时间戳的方式time.h里帮我们提供了函数调用mktime函数即可完成转换传入参数也是struct tm的指针类型然后返回写计数器的函数就好了 HAL_StatusTypeDef sakabu_RTC_SetTime(struct tm *time) {uint32_t unixTime mktime(time);return RTC_WriteTimeCounter(hrtc, unixTime); } 接下来写读取RTC时间的函数返回值为tm的指针没有入参思路很简单读取RTC计数器中的时间戳转换为tm类型即可读取RTC的函数我们已经复制过来了RTC_ReadTimeCounter接收变量的类型需要注意一下需要使用time.h提供的time_t64为位而不是uint32_t然后就可以使用time.h的时间戳转换为结构体tm的函数gmtime进行转换其接收time_t类型的指针 struct tm* sakabu_RTC_GetTime(void) {time_t unixTime RTC_ReadTimeCounter(hrtc);return gmtime(unixTime); } 在myrtc.h中声明就可以来写代码试试效果了 #include myrtc.h #include stdio.h #include string.hint main(void) {char mes[50] ;struct tm *now;struct tm time {//年要存储的是年份和1900年的差值.tm_year 2025 - 1900,//2025//月份的取值是0~11,代表1到12月.tm_mon 1-1,//1月.tm_mday 1,.tm_hour 23,.tm_min 59,.tm_sec 55,};sakabu_RTC_SetTime(time);while (1){now sakabu_RTC_GetTime();sprintf(mes, %d-%d-%d %02d:%02d:%02d,now-tm_year 1900, now-tm_mon 1, now-tm_mday,now-tm_hour, now-tm_min, now-tm_sec);/*用串口输出*/HAL_Delay(1000);} } 运行效果和我们预期的一样 不过每次复位都会重新从1月1号23点59分55秒开始计时这就需要用到我们之前说的备份寄存器实现掉电不丢失VDD掉电后由VBAT引脚供电我这里用的是纽扣电池。思路如下我们可以在初始化设定时间时同时往一个备份寄存器中写一个数据每次启动代码时检查此备份寄存器中是否有我们写入的数据如果是说明已经设定过时间就跳过此步骤否则便设置时间并写此备份寄存器 回到myrtc.c写一个RTC初始化函数sakabu_RTC_Init这里提前预知一个bug就是一直复位的话RTC会停止运行这也是之前说过的HAL库的bug是因为每次复位后运行的MX_RTC_Init函数中的这一句 其在初始化RTC的过程中会值RTC停止运行一小会解决思路和之前一样只要初始化过RTC后就不用每次都初始化了 #define RTC_INIT_FLAG 0xAAAAvoid sakabu_RTC_Init(void) {//读取备份寄存器的函数,10个寄存器uint32_t initFlag HAL_RTCEx_BKUPRead(hrtc, RTC_BKP_DR1);if(initFlag RTC_INIT_FLAG) return;//没有设置过RTC才执行这一句if (HAL_RTC_Init(hrtc) ! HAL_OK){Error_Handler();}struct tm time {//年要存储的是年份和1900年的差值.tm_year 2025 - 1900,//2025//月份的取值是0~11,代表1到12月.tm_mon 1-1,//1月.tm_mday 1,.tm_hour 23,.tm_min 59,.tm_sec 55,};sakabu_RTC_SetTime(time);HAL_RTCEx_BKUPWrite(hrtc, RTC_BKP_DR1, RTC_INIT_FLAG); } 最后修改一下CubeMX自动生成的初始化函数MX_RTC_Init在rtc.c中先添加我们自己的头文件 然后这样修改把原本的初始化函数移到BEGIN RTC_Init 0注释对中再调用我们自己写的sakabu初始化函数直接return跳过原本的代码即可 后续会持续更新。。。希望大家点赞支持一下
http://www.w-s-a.com/news/681714/

相关文章:

  • 长春网站建设net企业公示信息查询官网
  • 金鹏建设集团网站可在哪些网站做链接
  • 电子产品网站开发背景网站关键词优化方案
  • 建网站论坛wordpress提交数据库错误
  • 国内网站建设公司开源网站系统
  • 网站开发公司上大连网站建设流程图
  • 银川网站seo宁波网
  • 个人备案网站会影响吗网站添加 备案
  • 网站建设与电子商务的教案关于旅游网站建设的方案
  • 电子商务网站建设设计原则找做网站找那个平台做
  • 天津高端品牌网站建设韶关网站建设墨子
  • Wordpress多站点为什么注册不了2008iis搭建网站
  • 天津高端网站制作建网站的公司服务
  • 温州网站推广优化类似淘宝的网站怎么做的
  • 网站建设实训考试什么网站做玩具的比较多
  • 上海网站建设特点怎样给公司做一个网站做推广
  • 流量网站怎么做的济南优化排名公司
  • 保定网站制作套餐设计师导航网站大全
  • 惠州 商城网站建设石家庄新闻广播在线收听
  • 洪山网站建设域名购买之后怎么做网站
  • 北京网站建设公司服务哪家好wap是什么意思?
  • 怎么看公司网站做的好不好哦wordpress页面目录下
  • 做装修业务呢有多少网站平台搭建是什么
  • 潍坊优化网站排名淘宝做网站被骗
  • 建设专业网站的利弊免费logo设计生成器下载
  • 怎么在备案号添加网站网页设计动画网站
  • 网站开发 只要wordpress滑动注册
  • 跨境电商运营主要做什么静态网站如何做优化
  • 南充网站建设网站网站备案安全责任书是谁盖章
  • 怎么将网站设置为首页网站子目录怎么做