爆肝9万字,我已从小白晋升ARM嵌入式工程师!带你从零熟悉常用的M4嵌入式功能,建议收藏(含码源)
📚 前言
📑博客主页:@丘比特惩罚陆
💖欢迎关注:点赞收藏⭐留言✒
💬系列专栏:web前端、嵌入式、笔记专栏
🎮 加入社区: 丘比特惩罚陆
🥇人生格言:选对方向,每走一步都是进步!
✒️欢迎大佬指正,一起学习!一起加油!👏 希望大家能小手一动,帮忙点个赞!
😁资源邮箱:2237814512@qq.com
目录
6.3.1 TIM6和TIM7控制寄存器1(TIMx_CR1)
6.3.2 TIM6和TIM7控制寄存器2(TIMx_CR2)
6.3.3 TIM6 和 TIM7 DMA/中断使能寄存器 (TIMx_DIER)
6.3.5 TIM6和TIM7事件生产寄存器(TIMx_EGR)
6.3.8 TIM6和TIM7自动重载寄存器(TIMx_ARR)
9.6.3 RS485自定义协议+1主多从通信-按键控制-单次数据通信程序
11.2.1 SPI片选(/CS)引脚用于使能和禁止芯片操作
11.2.2 串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)
嵌入式简介
嵌入式系统定义
嵌入式系统的定义有很多的说法,但是没有一个定义是全面的,下面给到的两种方法我觉得应该算是比较合理的。
- 从技术的角度:嵌入式是以应用为中心,以计算机为基础的、软硬件可裁剪、适应应用系统对功能、可靠性、成本、体积、功耗严格要求的专用计算机系统;
- 从系统的角度:嵌入式系统是设计完成复杂功能的硬件和软件,并将它们紧密在一起的计算机系统。
嵌入式系统的实例–我们可以通过汽车控制系统来看:如下图
嵌入式发展过程
嵌入式微处理器(单板计算机)
嵌入式微处理器的基础是通用计算机中的CPU,在应用当中,将微处理器装配在专门设计的电路板上面,只用保留下我们和嵌入式应用有关的母版的功能,这样可以大幅度的减小系统的体积和相应的功耗。为了满足嵌入式应用的特殊要求,我们嵌入式微处理器虽然在功能上和标准的微处理器是一样的,但是在工作温度、抗电磁波等等方面一般都做到了和工业控制计算机相比较嵌入式微处理器体积小、重量轻、成本低、可靠性高等优点,嵌入式微处理器以及存储器、总线、外设等安装在一块电路板上面我们成为单板计算机–单片机;但是在电路板上面必须包含ROM、RAM、总线接口、外设接口等期间;从而降低系统的可靠性、技术保密性也比较差,所以现在用的都比较少了。
嵌入式处理器目前主要有Am186/88、386EX、SC-400、ARM系列等等;嵌入式微处理器又可分为CISC 和RISC两类。大家熟悉的大多数台式PC都是使用CISC微处理器,如Intel的x86。RISC结构体系有两大主流:Silicon Graphics公司(硅谷图形公司)的MIPS技术;ARM公司的Advanced RISC Machines技术。此外,Hitachi(日立公司)也有自己的一套RISC技术SuperH。
嵌入式微处理器的选型原则:
1)调查市场上已有的CPU供应商。2) CPU的处理速度。
3)技术指标。
4)处理器的低功耗。
5)处理器的软件支持工具。
6)处理器是否内置调试工具和处理器供应商是否提供评估板。
图1.1 单板计算机
选择一个嵌入式系统运行所需要的微处理器,在很多时候运算速度并不是最重要的考虑内容,有时候也必须考虑微处理器制造厂商对于该儇处埋器的文持态度,有些嵌入式系统产品一用就是几十年,如果过了五六年之后需要维修,却已经找不到该种微处理器的话,势必全部产品都要淘汰,所以许多专门生产嵌入式系统微处理器的厂商,都会为嵌入式系统的微处理器留下足够的库存或是生产线,即使过好几年之后还可以找到相同型号的微处理器或者完全兼容的替代品。
图示为:嵌入式微控制器(左)和微控制器芯片内部图
我们对上图进行一下解读,嵌入式微控制器我们又称为单片机,他已经将我们整一个计算机系统集成到一块芯片上面了。
嵌入式微控制器一般以某一 种微处理器内核为核心,芯片内部集成ROM/EPROM、RAM、 总线、总线逻辑、定时/计数器、WatchDog、 I/O、 串行口、脉宽调制输出、A/D、 D/A、 Flash RAM、EEPROM 等各种必要功能和外设。为适应不同的应用需求,-般- 个 系列的 单片机具有多种衍生产品, 每种衍生产品的处理器内核都是一样的,不同的是存储器和外设的配置及封装。这样可以使单片机最大限度和应用需求相匹配,功能不多不少,从而减少功耗和成本。
和嵌入式微处理器相比,微控制器的最大特点是单片化,体积大大减小,从而使功耗和成本下降、可靠性提高。微控制器是目前嵌入式系统工业的主流。微控制器的片上外设资源一般比较丰富, 适合于控制,因此称微控制器。嵌入式微控制器 目前的品种和数量最多,比较有代表性的通用系列包括8051、P51XA、MCS -251、MCS- -96/196/296、C166/167、MC68HC05/11/12/16、 68300、数目众多ARM芯片等。目前MCU占嵌入式系统约70%的市场份额。
当然,由于技术有限,目前能想到的,能查到的知识点只有那么一些,向更加深入了解的读者们可以到相关的网站、书籍以及视频课等等执查阅!
STM32F40x最小系统
概念:什么是最小系统? 最小系统指的是:能够让MCU工作的最小组成单位;结构:片内/片上外设、板上外设;我们可以看一下开发板的板上外设:
结构:片内/片上外设、板上外设;
大致如上:特殊单元有如右边的复位电路、震荡电路,称为板上电路。加上片上外设,组成芯片。
最小系统组成需要哪些部分?
- 电源部分:当前使用的芯片采用的是COMS电平(3.3V供电);
- 振荡电路:能够让MCU工作心跳产生起来的电路。一般这样的电路存在有很多种,比如RC振荡器、晶振等等;
- 复位电路: 概念–能够让MCU重新从main函数开始执行的电路;一般这样的复位电路可以分成很多种:比如软硬件复位、看门狗(WDG)复位等等;主要要掌握的是硬件复位:板上外设能够让MCU复位的电路。比如:你的板子卡住了,就需要一个按键进行板子的复位重新开始工作,这样的电路,就成为硬件复位。例如:重启键,下图为复位电路示例图
4. 启动方式:STM32F40x芯片启动的方式有很多种,可以根据自己的需要进行选择;
解释:当BOOT1:BOOT0为X 0的时候,这时候的启动方式为 从主Flash启动,MUC可以正常执行工作;
当BOOT1:BOOT0为0 1的时候,这时候的启动方式为 从系统存储器启动,但是程序可以进行烧录,但是不能执行;
当BOOT1:BOOT0为1 1的时候,这时候的启动方式为 从嵌入式SRAM启动,且程序可以进行烧录和进行代码执行;但是按下复位键的时候,程序不会执行,代码会丢失,因为当你按下复位键的时候,电源就会断电,因此代码程序也会跟着丢失。
因此,该芯片(STM32F407ZGT6为例,系列芯片差不多)最好用的是X0的方式进行启动。当然,当程序下载有某种错误无法操作的时候,可以进行启动方式的调整,这样后面就可以进行下载烧录了。
1、搭建开发环境
1.开发说明
我们开发一个功能首先第一步就是写各种各样不同模块的程序,但是在写程序模块之前,我们要的是搭建一个编程的开发的开发环境,我们要安装的开发的软件编程环境就是我们的keil5这个编程软件。跟着我的脚步,我们一步步的安装这个编程软件。
这个文件包大家可以到STM32官网上面自行下载,也不大就100来M,我们可以看到上面,“芯片支持包”,里面包含使用的是我们的芯片,一个工程要搭建起来 一定所需的就是问我们的芯片包。最下面的两个是我们下载的驱动,一个可以支持win7、win0系统。大家可以根据自己的电脑的配置自行选择下载,这一些资源后续有需要的可以私聊,需要什么都会进行分享,或者大家可以自行到官网下载,这样更加直接了当。
1.1软件安装
我们上述的软件安装包进行到这里,我们直接双击点击MDK进行运行安装,然后就会出现到一下的这个界面,然后接着我们点击next就好了。
下一步,我们勾上“同意”,再接着next就好了;
接下来出现的界面是要我们安装的地方,大家执自行安装到自己想要存放的位置就可以了,最好不要安装到系统盘,也最好不用使用中文、不要有空格,因为中文空格有时候会出现错误,当然出现的几率很小,但是我们还是严谨一点为好。
接下来就到这个界面,里面的参数我们可以随意修改,但是还是不要有中文;
到这里后就next,然后就可以进行到安装软件了;
接下里会出现一个这样的界面,我们需要的是要把这个选项勾选上的去掉,因为如果你选择他的话,它就会自动帮你运行工程;
下载安装完成之后,我们就会看到电脑自动弹出这样的一个界面,我们不需要理会,也不需要任何的操作,直接关掉就好了,因为我们这个步骤没有什么东西需要我们进行改动的,然后我们的软件就安装完毕了;
然后我们就可以在我们的桌面上看到我们的软件的图标,表示安装已经全部完成;
2. 软件注册
在我们刚刚的文件夹下面我们找到这个注册机,我们直接双击进行运行注册;
接下来我们右键 单击我们刚刚下载的keil5软件,然后以“管理员身份运行”;
然后我们到keil5里面在我们的file这一栏我们点击选择“license”这一栏,
点击之后我们机会进到下面这个界面,这个是我们的软件注册的界面;
然后下面的注册机上面的操作大家按照下面图示的步骤操作就可以了,这里图示比文字更加的详细;
到这个地方,我们的软件的安装加上我们注册机的注册就完成了;
3.安装相应的芯片包
改该文章我们使用的是stm32F407ZGT6,我们就要安装相对应的芯片包,(第一个是F4系列的,第二个是完整的芯片包)
我们的第一步就是直接运行我们的Keli.STM32F4xx_DFP.2.7.0这个包,然后我们就可以看到下面的这个界面;
在我们等待一段时间之后就会出现这样的一个界面,我们不需要进行任何的操作,我们直接next就好了,下一步,就会进行我们芯片包的安装,我们只需要等待下载完成就好了;如下图所示
4.安装下载器驱动
因为我们写好代码,我们要需要将代码烧录到我们的板子,所以我们需要安装一个可以下载的驱动,我们这里用的是ST-Link,我们可以根据自己的系统进行下载不同的驱动;
我们这一篇文章我们使用的是win10,所以这里就使用win10,如下图,我们点击的是win19的驱动,我们就会就会进入到我们这样的一个界面;
接着我们就会弹出这样的一个界面;
我们只需要点击next就可以了;
当出现这样的一个界面,这样就驱动安装完成。路径全部都选择默认的路径,不需要进行任何操作,下一步,我们怎么检查是否安装成功呢,我们电脑脸上ST-Link,打开我们的设备管理器,出现这样的,那么我们这个工程的前期工作就表示安装成功了。
当然不代表这样就成功了,只有驱动的灯不闪烁,才算是安装成功,灯颜色固定为红色,不闪烁即可。
这一个安装的步骤都是博主往期的博客,大家想看简单的,可以网页进行搜索就好了。相关的链接如下:本文链接:【Cortex-M4】学习 Day1 搭建开发环境_丘比特惩罚陆的博客-CSDN博客
2、GPIO实验
2.1GPIO概述
GPIO接口指的是通用输入/输出接口。通俗的讲,那就是芯片的引脚,可以通过他们输入高电平或者低电平,也可以通过读入他们的引脚的电平状态–到底是高还是低电平。GPIO口:简单而言,就是芯片用来控制或者采集外部器件的相关信息,这个又可以称之为输入输出的端口。
那GPIO的作用是什么呢?是芯片用来感知外部世界并且用来控制外部世界的媒介;
在我们的STM32中,GPIO口一共有114个,而且这114个GPIO口以分组的形式存在,如下图所示:我们由这个芯片内部结构框架这个图(STM32F407xx),我们可以知道GPIO口,会按照分组的形式存在。
如下图,分别可以从GPIOA到GPIOI口,但是上述的结构框架图是F407系列的,现在记笔记的芯片为STM32F407ZGTx芯片,所以上述的分组没有那么多,只含有分配到从GPIOA–GPIOG一共7组,每一组16个IO口,可以如上结构框架图可以知道从0口–15口,一共十六个,所以一共总得GPIO口一共是112个。
STM32F407ZGT6芯片
还有两个分别是,我们可以从电路图可以找到,因为这两个IO口比较特殊,不用我们直接操作的,所以可以不算到结构图当中来,我们可以了解一下,分别如下:
另一个也是在CPU右边,这个IO口是电池管理。
扩展比较一下,相比于STM32,我们的51单片机的IO口也是按照分组的形式存在的,分别从P0-P3组,而且每一组IO口都有8个口,分别由P0.0-P0.7.
2.2 GPIO的主要特征
- 受控IO口多达十六个,每一组有16个IO口;
- 输出状态:推挽或者开漏+上拉或下拉
- 从输出数据寄存器(GPIOx_ODR)或外设(复用功能输出)输出数据;
- 可以为每一个IO选择不同的速度;
- 输入状态:浮空、上下拉、模拟输入(数模转换器);
- 将数据输入到输入数据寄存器(GPIOx_ODR)或外设(复用功能输入)(这里简单而言,我们采集外面的信息,放到哪里去,就是我们的输入数据寄存器中);
- 置位或者复位寄存器(GPIOx_BSRR),对GPIOx_ODR具有按位写权限;
- 锁定机制(GPIOx_LCKR),可冻结IO配置(这些简单理解就是比如我们用引脚控制灯,我们就是只用一个引脚就可以了,后面不用改变这个引脚,然后我们就可以将这个引脚改成冻结配置模式,好处就是我们不小心碰了一个引脚转态,这个冻结就可以帮我们锁定,不然固定功能因此改变);
- 含有模拟功能;
- 复用功能输入/输出选择寄存器(一个IO口最多可以有十六个复用功能,和51不一样,我们每一个IO口都有复用功能)(这里简单理解就是我们所谓的复用就是除了GPIO我们还有很多的很多,如下图所示,TIMx、SPI等等);
- 快速翻转,每次翻转最多只需要两个时钟周期;
- 引脚复用非常灵活,允许将IO引脚用作GPIO或多种外设功能中的一种;
我们和我们的51单片机相互比较一样,就会发现和51的IO口不一样,我们下图图示比较一下:
如上圈起来的解释,51单片机这个P3只有P3.0和RxD这两个功能,所以我们的STM32的GPIO口用起来更加的灵活。
2.3 变量和寄存器
我们说一下这两个不同以及相同的地方,二者相同的地方:变量和寄存器一样都是向内存申请了存储的空间,可以存储数据,并且还有相应的地址;
二者存在差异的地方:变量的地址是随时变化的,但是寄存器的地址是不变的,简单说,寄存器的地址已经被我们的芯片给固定好了。
例如:
变量我们用一个个小的(右下角)方框表示,让后这个变量是可以改变的,可以进行左移右移等操作,但是我们的寄存器,寄存器相当于变量,变量就会有变量名,变量名就有地址,只不过我们的寄存器的地址是固定的(如上我们设置成0x32),所以0x32就是我们寄存器的名字。
2.4 GPIO实验(按键点亮LED灯)
我们通过下面的电路图,我们可以看到四个灯的引脚分别是PF9、PF10、PF6、PC0,我们直接进行宏定义这一些引脚就可以了,默认为高电平,电平低点亮;
进行灯的宏定义:(这里直接在我们的main.c进行宏定义)
#define led1 PFout(9)
#define led2 PFout(10)
#define led3 PFout(6)
#define led4 PCout(0)
然后我们再看看我们的按键;
有这个图我们直接可以在我们的main.c进行按键的宏定义就可以了;
#define key1 PAin(0)
#define key2 PEin(2)
#define key3 PEin(3)
#define key4 PEin(4)
LED.c
新建一个文件,我们命名为main.c 来配置我们灯的初始化 LED_Init,然后我们进行相应的配置就好了;
#include "led.h"
void LED_Init(void)
{
GPIO_InitTypeDef led;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); //使能GPIOC
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能GPIOC
led.GPIO_Mode=GPIO_Mode_OUT; //输出
led.GPIO_OType=GPIO_OType_PP; //推挽
led.GPIO_Pin=GPIO_Pin_0; //引脚
led.GPIO_PuPd=GPIO_PuPd_UP; //上拉
led.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOC,&led);
led.GPIO_Mode=GPIO_Mode_OUT; //输出
led.GPIO_OType=GPIO_OType_PP; //推挽
led.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_9|GPIO_Pin_10; //引脚6/9/10
led.GPIO_PuPd=GPIO_PuPd_UP; //上拉
led.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOF,&led);
GPIO_SetBits(GPIOC, GPIO_Pin_0); //给到高电平,因为我们前面说了电路为低电平点亮
GPIO_SetBits(GPIOF, GPIO_Pin_6|GPIO_Pin_9|GPIO_Pin_10);
}
Key.c 操作还是跟上面的,配置相应的时钟、功能等等;不过多的介绍,因为往期的博客已经有,而且这个东西相对的简单,就不讲了!
然后我们在函数进行调用就可以了!如下图:
这样我们的一个按键点亮我们LED灯的小实验就可以完成了,相关的项目文件夹后面有需要的话可以私聊!
3、中断实验
3.1中断简介
3.1.1中断概念
什么是中断?中断就是CPU在正常运行程序的时候,由于内部或者外部事件引起的暂时中止现行的程序,转去执行请求CPU为其服务的那个外设或者事件的服务程序,等待这个服务程序执行完毕又要返回到被中止的地方的程序,这么的一个过程。我,们可以根据一下的流程图进行一下描述:

中断指的很简单,指的是在我们的CPU执行正常的程序的时候,由于外部或者内部事件或由程序预先安排的事件,引起的CPU中断正在运行的程序,而转到为外部或内部事件有程序预先安排的事件服务的程序中去,服务完毕,再返回到执行暂时中断的程序。
3.1.2 中断意义
中断,程序设计中占着非常重要的地位。如果没有中断,则CPU的工作效率会大折扣。就像我们的UART模块中,接收电脑发送来的数据,使用了while(查询标志)(查询状态的方式,如果电脑没有发送数据,则程序会一直阻塞,使CPU做不了其他事情。如果有一种机制,不用CPU循环查询是否有数据到来,而是硬件自动接收数据,当收到数据时候的自动通知CPU,这时候CPU再去把数据读取出来。这样,在没有数据接收到前,CPU可以去做其他事情,工作效率自然就提高了。在CPU 硬件中,通过中断这种机制来实现这个功能,每个片上外设硬件都提供一个中断信号,当模块处理特定工作状态时,会发出中断信号通信CPU。
3.2 STM32的中断
3.2.1 STM32中断的特性
- Cortex-M4内部包含有嵌套向量中断控制器
- 与内核紧密联系的中断控制器,可支持低中断延时
- 可对系统异常和外设中断进行控制
16个可编程的优先等级(使用了4位中断优先级)
82个可屏藏中断通道:可重定位的向量表
- 不可屏蔽中断
- 软件中断功能
嵌套向量中断控制器(NIC)是Cortex-M4 的一个内部器件。与CPU紧密结合,降低中断延时,让新进中断可以得到高效处理。
Cortes-M3核心的中断实现是通过嵌入一个NVIC控制器,该控制器功能非常强大。在中断处理上效率很高,优先级配置也很灵活;
3.2.2 NVIC控制器的优先级表示
NVIC控制器的优先级使用8个二进制位表示,分成两部分进行表示,分别是抢占优先级以及响应优先级。
- 抢占优先级:含义是在不同的等级之间的中断可以相互嵌套,高优先级可以中断低优先级,数字小的优先级高;
- 响应优先级:含义是不同的响应优先级的中断不可以相互嵌套,但是当响应优先级相同的时候,响应优先级不同,多个中断源同时发生中断的时候,响应优先级高的中断事件可以优先响应,数字小的响应的优先级越高。
- 自然优先级:就是我们的NVIC控制器的中断源编号,数字小的优先级高。作用是:当抢占优先级和响应优先级相同且都发生了中断的时候,自然优先级高的优先响应,数字小的优先级高。
优先级等级小结:抢占优先级>响应优先级>自然优先级,抢占优先级决定了是否可以嵌套,响应优先级和自然优先级决定发生的时候,先响应哪一个(如果抢占相同,就比较我们的响应优先级,两个都相同的话,那就比较我们的自然优先级,自然优先级是我们厂家生产出来的时候已经决定好的了!)
STM32F10x把指定的中断优先级的寄存器位用了4个位,这四个寄存器位的分组方式可以如下:
组编号 |
PRIGROUP区域(3位) AIRCR[10:8]0xE000_ED0C |
抢占优先级位数 | 响应优先级位数 | 抢占优先级可配置范围 | 响应优先级可配置范围 |
0 | 0x07 | 0 | 4 | 0 | 0-15 |
1 | 0x06 | 1 | 3 | 0-1 | 0-7 |
2 | 0x05 | 2 | 2 | 0-3 | 0-3 |
3 | 0x04 | 3 | 1 | 0-7 | 0-1 |
4 | 0x03 | 4 | 1 | 0-15 | 0 |
要配置分组的话,按照上面的这个表格,要将PRIGROUP列的值写到我们的AIRCR[10:8]当中,一个程序只有一个分组,当分组确定的时候,那个其中的响应优先级和抢占优先级也就确定了!
3.3 中断向量表
位置 | 优先级 | 名称 | 说明 | 地址 |
0 | 7 | WWDG | 窗口定时器中断 | 0x0000_0040 |
1 | 8 | PVD | 连接倒EXTI的电源电压检测中断 | 0x0000_0044 |
2 | 9 | TAMPER | 清儒检测中断 | 0x0000 0048 |
3 | 10 | RTC | 实时时钟全局中断 | 0x0000 004c |
4 | 11 | FLASH | 闪存全局中断 | 0x0000 0050 |
5 | 12 | RCC | 复位和时钟控制中断 | 0x0000 0054 |
6 | 13 | EXTI0 | EXTI线0中断 | 0x0000 0058 |
7 | 14 | EXTI1 | EXTI线1中断 | 0x0000 005c |
8 | 15 | EXTI2 | EXTI线2中断 | 0x0000 0060 |
9 | 16 | EXTI3 | EXTI线3中断 | 0x0000 0064 |
10 | 17 | EXTI4 | EXTI线4中断 | 0x0000 0068 |
11 | 18 | DMA通道1 | DMA通道1全局中断 | 0x0000 006c |
12 | 19 | DMA通道2 | DMA通道2全局中断 | 0x0000 0070 |
13 | 20 | DMA通道3 | DMA通道3全局中断 | 0x0000 0074 |
14 | 21 | DMA通道4 | DMA通道4全局中断 | 0x0000 0078 |
15 | 22 | DMA通道5 | DMA通道5全局中断 | 0x0000 007c |
16 | 23 | DMA通道6 | DMA通道6全局中断 | 0x0000 0080 |
17 | 24 | DMA通道7 | DMA通道7全局中断 | 0x0000 0084 |
18 | 25 | ADC | ADC1、2、3的全局中断 | 0x0000 0088 |
19 | 26 | CAN1_TX | CAN TX中断 | 0x0000 008c |
20 | 27 | CAN1_RX0 | CAN1_RX0中断 | 0x0000 0090 |
21 | 28 | CAN_RX1 | CAN_RX1中断 | 0x0000 0094 |
22 | 29 | CAN_SCE | CAN_SCE中断 | 0x0000 0098 |
23 | 30 | EXTI9_5 | EXTI[9:5]中断 | 0x0000 009c |
24 | 31 | TIMI_BRK_TIM9 | TIMI刹车中断和TIM9全局中断 | 0x0000 00A0 |
25 | 32 | TIM1_UP_TIM10 | TIMI更新中断和TIM9全局中断 | 0x0000 00A4 |
26 | 33 | TIM1_CC | 捕获比较中断 | 0x0000 00AC |
27 | 34 | TIM2 | TIM2全局中断 | 0x0000 00B0 |
28 | 35 | TIM3 | TIM3全局中断 | 0x0000 00B4 |
29 | 36 | TIM4 | TIM4全局中断 | 0x0000 00B8 |
3.4 STM32中断编程
我们知道由于我们的NVIC是一个属于内核级别的外设,我们所有的厂家的芯片在寄存器上面都是一样的,所以配置方法以及配置后的作用都是一样的。所以ARM公司已经给我们提供了一份通用的NVIC的配置函数,我们只需要弄清楚他们的作用以及使用方法就可以了!
3.4.1 NVIC分组设置
voidNVIC_SetPriorutyGrouping(uint32_tPriorityGroup),
PriorityGroup:分组方式对应的值,0-7组我们前面已经说了,简单的例子就是比如你选择的是第二组,那么就是2位的抢占优先级,那么对应的值就是7-2=5;我们设置分组2的方式,
NVIC_SetPriorutyGrouping(7-2);AIRCR[10:8] .
3.4.2 NVIC优先级编码
uint32_t NVIC_EncodePriority(uint32_tPriorutyGroup,uint32_tPreempPriority,uint32_tSubPRiority);
我们这个函数把分组方式,抢占优先级,子优先级数值编码成一个32位的数字,作为我们的返回值返回。
PriorutyGroup:分组方式对应的值,也就是AIRCR[10:8]中的值,或者也可以说是NVIC_SetPriorityGrouuping函数的参数。
PreemptPriority:正确的抢占优先级值,比如分组2,范围是0~3取值,SubPriority:正确的子优先级值,比如分组2,范围是0~3取值,
说明:这个函数并没有把优先级设置到寄存器中,只是简单的合成一个表示优先级数字。后面再使用其他数设置这个值到寄存器中。
可以使用移位方式配置优先级。但是配置上比较麻烦。
Pri=NWIC_EncodePriority(7-2,1,3);→分组2,抢占优先级是1,响应优先级是3。返回编码后的值,个值就是写入对应中断源中断寄存器中的值。这个函数并没有写入寄存器中。
3.4.3 NVIC优先级配置
void NVIC_SetPriority(lRQn_TypeIRQn,uint32_tpriority)该函数用于设置指定中断源的中断优先级。
IRQn_Type是在 stm32F10x.h文件中有定义,一个枚举类型,定义了STM32F10x芯片对应所有的中断源编引IRQn:中断源编号;
priority:中断优先级,就是使用NIC_EncodePriority()函数编码的返回值。Pri=NVIC_EncodePriority(7-2,1.3); //USART1__IRQn=37,
NVIC_SetPriority(USART1_IRQn,Pri); //设置串口1的中断优先级。
3.4.4 NVIC中断使能
中断使能也是由NVIC控制器控制的。
0xE000_E100 – 0xE000_E11C这个地址范围的奇存器是用来配置中断使能。一个中断源只要一个位。
一个寄存器是32位。总共有8个寄存器,位数是32*8=256.
下图最后一个寄存器SETENA7管224~239号中断。共15+1=16个中断。前面的都是管32个。
原因是SETENA0~SETENA7只能用于使能厂家的外扩的中断源。不用于管理Cortex-M3核心占用的前16个中断。
比如:STM32F103的USARTI中断使能,就要配置SETENA1[3]位为l,其他中断类似。void NVIC_EnableIRQ(IRQn_TypeIRQn)
该函数用于使能指定中断源。//使能串口中断
3.4.5 初始化NVIC控制器的步骤
1.设置分组方式
2.确定中断源的抢占优先级和响应优先级,使用NVIC_EncodePriority函数进行编码
3.把上下编码得到优先级值写入优先级配置寄存器,使用NVIC_SetPriority函数。
4使能对应中断源,使用NVIC_EnableIRQ函数。
5.具体的模块中断:还要去配置外设中断相关的寄存器(下小节会以外部中断以例子讲解)
6.编写中断服务函数:
1)到启动代码中找到对应的中断入口,以确定中断函数名字;
2)在C文件中编写一个无返回值,无类型的函数。函数名就是上一步得到的名字。
3.4.6 中断服务函数编写
比如我们要编写一个中断服务函数的话,我们直接到你工程的启动文件就可以将这个函数的命名复制下来,千万别自己手动敲;
例如我们编写一个EXTI线0的中断服务函数;我们直接到我们的启动文件找到我们的相应的函数名复制下来就可以了:
函数的编写格式如下:
void EXTI0_IRQHandler(void)
{
你想要编写实现的内容:()
}
我们写一个中断按键控制灯的,我们就可以这样子写;
void EXTI0_IRQHandler(void)
{
EXTI_ClearITPendingBit(EXTI_Line0); //清除外部中断0标志位
led1= !led1;
}
注意:这里说明一下,我们的中断服务函数在编写内容上面和我们的普通代码没有什么太大的区别,大家可以根据自己的需要编写功能代码,只有里面一定要有清除中断标志位的代码就可以了。中断服务函数名要和我们的启动文件对应的中断源的中断入口标志相同。并且还是需要原型为voidfun(void).
4.UART和USART
4.1 UART简介
我们的串行接口指的是数据一位一位的顺序传输传送过去,它的特点就是通信的线路简单,只要有一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大的降低了成本,特别是在我们的远距离通信,但是传输的速度很慢。
一条信息的个位数据被逐位按顺序传送的通讯方式成为串行通讯。该通讯的特点是:数据位的传送是按位进行的,最少只需要一条数据线就可以完成;成本比较低但是速度很慢。串行通讯的距离一般可以从机密到几千米;我们根据数据的传输方向,串行通讯可以分成三种–单工、半双工、全双工;
- 单工:只有一个方向的数据传输;
- 半双工:数据可以在一个信号的载体上的两个方向传输,不是不可以进行到同时的传输,数据线分为复用;
- 全双工:双向的数据传输;允许数据在两个方向上面的同时传输,他在能力上相当于两个但施工的结合。
重点来了:
UART(universal asynchronous receiver transmitter),我们指的是异步串行,universal asynchronous receiver transmitter(通用异步接收发送)。在这里UART是一个并行输入成为串行输入的芯片,通常会集成在我们的板载上面。
UART包含TLT电平(电平为3.3V)的串口和RS232电平(负逻辑电平:+5-12v低电平;-12-5位高电平)的串口。
4.2 USART
通用同步异步接受发送控制器,是一套提供了灵活的方法与使用工业标准NRZ异步创新数据格式的外部设备之间进行全双工数据交换。USART利用分数串列传输速率发生器提供宽范围的串列传输速率选择。它支持同步单线通信和半双工单线通信,也支持局部联网,智能卡协议和红外数据组织以及调制解调器操作,还允许多个处理器通信。使用多缓冲器配置的DMA方式,可以实现高速数据通信。
我们接口通过的是三个引脚和其他的设备链接到一起,任何的USART双向通信至少都要两个引脚:接收数据输入(RX)和发送数据输出(TX);
RX:接收数据输入。通过采样技术来区别数据和噪音,从而恢复数据;
TX:发送数据输出。当发送器被禁止的时候,输出引脚恢复到它的I/O端口配置。当发送器激活的时候,并且不发送数据的时候,TX引脚处于高电平。
- 总线在发送或者接收前处于空闲状态;
- 一个起始位;
- 一个8或9位的书局字,最低有效位在前;
- 0.5、1.5、2个停止位,由此表明数据帧结束;
- 使用分数波特率发生器–12位整数和4位小数;
- 一个状态寄存器(USART_SR);
- 数据寄存器(USART_DR);
- 一个波特率寄存器(USART_BRR),12位整数和4位小数;
- 一个智能卡模式下的保护时间寄存器(USART_GTPR)。
4.3 UART特性
- 字节可以通过编程USART——CR1寄存器中的M位,选择成8或者9位。在其实期间,TX脚处于低电平,在停止位期间处于高电平;
- 空闲符号被视为完全有‘1’组成的一个完整的数据帧,后面跟着包含的下一个帧烦人开始为(‘1’的位数也包括了定制为的位数);
- 断开符号 被视为在一个帧周期全部收到“0”包括停止位,也是“0”。在断开帧结束的时候,发送器再插入1或2个停止位(‘1’)来应答起始位;在断开帧结束时,发送器再插入1或2停止位(‘1’)来应答起始位;
- 发送和接收由一个用的波特率发生器驱动,当发送器和接收器的使能位分别置位的时候,分别为其产生时钟。
4.4 串口如何工作
串口的工作有两种方式:查询和中断;
查询:串口程序不断的循环查询,查看当前有没有数据要他传送,有的话,就帮它传送(可以从PC端到STM32板子传送,也可以从STM32板子到PC端);
中断:当串口打开中断时,如果发现有一个数据到来或数据发送完毕,则会发生中断,就以为着它帮助传输数据–它就马上进行数据的传送。同时,可以从PC机到STM32F4板子,也可以从STM32F4板子到PC端!


波特率的产生
接收器和发送器的波特率在UASRTDIV的整数和小数寄存器中的值应设置成相同。这里的FCK是给外设的时钟(PCLK1用于USART2、3、4、5,PCLK2用于USART1)。USARTDIV是一个无符号的定点数。这12位的值设置在USART_BRR寄存器。方图如下:

4.5 与UART相关的寄存器
4.5.1 状态寄存器(USART_SR)
位 9 CTS:CTS 标志 (CTS flag)
如果 CTSE 位置 1,当 nCTS 输入变换时,此位由硬件置 1。通过软件将该位清零(通过向
该位中写入 0)。如果 USART_CR3 寄存器中 CTSIE=1,则会生成中断。
0:nCTS 状态线上未发生变化
1:nCTS 状态线上发生变化
注意:该位不适用于 UART4 和 UART5。
位 8 LBD:LIN 断路检测标志 (LIN break detection flag)
检测到 LIN 断路时,该位由硬件置 1。通过软件将该位清零(通过向该位中写入 0)。如果
USART_CR2 寄存器中 LBDIE = 1,则会生成中断。
0:未检测到 LIN 断路
1:检测到 LIN 断路
注意:如果 LBDIE=1,则当 LBD=1 时生成中断
位 7 TXE:发送数据寄存器为空 (Transmit data register empty)
当 TDR 寄存器的内容已传输到移位寄存器时,该位由硬件置 1。如果 USART_CR1 寄存器
中 TXEIE 位 = 1,则会生成中断。通过对 USART_DR 寄存器执行写入操作将该位清零。
0:数据未传输到移位寄存器
1:数据传输到移位寄存器
注意:单缓冲区发送期间使用该位
位 6 TC:发送完成 (Transmission complete)
如果已完成对包含数据的帧的发送并且 TXE 置 1,则该位由硬件置 1。如果 USART_CR1 寄存
器中 TCIE = 1,则会生成中断。该位由软件序列清零(读取 USART_SR 寄存器,然后写入
USART_DR 寄存器)。TC 位也可以通过向该位写入‘
0’来清零。建议仅在多缓冲区通信
时使用此清零序列。
0:传送未完成
1:传送已完成
位 5 RXNE:读取数据寄存器不为空 (Read data register not empty)
当 RDR 移位寄存器的内容已传输到 USART_DR 寄存器时,该位由硬件置 1。如果
USART_CR1 寄存器中 RXNEIE = 1,则会生成中断。通过对 USART_DR 寄存器执行读入
操作将该位清零。RXNE 标志也可以通过向该位写入零来清零。建议仅在多缓冲区通信时使
用此清零序列。
0:未接收到数据
1:已准备好读取接收到的数据
位 4 IDLE:检测到空闲线路 (IDLE line detected)
检测到空闲线路时,该位由硬件置 1。如果 USART_CR1 寄存器中 IDLEIE = 1,则会生成中
断。该位由软件序列清零(读入 USART_SR 寄存器,然后读入 USART_DR 寄存器)。
0:未检测到空闲线路
1:检测到空闲线路
注意:直到 RXNE 位本身已置 1 时(即,当出现新的空闲线路时)IDLE 位才会被再次置 1。
位 3 ORE:上溢错误 (Overrun error)
在 RXNE = 1 的情况下,当移位寄存器中当前正在接收的字准备好传输到 RDR 寄存器时,该
位由硬件置 1。如果 USART_CR1 寄存器中 RXNEIE = 1,则会生成中断。该位由软件序列清
零(读入 USART_SR 寄存器,然后读入 USART_DR 寄存器)。
0:无上溢错误
1:检测到上溢错误
注意:当该位置 1 时,RDR 寄存器的内容不会丢失,但移位寄存器会被覆盖。如果 EIE 位置 1,
则在进行多缓冲区通信时会对 ORE 标志生成一个中断。
位 2 NF:检测到噪声标志 (Noise detected flag)
当在接收的帧上检测到噪声时,该位由硬件置 1。该位由软件序列清零(读入 USART_SR 寄
存器,然后读入 USART_DR 寄存器)。
0:未检测到噪声
1:检测到噪声
注意:如果 EIE 位置 1,则在进行多缓冲区通信时,该位不会生成中断,因为该位出现的时间与
本身生成中断的 RXNE 位因 NF 标志而生成的时间相同。
注意:当线路无噪声时,可以通过将 ONEBIT 位编程为 1 提高 USART 对偏差的容差来禁止
位 1 FE:帧错误 (Framing error)
当检测到去同步化、过度的噪声或中断字符时,该位由硬件置 1。该位由软件序列清零(读入
USART_SR 寄存器,然后读入 USART_DR 寄存器)。
0:未检测到帧错误
1:检测到帧错误或中断字符
注意:该位不会生成中断,因为该位出现的时间与本身生成中断的 RXNE 位出现的时间相同。
如果当前正在传输的字同时导致帧错误和上溢错误,则会传输该字,且仅有 ORE 位被
置 1。
如果 EIE 位置 1,则在进行多缓冲区通信时会对 FE 标志生成一个中断。
位 0 PE:奇偶校验错误 (Parity error)
当在接收器模式下发生奇偶校验错误时,该位由硬件置 1。该位由软件序列清零(读取状态
寄存器,然后对 USART_DR 数据寄存器执行读或写访问)。将 PE 位清零前软件必须等待
RXNE 标志被置 1。
如果 USART_CR1 寄存器中 PEIE = 1,则会生成中断。
0:无奇偶校验错误
1:奇偶校验错误
4.5.2 数据寄存器(USART_DR)
位 8:0 DR[8:0]:数据值
包含接收到数据字符或已发送的数据字符,具体取决于所执行的操作是“读取”操作还是
“写入”操作。
因为数据寄存器包含两个寄存器,一个用于发送 (TDR),一个用于接收 (RDR),因此它具有
双重功能(读和写)。
TDR 寄存器在内部总线和输出移位寄存器之间提供了并行接口(参见图 1)。
RDR 寄存器在输入移位寄存器和内部总线之间提供了并行接口。
在使能奇偶校验位的情况下(USART_CR1 寄存器中的 PCE 位被置 1)进行发送时,由于
MSB 的写入值(位 7 或位 8,具体取决于数据长度)会被奇偶校验位所取代,因此该值不
起任何作用。
在使能奇偶校验位的情况下进行接收时,从 MSB 位中读取的值为接收到的奇偶校验位。
4.5.3 波特率寄存器(USART_BRR)
位 31:16 保留,必须保持复位值 ;位 15:4 DIV_Mantissa[11:0]:USARTDIV 的尾数
这 12 个位用于定义 USART 除数 (USARTDIV) 的尾数
位 3:0 DIV_Fraction[3:0]:USARTDIV 的小数;
这 4 个位用于定义 USART 除数 (USARTDIV) 的小数。当 OVER8 = 1 时,不考虑 DIV_Fraction3
位,且必须将该位保持清零。
4.5.4 控制寄存器(USART_CR1)
位 31:16 保留,必须保持复位值
位 15 OVER8:过采样模式 (Oversampling mode)
0:16 倍过采样
1:8 倍过采样
注意:8 倍过采样在智能卡、IrDA 和 LIN 模式下不可用:当 SCEN=1、IREN=1 或 LINEN=1 时,
OVER8 由硬件强制清零。
位 14 保留,必须保持复位值
位 13 UE:USART 使能 (USART enable)
该位清零后,USART 预分频器和输出将停止,并会结束当前字节传输以降低功耗。此位由软
件置 1 和清零。
0:禁止 USART 预分频器和输出
1:使能 USART
位 12 M:字长 (Word length)
该位决定了字长。该位由软件置 1 或清零。
0:1 起始位,8 数据位,n 停止位
1:1 起始位,9 数据位,n 停止位
注意:在数据传输(发送和接收)期间不得更改 M 位
位 11 WAKE:唤醒方法 (Wakeup method)
该位决定了 USART 唤醒方法,该位由软件置 1 或清零。
0:空闲线路
1:地址标记
位 10 PCE:奇偶校验控制使能 (Parity control enable)
该位选择硬件奇偶校验控制(生成和检测)。使能奇偶校验控制时,计算出的奇偶校验位被
插入到 MSB 位置(如果 M=1,则为第 9 位;如果 M=0,则为第 8 位),并对接收到的数据
检查奇偶校验位。此位由软件置 1 和清零。一旦该位置 1,PCE 在当前字节的后面处于活动
状态(在接收和发送时)。
0:禁止奇偶校验控制
1:使能奇偶校验控制
位 9 PS:奇偶校验选择 (Parity selection)
该位用于在使能奇偶校验生成/检测(PCE 位置 1)时选择奇校验或偶校验。该位由软件置 1 和
清零。将在当前字节的后面选择奇偶校验。
0:偶校验
1:奇校验
位 8 PEIE:PE 中断使能 (PE interrupt enable)
此位由软件置 1 和清零。
0:禁止中断
1:当 USART_SR 寄存器中 PE=1 时,生成 USART 中断
位 7 TXEIE:TXE 中断使能 (TXE interrupt enable)
此位由软件置 1 和清零。
0:禁止中断
1:当 USART_SR 寄存器中 TXE=1 时,生成 USART 中断。
位 6 TCIE:传送完成中断使能 (Transmission complete interrupt enable)
此位由软件置 1 和清零。
0:禁止中断
1:当 USART_SR 寄存器中 TC=1 时,生成 USART 中断
位 5 RXNEIE:RXNE 中断使能 (RXNE interrupt enable)
此位由软件置 1 和清零。
0:禁止中断
1:当 USART_SR 寄存器中 ORE=1 或 RXNE=1 时,生成 USART 中断
位 4 IDLEIE:IDLE 中断使能 (IDLE interrupt enable)
此位由软件置 1 和清零。
0:禁止中断
1:当 USART_SR 寄存器中 IDLE=1 时,生成 USART 中断
位 3 TE:发送器使能 (Transmitter enable)
该位使能发送器。该位由软件置 1 和清零。
0:禁止发送器
1:使能发送器
注意:1:除了在智能卡模式下以外,传送期间 TE 位上的“0”脉冲(“0”后紧跟的是“1”)
会在当前字的后面发送一个报头(空闲线路)。
2:当 TE 置 1 时,在发送开始前存在 1 位的时间延迟。
位 2 RE:接收器使能 (Receiver enable)
该位使能接收器。该位由软件置 1 和清零。
0:禁止接收器
1:使能接收器并开始搜索起始位
位 1 RWU:接收器唤醒 (Receiver wakeup)
该位决定 USART 是否处于静音模式。该位由软件置 1 和清零,并可在识别出唤醒序列时由硬
件清零。
0:接收器处于活动模式
1:接收器处于静音模式
注意:1:选择静音模式前(通过将 RWU 位置 1),USART 必须首先接收一个数据字节,否则
当由空闲线路检测到唤醒时,它无法于静音模式下正常工作。
2:在地址标记检测唤醒配置(WAKE 位 = 1)中,RXNE 位置 1 时,RWU 位不能由软件
进行修改。
位 0 SBK:发送断路 (Send break)
该位用于发送断路字符。该位可由软件置 1 和清零。该位应由软件置 1,并在断路停止位期间
由硬件重置。
0:不发送断路字符
1:将发送断路字符
4.5.5 控制寄存器(USART_CR2)
位 31:15 保留,必须保持复位值
位 14 LINEN:LIN 模式使能 (LIN mode enable)
此位由软件置 1 和清零。
0:禁止 LIN 模式
1:使能 LIN 模式
LIN 模式可以使用 USART_CR1 寄存器中的 SBK 位发送 LIN 同步断路(13 个低位),并可检测 LIN
同步断路。
位 13:12 STOP:停止位 (STOP bit)
这些位用于编程停止位。
00:1 个停止位
01:0.5 个停止位
10:2 个停止位
11:1.5 个停止位
注意:0.5 个停止位和 1.5 个停止位不适用于 UART4 和 UART5。
位 11 CLKEN:时钟使能 (Clock enable)
该位允许用户使能 SCLK 引脚。
0:禁止 SCLK 引脚
1:使能 SCLK 引脚
该位不适用于 UART4 和 UART5。
位 10 CPOL:时钟极性 (Clock polarity)
该位允许用户在同步模式下选择 SCLK 引脚上时钟输出的极性。它与 CPHA 位结合使用可获得所需
的时钟/数据关系
0:空闲时 SCLK 引脚为低电平。
1:空闲时 SCLK 引脚为高电平。
该位不适用于 UART4 和 UART5。
位 9 CPHA:时钟相位
该位允许用户在同步模式下选择 SCLK 引脚上时钟输出的相位。它与 CPOL 位结合使用可获得所需
的时钟/数据关系(请参见图258 至259)
0:在时钟第一个变化沿捕获数据
1:在时钟第二个变化沿捕获数据
注意:该位不适用于 UART4 和 UART5。
位 8 LBCL:最后一个位时钟脉冲 (Last bit clock pulse)
该位允许用户在同步模式下选择与发送的最后一个数据位 (MSB) 关联的时钟脉冲是否必须在 SCLK
引脚上输出。
0:最后一个数据位的时钟脉冲不在 SCLK 引脚上输出
1:最后一个数据位的时钟脉冲在 SCLK 引脚上输出
注意:1:最后一位为发送的第 8 或第 9 个数据位,具体取决于 USART_CR1 寄存器中 M 位所选择的
8 位或 9 位格式。
2:该位不适用于 UART4 和 UART5。
位 7 保留,必须保持复位值
位 6 LBDIE:LIN 断路检测中断使能 (LIN break detection interrupt enable)
断路中断屏蔽(使用断路分隔符进行断路检测)
0:禁止中断
1:当 USART_SR 寄存器中 LBD = 1 时,生成中断
位 5 LBDL:lin 断路检测长度 (lin break detection length)
该位用于选择 11 位断路检测或 10 位断路检测。
0:10 位断路检测
1:11 位断路检测
位 4 保留,必须保持复位值
位 3:0 ADD[3:0]:USART 节点的地址
该位域用于指定 USART 节点的地址。
将在多处理器通信时于静音模式下使用该位域,以通过地址标记检测进行唤醒。
注意: 使能发送器时不应对这 3 个位(CPOL、CPHA、LBCL)进行写操作。
4.5.6 控制寄存器3(USART_CR3)
位 31:12 保留,必须保持复位值
位 11 ONEBIT:一个采样位方法使能 (One sample bit method enable)
该位允许用户选择采样方法。选择一个采样位方法后,将禁止噪声检测标志 (NF)。
0:三个采样位方法
1:一个采样位方法
位 10 CTSIE:CTS 中断使能 (CTS interrupt enable)
0:禁止中断
1:当 USART_SR 寄存器中 CTS = 1 时,生成中断
注意:该位不适用于 UART4 和 UART5。
位 9 CTSE: CTS 使能 (CTS enable)
0:
禁止 CTS 硬件流控制
1:
使能 CTS 模式,仅当 nCTS 输入有效 (连接到 0)时才发送数据。
如果在发送数据时使
nCTS 输入无效,会在停止之前完成发送。
如果使 nCTS 有效时数据已写入数据寄存器,则
将延迟发送,直到 nCTS 有效。
注意:该位不适用于 UART4 和 UART5。
位 8 RTSE: RTS 使能 (RTS enable)
0:
禁止 RTS 硬件流控制
1:
使能 RTS 中断,仅当接收缓冲区中有空间时才会请求数据。
发送完当前字符后应停止发
送数据。
可以接收数据时使 nRTS 输出有效 (连接到 0)。
注意:该位不适用于 UART4 和 UART5。
位 7 DMAT: DMA 使能发送器 (DMA enable transmitter)
该位由软件置 1/ 复位。
1:
针对发送使能 DMA 模式。
0:
针对发送禁止 DMA 模式。
位 6 DMAR: DMA 使能接收器 (DMA enable receiver)
该位由软件置 1/ 复位。
1:
针对接收使能 DMA 模式
0:针对接收禁止 DMA 模式
位 5 SCEN:智能卡模式使能 (Smartcard mode enable)
该位用于使能智能卡模式。
0:
禁止智能卡模式
1:使能智能卡模式
注意:该位不适用于 UART4 和 UART5。
位 4 NACK:智能卡 NACK 使能 (Smartcard NACK enable)
0:
出现奇偶校验错误时禁止 NACK 发送
1:出现奇偶校验错误时使能 NACK 发送
注意:该位不适用于 UART4 和 UART5。
位 3 HDSEL:半双工选择 (Half-duplex selection)
选择单线半双工模式
0:未选择半双工模式
1:选择半双工模式
位 2 IRLP:IrDA 低功耗 (IrDA low-power)
该位用于选择正常模式和低功耗 IrDA 模式
0:正常模式
1:低功耗模式
位 1 IREN:IrDA 模式使能 (IrDA mode enable)
此位由软件置 1 和清零。
0:禁止 IrDA
1:使能 IrDA
位 0 EIE:错误中断使能 (Error interrupt enable)
对于多缓冲区通信(USART_CR3 寄存器中 DMAR = 1),如果发生帧错误、上溢错误或出
现噪声标志(USART_SR 寄存器中 FE = 1 或 ORE = 1 或 NF = 1),则需要使用错误中断
使能位来使能中断生成。
0:禁止中断
1:当 USART_CR3 寄存器中的 DMAR = 1 并且 USART_SR 寄存器中的 FE = 1 或 ORE = 1
或 NF = 1 时,将生成中断
4.5.7 保护时间和预分频USART_GTPR)
位 31:16 保留,必须保持复位值
位 15:8 GT[7:0]:
保护时间值 (Guard time value)
该位域提供保护时间值 (以波特时钟数为单位)。
该位用于智能卡模式。
经过此保护时间后,发送完成标志置 1。
注意:该位不适用于 UART4 和 UART5。
位 7:0 PSC[7:0]:预分频器值
— 在 IrDA 低功耗模式下:
PSC[7:0] = IrDA 低功耗波特率
用于编程预分频器,进行系统时钟分频以获得低功耗频率:
使用寄存器中给出的值(8 个有效位)对源时钟进行分频:
00000000:保留 – 不编程此值
源时钟 1 分频
00000010:源时钟 2 分频
— 在正常 IrDA 模式下:PSC 必须设置为 00000001。
— 在智能卡模式下:
PSC[4:0]:预分频器值
用于编程预分频器,进行系统时钟分频以提供智能卡时钟。
将寄存器中给出的值(5 个有效位)乘以 2 得出源时钟频率的分频系数:
00000:保留 – 不编程此值
00001:源时钟 2 分频
00010:源时钟 4 分频
00011:源时钟 6 分频
注意:1:如果使用智能卡模式,则位 [7:5] 不起作用。
2:该位不适用于 UART4 和 UART5。

4.5.8 程序配置步骤
- RCC配置:要开相应的GPIO组时钟和相应的串口时钟;
- GPIO配置:在GPIO配置中,将发送端的管脚配置为复用推挽输出,将接收端的管脚配置为浮空输入;
- USART配置:USART寄存器配置;
- NVIC配置:设置串口的中断抢占优先级和亚优先级;
- 发送/接收数据:通过串口寄存器接收/发送数据。
4.5.9 实例代码
#include "usart1.h" // Device header
void USART1_Init(int bps)
{
GPIO_InitTypeDef U1_TXRX;
USART_InitTypeDef U1;
NVIC_InitTypeDef U1_NVIC;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource9,GPIO_AF_USART1);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource10,GPIO_AF_USART1);
//PA9-发送
U1_TXRX.GPIO_Mode =GPIO_Mode_AF;
U1_TXRX.GPIO_OType = GPIO_OType_PP;
U1_TXRX.GPIO_Pin = GPIO_Pin_9;
U1_TXRX.GPIO_PuPd = GPIO_PuPd_UP; //默认下拉
U1_TXRX.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA,&U1_TXRX); //根据U1_TXRX配置的参数进行初始化
//PA10-接收
U1_TXRX.GPIO_Mode =GPIO_Mode_AF;
U1_TXRX.GPIO_OType = GPIO_OType_PP;
U1_TXRX.GPIO_Pin = GPIO_Pin_10;
U1_TXRX.GPIO_PuPd = GPIO_PuPd_NOPULL; //默认下拉
U1_TXRX.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA,&U1_TXRX);
//USART
U1.USART_BaudRate=bps; //波特率
U1.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //无硬件流
U1.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //发送和接收
U1.USART_Parity=USART_Parity_No; //无校验
U1.USART_StopBits=USART_StopBits_1; //停止位
U1.USART_WordLength=USART_WordLength_8b; //数据位
USART_Init(USART1, &U1); //根据U1配置的参数进行初始化
//记得分开写中断
USART_ITConfig(USART1, USART_IT_RXNE,ENABLE); //使能串口接收中断
USART_ITConfig(USART1, USART_IT_IDLE,ENABLE); //使能串口空闲中断
U1_NVIC.NVIC_IRQChannel = USART1_IRQn;
U1_NVIC.NVIC_IRQChannelPreemptionPriority = 1; //抢占为1
U1_NVIC.NVIC_IRQChannelSubPriority = 1;//响应为0
U1_NVIC.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&U1_NVIC);
USART_Cmd(USART1, ENABLE); //使能串口进行工作
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void usart1_sendbyte(char data)
{
while(!(USART_GetFlagStatus(USART1,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART1,data);
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void usart1_sendstring(char *data)
{
while(*data != '\0')usart1_sendbyte(*data++); //循环发送,直到遇到\0,停止发送
}
/********************************************************************
函数功能:串口1接收读取1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
char usart1_readbyte(void)
{
while(!(USART_GetFlagStatus(USART1,USART_FLAG_RXNE))); //等待判断接收寄存器非空
return USART_ReceiveData(USART1); //读到的数据
}
/*
//接收中断
struct U1_DATA u1={0,0,0}; //定义结构体变量,同时初始化为0
char data;
//串口中断服务函数:每收到一个字符数据,CPU都会先暂停所有程序,自动执行一次USART1_IRQHandler函数
void USART1_IRQHandler(void)
{
data=USART_ReceiveData(USART1); //读数据并清除中断标志位
if(data=='\r' || data=='\n')
{
u1.ok_flag=1; //u1.ok_flag=1,说明接收完成
u1.buf[u1.len]='\0'; //添加结束符\0
}else {
u1.buf[u1.len++]=data; //没收到\r\n,就把数据存储到数组
}
}
*/
//接收中断+空闲中断
struct U1_DATA u1={0,0,0}; //定义结构体变量,同时初始化为0
char data;
//接收中断:每收到一个字符数据就会执行一次USART1_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次USART1_IRQHandler中断服务函数
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET) //接收中断,存储数据
{
u1.buf[u1.len]=USART_ReceiveData(USART1); //读数据并清除中断标志位
u1.len++; //自增,为下个数据存储做准备
}else if(USART_GetITStatus(USART1, USART_IT_IDLE)==SET) //空闲中断,接收数据结束
{
USART_ClearITPendingBit(USART1, USART_IT_IDLE); //读SR
USART_ReceiveData(USART1); //读DR
u1.ok_flag=1; //接收完成
u1.buf[u1.len]='\0'; //添加结束符
}
}
/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)
/* 定义 _sys_exit() 以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
/* 标准库需要的支持类型 */
struct __FILE
{
int handle;
};
FILE __stdout;
/* */
int fputc(int ch, FILE *stream)
{
/* 堵塞判断串口是否发送完成 */
usart1_sendbyte(ch); //串口发送一个字符
return ch;
}
5.系统滴答定时器
学习这个我们要知道定时器部分学习的三个目的:了解系统滴答定时器的作用是什么?掌握系统滴答定时器寄存器的配置;掌握系统滴答定时器的驱动软件设计。
5.1 系统滴答定时器概述
在我们的Cortex-M4当中,systick定时器被捆绑到我们的NVIC当中,用于产生systick异常。在我们STM32的SysTick定时器有两个可选的时钟源,一个是外部时钟源(STCLK,等于HCLK/8-仅限于stm32f10x),另一个是内核时钟(FCLK, 等于HCLK)。假若你选择内核时钟,并将HCLK频率设置为72Mhz的话,系统时钟周期为1/(168M); systick 有一个24位的递减计数器,每个系统时钟周期计数器值减一.那么当计数器减到零时,时间经过了:系统时钟周期*计数器初值。当你将计数器初值设为72000时,当计数器值减到0时经过了1/(72M)*(72000)=0. 001s,即1ms.
5.2 系统滴答定时器的作用
- 1.产生操作系统的时钟节拍;
SysTick定时器模捆绑在NVIC中,用于产生SYSTICK异常(异常号: 15)。大多操作系统需要一个硬件定时器来产生操作系统需要的滴等中断,作为整个系统的时基。因此,需要一个定时器来产生周期性的中断。而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
- 2.方便于不同处理器之间的程序移植;
Cortex”M4处理器内部包含了一个简单的定时器。因为所有的CM4芯片都带有这个定时器,软件在不同CM4器件间的移植工作得以化简。该定时器的时钟源可以是内部时钟(FCLK, CM4上的自由运行时钟),或者是外部时钟(CM4处理器上的STCLK信号)。不过,STCLK 的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能会大不相同,你需要检视芯片的器件手册来决定选择什么作为时钟源。SysTick定时器能产生中断,CM4为它专门开出一个异常类型,并且在向量表中有它的一席之地。它使操作系统和其它系统软件在CM4器件间的移植变得简单多了,因为在所
有CM4产品间对其处理都是相同的。
- 3.作为一个闹钟用来测量时间;
SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。要注意的是,当处理器在调试期间被喊停(halt) 时,则SysTick定时器亦将暂停运作。
5.3 系统滴答定时器寄存器
寄存器名 | 描述 |
CTRL | systick控制和状态寄存器 |
LOAD | systick重装载值寄存器 |
VAL | systick当前值寄存器 |
CALIB | systick校准值寄存器 |
更加详细的说明大家可以到相关固件库手册自行查阅哦!
5.5 代码实例
我们使用的是在裸机状态下完成的,因为在逻辑状态下的原因,所以可以使用系统滴答定时器产生精准延时;
#include "delay.h" // Device header
//系统滴答延时初始化
void SysTick_init(void)
{
SysTick->CTRL &=~(1<<2); //选择外部时钟源,systick时钟主频为168/8=21MHZ
}
//最大的us延时时间:24位最大值16777215/21=798915us,转换为ms,798.915ms
void delay_us(int us)
{
SysTick->LOAD = us*21; //1us*9个数,开始计数前,先填充好计数时间的值
SysTick->CTRL |=1<<0; //使能定时器,开始向下计数
SysTick->VAL=0; //清除0
while(!(SysTick->CTRL & 1<<16)); //等待之前填充好计数时间的值为0,延时时间到
SysTick->CTRL &=~(1<<0); //关闭定时器
SysTick->VAL=0; //清除0
}
//最大的us延时时间:24位最大值16777215/21=798915us,转换为ms,798.915ms
void delay_nms(int nms)
{
SysTick->LOAD = nms*21000; //1us*21000个数,开始计数前,先填充好计数时间的值
SysTick->CTRL |=1<<0; //使能定时器,开始向下计数
while(!(SysTick->CTRL & 1<<16)); //等待之前填充好计数时间的值为0,延时时间到
SysTick->CTRL &=~(1<<0); //关闭定时器
}
//函数功能:将ms进行扩大
void delay_ms(int ms)
{
int i;
int beishu=ms/500; //循环调用倍数
int yushu=ms%500; //循环调用余数
for(i=0;i<beishu;i++)
{
delay_nms(500);
}
if(yushu!=0)delay_nms(yushu); //余数
}
6.定时器
6.1 通用定时器
什么是通用定时器?是一个可以通过可编程预分频器驱动的16位自动装载计数器。可以用于很多场合,比如测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出和PWM)。使用定时器预分频器和RCC时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几毫秒间的调整。每个定时器都是完全独立的,没有互相共享的任何资源!
6.2 STM32F4系列通用定时器特点

- 16位向上、向下、向上/向下自动装载计数器
- 16位可编程预分频器,计数器时钟频率的分频系数为1-65536之间的任意数值;
- 4个独立通道:–输入捕获;一输出比较;一PWM生成(边缘或中间对齐模式);一单脉冲模式输出
- 死区时间可编程的互补输出
- 使用外部信 号控制定时 器和定时器互联的同步电路
- 允许在指定数目的计数器周期之后更新定时器寄存器的重复计数器
- 刹车输入信号可以将定时器输出信号置于复位状态或者一个已知状态
- 如下事件发生时产生中断/DMA :
—更新:计数器向上溢出向下滋出,计数器初始化(通过软件或者内部/外部触发)
一触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
一输入捕获
一输出比较
一刹车信号输入
- 支持计对定位的增量(正交)编码器和霍尔传感器电路
- 触发输入作为外部时钟或者按周期的电流管理
6.3 与基本定时器相关的寄存器
6.3.1 TIM6和TIM7控制寄存器1(TIMx_CR1)
位 15:8 保留,必须保持复位值。
位 7 ARPE:自动重载预装载使能 (Auto-reload preload enable)
0
:
TIMx_ARR
寄存器不进行缓冲。
1
:
TIMx_ARR
寄存器进行缓冲。
位 6:4 保留,必须保持复位值。
位 3 OPM:单脉冲模式 (One-pulse mode)
0
:计数器在发生更新事件时不会停止计数
1
:计数器在发生下一更新事件时停止计数(将
CEN
位清零)。
位 2 URS:更新请求源 (Update request source)
此位由软件置
1
和清零,用以选择
UEV
事件源。
0
:使能时,所有以下事件都会生成更新中断或
DMA
请求。此类事件包括:
—
计数器上溢
/
下溢
—
将
UG
位置
1
—
通过从模式控制器生成的更新事件
1
:使能时,只有计数器上溢
/
下溢会生成更新中断或
DMA
请求。
位 1 UDIS:更新禁止 (Update disable)
此位由软件置
1
和清零,用以使能
/
禁止
UEV
事件生成。
0
:使能
UEV
。更新
(UEV)
事件可通过以下事件之一生成:
—
计数器上溢
/
下溢
—
将
UG
位置
1
—
通过从模式控制器生成的更新事件
然后更新影子寄存器的值。
1
:禁止
UEV
。不会生成更新事件,各影子寄存器的值(
ARR
和
PSC
)保持不变。但如果将
UG
位置
1
,或者从从模式控制器接收到硬件复位,则会重新初始化计数器和预分频器。
位 0 CEN:计数器使能 (Counter enable)
0
:禁止计数器
1
:使能计数器
注意:只有事先通过软件将
CEN
位置
1
,才可以使用门控模式。而触发模式可通过硬件自动将
CEN
位置
1
。
在单脉冲模式下,当发生更新事件时会自动将
CEN
位清零。
6.3.2 TIM6和TIM7控制寄存器2(TIMx_CR2)
位 15:7 保留,必须保持复位值。
位 6:4 MMS:主模式选择 (Master mode selection)
这些位用于选择主模式下将要发送到从定时器以实现同步的信息
(TRGO)
。这些位的组合如下:
000
:
复位
——
TIMx_EGR
寄存器中的
UG
位用作触发输出
(TRGO)
。如果复位由触发输入
生成(从模式控制器配置为复位模式),则
TRGO
上的信号相比实际复位会有延迟。
001
:
使能
——计数器使能信号
(CNT_EN)
用作触发输出
(TRGO)
。该触发输出可用于同时
启动多个定时器,或者控制在一段时间内使能从定时器。计数器使能信号由
CEN
控制位与
门控模式下的触发输入的逻辑或运算组合而成。
当计数器使能信号由触发输入控制时,
TRGO
上会存在延迟,选择主
/
从模式时除外(请参见
TIMx_SMCR
寄存器中对
MSM
位的说明)。
010
:
更新
——选择更新事件作为触发输出
(TRGO)
。例如,主定时器可用作从定时器的预分
频器。
位 3:0 保留,必须保持复位值。
6.3.3 TIM6 和 TIM7 DMA/中断使能寄存器 (TIMx_DIER)
位 15:9 保留,必须保持复位值
位 8 UDE:更新 DMA 请求使能 (Update DMA request enable)
0
:禁止更新
DMA
请求。
1
:使能更新
DMA
请求。
位 7:1 保留,必须保持复位值
位 0 UIE:更新中断使能 (Update interrupt enable)
0
:禁止更新中断。
1
:使能更新中断。
6.3.4 TIM6和TIM7状态寄存器(TIMx_SR)
位 15:1 保留,必须保持复位值。
位 0 UIF:更新中断标志 (Update interrupt flag)
该位在发生更新事件时通过硬件置
1
。但需要通过软件清零。
0
:未发生更新。
1
:更新中断挂起。该位在以下情况下更新寄存器时由硬件置
1
:
—
上溢或下溢并且当
TIMx_CR1
寄存器中
UDIS = 0
时。
—
当由于
TIMx_CR1
寄存器中
URS = 0
且
UDIS = 0
而通过软件使用
TIMx_EGR
寄存器中
的
UG
位重新初始化
CNT
时。
6.3.5 TIM6和TIM7事件生产寄存器(TIMx_EGR)
位 15:1 保留,必须保持复位值。
位 0 UG:更新生成 (Update generation)
该位可通过软件置
1
,并由硬件自动清零。
0
:不执行任何操作。
1
:重新初始化定时器计数器并生成寄存器更新事件。请注意,预分频器计数器也将清零(但
预分频比不受影响)。
6.3.6 TIM6和TIM7计数器(TIMx_CNT)
6.3.7 TIM6和TIM7预分频器(TIMx_PSC)
位 15:0 PSC[15:0]:预分频器值 (Prescaler value)
计数器时钟频率
CK_CNT
等于
f
CK_PSC
/ (PSC[15:0] + 1)
。
PSC
包含在每次发生更新事件时要装载到实际预分频器寄存器的值。
6.3.8 TIM6和TIM7自动重载寄存器(TIMx_ARR)
位 15:0 ARR[15:0]:自动重载值 (Auto-reload value)
ARR
为要装载到实际自动重载寄存器的值。
当自动重载值为空时,计数器不工作
6.4 实例代码
#include "tim4.h" // Device header
#include "stdio.h"
void TIM4_Init(u16 psc,u16 arr)
{
TIM_TimeBaseInitTypeDef tim4;
NVIC_InitTypeDef NVIC_tim4;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //使能定时器4时钟
tim4.TIM_ClockDivision=TIM_CKD_DIV1;
tim4.TIM_CounterMode=TIM_CounterMode_Down;
tim4.TIM_Period=arr;
tim4.TIM_Prescaler=psc-1;
tim4.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM4, &tim4); //初始化定时器4的配置
TIM_ITConfig(TIM4,TIM_IT_Update, ENABLE); //使能更新中断
NVIC_tim4.NVIC_IRQChannel=TIM4_IRQn; //设置的NVIC优先级的中断编号
NVIC_tim4.NVIC_IRQChannelCmd=ENABLE;
NVIC_tim4.NVIC_IRQChannelPreemptionPriority=2; //抢占优先级
NVIC_tim4.NVIC_IRQChannelSubPriority=0; //响应优先级
NVIC_Init(&NVIC_tim4);
TIM_Cmd(TIM4, ENABLE);
}
//中断服务函数
void TIM4_IRQHandler(void)
{
TIM_ClearITPendingBit(TIM4, TIM_IT_Update); //清除更新中断
printf("1S时间到\r\n");
}
7.RTC实时时钟
7.1 简介
什么是实时时钟?实时时钟RTC是一个独立的定时器。RTC模块拥有一组连续计数的计数器,在相应软件配置下,可以供应时钟日历的功能。修改计数器的数值可以重新设置当前的时间和日期。RTC模块和时钟配置系统(RCC_BDCR寄存器)处于后备区域,即在系统复位或待机模式唤醒以后,RTC的设置和事件维持不变。RTC由自带的电源引脚VBat供电,VBat可以和蓄电池相互连接,也可以和外部的3.3V电源引脚相连接或保持断开!
PS:RTC是一个系统定时器中独立出来的设备,用于设置系统时钟,提供报警器或周期性的定时器。
7.2 STM32F4系列RTC特点
- 包含里秒、秒,分钟、小时( 12/24 小时制)、星期儿,日期、月份和年份的日历。
- 软件可编程的夏令时补偿。
- 两个具有中断功能的可编程网钟。可通过任意日历字段的组合驱动闹钟。
- 自动唤醒单元,可周期性地生成标志以触发自动唤醒中断。
- 参考时钟检测:可使用更加精确的第二时钟源(50 Hz或60 Hz)来提高日历的精确度。
- 利用亚秒级移位特性与外部时钟实现精确同步。
- 可屏蔽中断/事件:
- 一闹钟A;
- –闹钟B
- 唤醒中断
- -时间戳
- 一入侵检测
- 数字校准电路(周期性计数器调整)
- 一精度为5ppa
- -精度为0. 95 ppa,在数秒钟的校准窗口中获取
- 用于事件保存的时间戳功能( 1个事件)
- 入侵检测:
- 一2个带可能置过速器和内部上拉的入侵事件
- 20个备份寄存器80字节) .发生入侵检测事件时,将复位备份寄存器
- 复用功能输出 (RTC_OUT),可选择以下两个输出之一:
— RTC_CALIB
:
512 Hz
或
1 Hz
时钟输出(
LSE
频率为
32.768 kHz)
。
可通过将
RTC_CR
寄存器中的
COE[23]
位置
1
来使能此输出。该输出可连接到器
件
RTC_AF1
功能。
— RTC_ALARM
(闹钟
A
、闹钟
B
或唤醒)。
可通过配置
RTC_CR
寄存器的
OSEL[1:0]
位选择此输出。该输出可连接到器件
RTC_AF1
功能。
- RTC 复用功能输入:
— RTC_TS
:时间戳事件检测。该输入可连接到器件
RTC_AF1
和
RTC_AF2
功能。
— RTC_TAMP1
:
TAMPER1
事件检测。该输入可连接到器件
RTC_AF1
和
RTC_AF2
功能。
— RTC_TAMP2
:
TAMPER2
事件检测。
— RTC_REFIN
:参考时钟输入(通常为市电,
50 Hz
或
60 Hz
)。

7.3 RTC 初始化和配置
7.3.1 寄存器访问
RTC
寄存器为
32
位寄存器。除了当
BYPSHAD=0
时对日历影子寄存器执行的读访问之外,
APB
接口会在访问
RTC
寄存器时引入
2
个等待周期。
7.3.2 寄存器写保护
系统复位后,可通过
PWR
电源控制寄存器
(PWR_CR)
的
DBP
位保护
RTC
寄存器以防止
非正常的写访问。必须将
DBP
位置
1
才能使能
RTC
寄存器的写访问。
上电复位后,所有
RTC
寄存器均受到写保护。通过向写保护寄存器
(RTC_WPR)
写入一个
密钥来使能对
RTC
寄存器的写操作。
要解锁所有
RTC
寄存器(
RTC_ISR[13:8]
、
RTC_TAFCR
和
RTC_BKPxR
除外)的写保护,
需要执行以下步骤:
1. 将“0xCA
”写入
RTC_WPR
寄存器。
2. 将“0x53
”写入
RTC_WPR
寄存器。
写入一个错误的关键字会再次激活写保护。
保护机制不受系统复位影响
7.3.3 日历初始化和配置
要编程包括时间格式和预分频器配置在内的初始时间和日期日历值,需按照以下顺序操作:
- 将 RTC_ISR 寄存器中的 INIT 位置 1 以进入初始化模式。在此模式下,日历计数器将停
止工作并且其值可更新。
- 轮询 RTC_ISR 寄存器中的 INITF 位。当 INITF 置 1 时进入初始化阶段模式。大约需要
2
个
RTCCLK
时钟周期(由于时钟同步)。
- 要为日历计数器生成 1 Hz 时钟,应首先编程 RTC_PRER 寄存器中的同步预分频系数,
然后编程异步预分频系数。即使只需要更改这两个字段中之一,也必须对
RTC_PRER
寄存器执行两次单独的写访问。
- 在影子寄存器(RTC_TR 和 RTC_DR)中加载初始时间和日期值,然后通过 RTC_CR
寄存器中的
FMT
位配置时间格式(
12
或
24
小时制)。
- 通过清零 INIT 位退出初始化模式。随后,自动加载实际日历计数器值,在 4 个 RTCCLK
时钟周期后重新开始计数。
当初始化序列完成之后,日历开始计数。
7.3.4 夏令时
可通过
RTC_CR
寄存器的
SUB1H
、
ADD1H
和
BKP
位管理夏令时。 利用 SUB1H
或
ADD1H
,软件只需单次操作便可在日历中减去或增加一个小时,无需执行整个初始化步骤。 此外,软件还可以使用 BKP
位来记录是否曾经执行过此操作。
7.4 RTC 相关寄存器
7.4.1 RTC 时间寄存器(RTC_TR)
位 31-24 保留
位 23 保留,必须保持复位值。
位 22 PM:AM/PM 符号 (AM/PM notation)
0
:
AM
或
24
小时制
1
:
PM
位 21:20 HT[1:0]:小时的十位(BCD 格式)(Hour tens in BCD format)
位 16:16 HU[3:0]:小时的个位(BCD 格式)(Hour units in BCD format)
位 15 保留,必须保持复位值。
位 14:12 MNT[2:0]:分钟的十位(BCD 格式)(Minute tens in BCD format)
位 11:8 MNU[3:0]:分钟的个位(BCD 格式)(Minute units in BCD format)
位 7 保留,必须保持复位值。
位 6:4 ST[2:0]:秒的十位(BCD 格式)(Second tens in BCD format)
位 3:0 SU[3:0]:秒的个位(BCD 格式)(Second units in BCD format)
7.4.2 RTC日期寄存器(RTC_DR)
位 31-24 保留
位 23:20 YT[3:0]:年份的十位(BCD 格式)(Year tens in BCD format)
位 19:16 YU[3:0]:年份的个位(BCD 格式)(Year units in BCD format)
位 15:13 WDU[2:0]:星期几的个位 (Week day units)
000
:禁止
001
:星期一
…
111
:星期日
位 12 MT:月份的十位(BCD 格式)(Month tens in BCD format)
位 11:8 MU:月份的个位(BCD 格式)(Month units in BCD format)
位 7:6 保留,必须保持复位值。
位 5:4 DT[1:0]:日期的十位(BCD 格式)(Date tens in BCD format)
位 3:0 DU[3:0]:日期的个位(BCD 格式)(Date units in BCD format)
7.4.3 RTC控制寄存器
位 31:24 保留,必须保持复位值
位 23 COE:校准输出使能 (Calibration output enable)
该位使能
RTC_CALIB
输出
0
:禁止校准输出
1
:使能校准输出
位 22:21 OSEL[1:0]:输出选择 (Output selection)
这些位用于选择要连接到
RTC_ALARM
输出的标志
00
:禁止输出
01
:使能闹钟
A
输出
10
:使能闹钟
B
输出
11
:使能唤醒输出
位 20 POL:输出极性 (Output polarity)
该位用于配置
RTC_ALARM
输出的极性
0
:当
ALRAF/ALRBF/WUTF
置
1
时(取决于
OSEL[1:0]
),该引脚为高电平
1
:当
ALRAF/ALRBF/WUTF
置
1
时(取决于
OSEL[1:0]
),该引脚为低电平
位 19 COSEL:校准输出选择 (Calibration output selection)
当
COE=1
时,该位可选择
RTC_CALIB
上输出的信号。
0
:校准输出为
512 Hz
1
:校准输出为
1 Hz
在
RTCCLK
为
32.768 kHz
且预分频器为其默认值(
PREDIV_A=127
且
PREDIV_S=255
)
的条件下,这些频率有效。
位 18 BKP:备份 (Backup)
用户可对此位执行写操作以记录是否已对夏令时进行更改。
位 17 SUB1H:减少 1 小时(冬季时间更改)(Subtract 1 hour (winter time change))
当该位在初始化模式以外的模式下置
1
时,如果当前小时不是
0
,则日历时间将减少
1
小
时。此位始终读为
0
。
当前小时为
0
时,将此位置
1
没有任何作用。
0
:无作用。
1
:将当前时间减少
1
小时。这可用于冬季时间更改。
位 16 ADD1H:增加 1 小时(夏季时间更改)(Add 1 hour (summer time change))
当该位在初始化模式以外的模式下置
1
时,日历时间将增加
1
小时。此位始终读为
0
。
0
:无作用。
1
:将当前时间增加
1
小时。这可用于夏季时间更改
位 15 TSIE:时间戳中断使能 (Timestamp interrupt enable)
0
:禁止时间戳中断
1
:使能时间戳中断
位 14 WUTIE:使能唤醒定时器使能 (Wakeup timer interrupt enable)
0
:禁止唤醒定时器中断
1
:使能唤醒定时器中断
位 13 ALRBIE:闹钟 B 中断使能 (Alarm B interrupt enable)
0
:闹钟
B
中断禁止
1
:闹钟
B
中断使能
位 12 ALRAIE:闹钟 A 中断使能 (Alarm A interrupt enable)
0
:禁止闹钟
A
中断
1
:使能闹钟
A
中断
位 11 TSE:时间戳使能 (Time stamp enable)
0
:禁止时间戳
1
:使能时间戳
位 10 WUTE:唤醒定时器使能 (Wakeup timer enable)
0
:禁止唤醒定时器
1
:使能唤醒定时器
位 9 ALRBE:闹钟 B 使能 (Alarm B enable)
0
:禁止闹钟
B
1
:使能闹钟
B
位 8 ALRAE:闹钟 A 使能 (Alarm A enable)
0
:禁止闹钟
A
1
:使能闹钟
A
位 7 DCE:粗略数字校准使能 (Coarse digital calibration enable)
0
:禁止数字校准
1
:使能数字校准
PREDIV_A
必须大于或等于
6
位 6 FMT:小时格式 (Hour format)
0
:
24
小时
/
天格式
1
:
AM/PM
小时格式
位 5 BYPSHAD:旁路影子寄存器 (Bypass the shadow registers)
0
:日历值(从
RTC_SSR
、
RTC_TR
和
RTC_DR
读取时)取自影子寄存器,该影子寄存器
每两个
RTCCLK
周期更新一次。
1
:日历值(从
RTC_SSR
、
RTC_TR
和
RTC_DR
读取时)直接取自日历计数器。
注意:如果
APB1
时钟的频率低于
7
倍的
RTCCLK
频率,则必须将
BYPSHAD
置“
1
”
位 4 REFCKON:参考时钟检测使能(50 Hz 或 60 Hz)(Reference clock detection enable (50 or
60 Hz))
0
:禁止参考时钟检测
1
:使能参考时钟检测
注意:
PREDIV_S
必须为
0x00FF
。
位 3 TSEDGE:时间戳事件有效边沿 (Timestamp event active edge)
0
:
TIMESTAMP
上升沿生成时间戳事件
1
:
TIMESTAMP
下降沿生成时间戳事件
TSEDGE
发生更改时,必须复位
TSE
以避免将
TSF
意外置
1
位 2:0 WUCKSEL[2:0]:唤醒时钟选择 (Wakeup clock selection)
000
:选择
RTC/16
时钟
001
:选择
RTC/8
时钟
010
:选择
RTC/4
时钟
011
:选择
RTC/2
时钟
10x
:选择
ck_spre
时钟(通常为
1 Hz
)
11x
:选择
ck_spre
时钟(通常为
1 Hz
)并将
WUT
计数器值增加
2
16
(见下面的注释)
7.4.4 RTC初始化和状态寄存器(RTC_ISR)
位 31:17 保留
位 16 RECALPF:重新校准挂起标志 (Recalibration pending Flag)
当软件对
RTC_CALR
寄存器执行写操作时,
RECALPF
状态标志将自动置“
1
”,指示
RTC_CALR
寄存器已屏蔽。当采用新的校准设置时,该位恢复为“
0
”。请参见
动态重校准
一节。
位 15 保留,必须保持复位值。
位 14 TAMP2F:TAMPER2 检测标志 (TAMPER2 detection flag)
在入侵输入
2
上检测到入侵检测事件时,由硬件将此标志置
1
。
该标志由软件写零清除。
位 13 TAMP1F:入侵检测标志 (Tamper detection flag)
当检测到入侵检测事件时,由硬件将此标志置
1
。
该标志由软件写零清除。
位 12 TSOVF:时间戳溢出标志 (Timestamp overflow flag)
当在
TSF
已置
1
的情况下发生时间戳事件时,由硬件将此标志置
1
。
该标志由软件写零清除。建议仅在
TSF
位清零之后再检查并清零
TSOVF
位。否则,如果时
间戳事件恰好在清零
TSF
位之前刚刚发生,则溢出事件可能会被漏掉。
位 11 TSF:时间戳标志 (Timestamp flag)
发生时间戳事件时,由硬件将此标志置
1
。
该标志由软件写零清除。
位 10 WUTF:唤醒定时器标志 (Wakeup timer flag)
当唤醒自动重载计数器计数到
0
时,由硬件将此标志置
1
。
该标志由软件写零清除。
软件必须在
WUTF
再次置
1
的
1.5
个
RTCCLK
周期之前将该标志清零。
位 9 ALRBF:闹钟 B 标志 (Alarm B flag)
当时间
/
日期寄存器(
RTC_TR
和
RTC_DR
)与闹钟
B
寄存器
(RTC_ALRMBR)
匹配时,由
硬件将该标志置
1
。
该标志由软件写零清除。
位 8 ALRAF:闹钟 A 标志 (Alarm A flag)
当时间
/
日期寄存器(
RTC_TR
和
RTC_DR
)与闹钟
A
寄存器
(RTC_ALRMAR)
匹配时,由
硬件将该标志置
1
。
该标志由软件写零清除。
位 7 INIT:初始化模式 (Initialization mode)
0
:自由运行模式。
1
:初始化模式,用于编程时间和日期寄存器(
RTC_TR
和
RTC_DR
)以及预分频器寄存器
(RTC_PRER)
。计数器停止计数,当
INIT
被复位后,计数器从新值开始计数。
位 6 INITF:初始化标志 (Initialization flag)
当此位置
1
时,
RTC
处于初始化状态,此时可更新事件、日期和预分频器寄存器。
0
:不允许更新日历寄存器。
1
:允许更新日历寄存器。
位 5 RSF:寄存器同步标志 (Registers synchronization flag)
每次将日历寄存器的值复制到影子寄存器(
RTC_SSRx
、
RTC_TRx
和
RTC_DRx
)时,都
会由硬件将此位置
1
。在初始化模式下、平移操作挂起时
(SHPF=1)
或在旁路影子寄存器模
式
(BYPSHAD=1)
下,该位由硬件清零。该位还可由软件清零。
0
:日历影子寄存器尚未同步
1
:日历影子寄存器已同步
位 4 INITS:初始化状态标志 (Initialization status flag)
当日历年份字段不为
0
时(上电复位状态),由硬件将该位置
1
。
0
:日历尚未初始化
1
:日历已经初始化
位 3 SHPF:平移操作挂起 (Shift operation pending)
0
:没有平移操作挂起
1
:某个平移操作挂起
只要通过对
RTC_SHIFTR
寄存器执行写操作来启动平移操作,此标志便由硬件置
1
。执行完
相应的平移操作后,此标志由硬件清零。对
SHPF
执行写入操作不起作用。
位 2 WUTWF:唤醒定时器写标志 (Wakeup timer write flag)
在
RTC_CR
寄存器中的
WUTE
位置
0
后,当唤醒定时器值可更改时,由硬件将该位置
1
。
0
:不允许更新唤醒定时器配置
1
:允许更新唤醒定时器配置
位 1 ALRBWF:闹钟 B 写标志 (Alarm B write flag)
在
RTC_CR
寄存器中的
ALRBIE
位置
0
之后,当闹钟
B
的值可更改时,由硬件将该位置
1
。
该位在初始化模式下由硬件清零。
0
:不允许更新闹钟
B
1
:允许更新闹钟
B
位 0 ALRAWF:闹钟 A 写标志 (Alarm A write flag)
在
RTC_CR
寄存器中的
ALRAE
位置
0
后,当闹钟
A
的值可更改时,由硬件将该位置
1
。
该位在初始化模式下由硬件清零。
0
:不允许更新闹钟
A
1
:允许更新闹钟
A
7.4.5 RTC预分频寄存器(TRC_PRER)
位 31:24 保留
位 23 保留,必须保持复位值。
位 22:16 PREDIV_A[6:0]:异步预分频系数 (Asynchronous prescaler factor)
下面是异步分频系数的公式:
ck_apre
频率
= RTCCLK
频率
/(PREDIV_A+1)
注意:
PREDIV_A [6:0]= 000000
为禁用值。
位 15 保留,必须保持复位值。
位 14:0 PREDIV_S[14:0]:同步预分频系数 (Synchronous prescaler factor)
下面是同步分频系数的公式:
ck_spre
频率
= ck_apre
频率
/(PREDIV_S+1)
7.4.6 RTC唤醒定时器寄存器(RTC_WUTR)
位 31:16 保留
位 15:0 WUT[15:0]:唤醒自动重载值位 (Wakeup auto-reload value bit)
当使能唤醒定时器时(
WUTE
置
1
),每
(WUT[15:0] + 1)
个
ck_wut
周期将
WUTF
标志置
1
一次。
ck_wut
周期通过
RTC_CR
寄存器的
WUCKSEL[2:0]
位进行选择。
当
WUCKSEL[2] = 1
时,唤醒定时器变为
17
位,
WUCKSEL[1]
等效为
WUT[16]
,即要重载
到定时器的最高有效位。
注意:
WUTF
第一次置
1
发生在
WUTE
置
1
之后
(WUT+1)
个
ck_wut
周期。禁止在
WUCKSEL[2:0]=011(RTCCLK/2)
时将
WUT[15:0]
设置为
0x0000
。
7.4.7 RTC校准寄存器(RTC_CALIBR)
位 31:8 保留
位 7 DCS:数字校准符号 (Digital calibration sign)
0
:正校准:增加日历更新频率
1
:负校准:降低日历更新频率
位 6:5 保留,必须保持复位值。
位 4:0 DC[4:0]:数字校准 (Digital calibration)
DCS = 0
(正校准)
00000
:
+ 0 ppm
00001
:
+ 4 ppm
(舍入值)
00010
:
+ 8 ppm
(舍入值)
..
11111
:
+ 126 ppm
(舍入值)
DCS = 1
(负校准)
00000
:
0 ppm
00001
:
2 ppm
(舍入值)
00010
:
4 ppm
(舍入值)
..
11111
:
63 ppm
(舍入值)
7.4.8 RTC闹钟A寄存器(RTC_ALRMAR)
位 31 MSK4:闹钟 A 日期掩码 (Alarm A date mask)
0
:如果日期
/
日匹配,则闹钟
A
置
1
1
:在闹钟
A
比较中,日期
/
日无关
位 30 WDSEL:星期几选择 (Week day selection)
0
:
DU[3:0]
代表日期的个位
1
:
DU[3:0]
代表星期几。
DT[1:0]
为无关位。
位 29:28 DT[1:0]:日期的十位(BCD 格式)(Date tens in BCD format)。
位 27:24 DU[3:0]:日期的个位或日(BCD 格式)(Date units or day in BCD format)。
位 23 MSK3:闹钟 A 小时掩码 (Alarm A hours mask)
0
:如果小时匹配,则闹钟
A
置
1
1
:在闹钟
A
比较中,小时无关
位 22 PM:AM/PM 符号 (AM/PM notation)
0
:
AM
或
24
小时制
1
:
PM
位 21:20 HT[1:0]:小时的十位(BCD 格式)(Hour tens in BCD format)。
位 19:16 HU[3:0]:小时的个位(BCD 格式)(Hour units in BCD format)。
位 15 MSK2:闹钟 A 分钟掩码 (Alarm A minutes mask)
0
:如果分钟匹配,则闹钟
A
置
1
1
:在闹钟
A
比较中,分钟无关
位 14:12 MNT[2:0]:分钟的十位(BCD 格式)(Minute tens in BCD format)。
位 11:8 MNU[3:0]:分钟的个位(BCD 格式)(Minute units in BCD format)。
位 7 MSK1:闹钟 A 秒掩码 (Alarm A seconds mask)
0
:如果秒匹配,则闹钟
A
置
1
1
:在闹钟
A
比较中,秒无关
位 6:4 ST[2:0]:秒的十位(BCD 格式)(Second tens in BCD format)。
位 3:0 SU[3:0]:秒的个位(BCD 格式)(Second units in BCD format)
7.4.9 RTC闹钟B寄存器(RTC_ALRMBR)
位 31 MSK4:闹钟 B 日期掩码 (Alarm B date mask)
0
:如果日期和日匹配,则闹钟
B
置
1
1
:在闹钟
B
比较中,日期和日无关
位 30 WDSEL:星期几选择 (Week day selection)
0
:
DU[3:0]
代表日期的个位。
1
:
DU[3:0]
代表星期几。
DT[1:0]
为无关位。
位 29:28 DT[1:0]:日期的十位(BCD 格式)(Date tens in BCD format)
位 27:24 DU[3:0]:日期个位或日(BCD 格式)(Date units or day in BCD format)
位 23 MSK3:闹钟 B 小时掩码 (Alarm B hours mask)
0
:如果小时匹配,则闹钟
B
置
1
1
:在闹钟
B
比较中,小时无关
位 22 PM:AM/PM 符号 (AM/PM notation)
0
:
AM
或
24
小时制
1
:
PM
位 21:20 HT[1:0]:小时的十位(BCD 格式)(Hour tens in BCD format)
位 19:16 HU[3:0]:小时的个位(BCD 格式)(Hour units in BCD format)
位 15 MSK2:闹钟 B 分钟掩码 (Alarm B minutes mask)
0
:如果分钟匹配,则闹钟
B
置
1
1
:在闹钟
B
比较中,分钟无关
位 14:12 MNT[2:0]:分钟的十位(BCD 格式)(Minute tens in BCD format)
位 11:8 MNU[3:0]:分钟的个位(BCD 格式)(Minute units in BCD format)
位 7 MSK1:闹钟 B 秒掩码 (Alarm B seconds mask)
0
:如果秒匹配,则闹钟
B
置
1
1
:在闹钟
B
比较中,秒无关
位 6:4 ST[2:0]:秒的十位(BCD 格式)(Second tens in BCD format)
位 3:0 SU[3:0]:秒的个位(BCD 格式)(Second units in BCD format)
7.4.10 RTC写保护寄存器(RTC_WPR)
上电后,所有 RTC 寄存器都受到写保护( RTC_ISR[13:8]、 RTC_TAFCR 和 RTC_BKPxR 除外),必须依次写入: 0XCA、 0X53 两关键字到 RTC_WPR 寄存 器,才可以解锁。
位 31:8 保留,必须保持复位值。
位 7:0 KEY:写保护关键字 (Write protection key)
可通过软件对该字节执行写操作。
读取该字节时,始终返回
0x00
。
7.5 实例代码(初始化时间和日期)
#include "rtc.h" // Device header
#include "stdio.h"
void RTC_Time_Init(u16 Year, u16 month, u16 day,u16 hour,u16 minute,u16 second,u16 WeekDay)
{
RTC_InitTypeDef rtc;
RTC_TimeTypeDef rtcTime;
RTC_DateTypeDef rtcDate;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //PWR控制写保护的电源/备份区电源
PWR_BackupAccessCmd(ENABLE); //使能PWR备份域电源
PWR_BackupRegulatorCmd(ENABLE); //使能RTC备份寄存器
//1、选择RTC时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //RTC选择LSE外部低速时钟
RCC_RTCCLKCmd(ENABLE); //使能RTC时钟
RCC_LSEConfig(RCC_LSE_ON); //打开LSE时钟
while(!(RCC_GetFlagStatus(RCC_FLAG_LSERDY)==SET)); //等待LSE时钟精密校准完成
while(!(RTC_WaitForSynchro() == SUCCESS)); //等待LSE时钟和RTC同步完成
RTC_WriteProtectionCmd(DISABLE); //写DISABLE,是失能写保护功能
while(!(RTC_EnterInitMode() == SUCCESS) ); //等待进入初始化模式完成
//初始化RTC
rtc.RTC_AsynchPrediv= 127; //异步,寄存器已经+1
rtc.RTC_HourFormat=RTC_HourFormat_24; //24小时制
rtc.RTC_SynchPrediv=255; //同步,寄存器已经+1
RTC_Init(&rtc); //初始化RTC
//设置时间
rtcTime.RTC_Hours=hour; //时
rtcTime.RTC_Minutes=minute; //分
rtcTime.RTC_Seconds=second; //秒
RTC_SetTime(RTC_Format_BIN, &rtcTime);
//设置日期
rtcDate.RTC_Year=Year; //年
rtcDate.RTC_Month=month; //月
rtcDate.RTC_Date=day; //日
rtcDate.RTC_WeekDay=WeekDay; //星期
RTC_SetDate(RTC_Format_BIN, &rtcDate);
RTC_WriteProtectionCmd(ENABLE); //开启写保护功能
}
7.6 代码实现(RTC备份区–复位不修改时间)
#include "rtc.h" // Device header
#include "stdio.h"
void RTC_Time_Init(u16 Year, u16 month, u16 day,u16 hour,u16 minute,u16 second,u16 WeekDay)
{
RTC_InitTypeDef rtc;
RTC_TimeTypeDef rtcTime;
RTC_DateTypeDef rtcDate;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //PWR控制写保护的电源/备份区电源
PWR_BackupAccessCmd(ENABLE); //使能PWR备份域电源
PWR_BackupRegulatorCmd(ENABLE); //使能RTC备份寄存器
//1、选择RTC时钟源为LSE
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //RTC选择LSE外部低速时钟
RCC_RTCCLKCmd(ENABLE); //使能RTC时钟
//2、打开LSE时钟
RCC_LSEConfig(RCC_LSE_ON);
while(!(RCC_GetFlagStatus(RCC_FLAG_LSERDY)==SET)); //等待LSE时钟校准完成
while(!(RTC_WaitForSynchro() == SUCCESS)); //等待LSE时钟和RTC进行同步完成
RTC_WriteProtectionCmd(DISABLE); //写DISABLE,是失能写保护功能
if(RTC_ReadBackupRegister(RTC_BKP_DR0)!=0xaa) //读备份区,判断是否初始化过
{
while(!(RTC_EnterInitMode() == SUCCESS) ); //使能初始化之前,必须先关闭写保护,等待进入初始化模式完成
//初始化RTC
rtc.RTC_AsynchPrediv= 127; //异步,寄存器已经+1
rtc.RTC_HourFormat=RTC_HourFormat_24; //24小时制
rtc.RTC_SynchPrediv=255; //同步,寄存器已经+1
RTC_Init(&rtc); //初始化RTC
//设置时间
rtcTime.RTC_Hours=hour; //时
rtcTime.RTC_Minutes=minute; //分
rtcTime.RTC_Seconds=second; //秒
RTC_SetTime(RTC_Format_BIN, &rtcTime);
//设置日期
rtcDate.RTC_Year=Year; //年
rtcDate.RTC_Month=month; //月
rtcDate.RTC_Date=day; //日
rtcDate.RTC_WeekDay=WeekDay; //星期
RTC_SetDate(RTC_Format_BIN, &rtcDate);
RTC_WriteBackupRegister(RTC_BKP_DR0, 0xaa); //写入备份区0xaa,记录已经初始化化过一次了
}
RTC_WriteProtectionCmd(ENABLE); //开启写保护功能
}
复位后时间没有改变的;
7.7 代码实现(LCD显示+动态修改RTC时间日期)
#include "rtc.h" // Device header
#include "stdio.h"
void RTC_Time_Init(u16 Year, u16 month, u16 day,u16 hour,u16 minute,u16 second,u16 WeekDay)
{
RTC_InitTypeDef rtc;
RTC_TimeTypeDef rtcTime;
RTC_DateTypeDef rtcDate;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE); //PWR控制写保护的电源/备份区电源
PWR_BackupAccessCmd(ENABLE); //使能PWR备份域电源
PWR_BackupRegulatorCmd(ENABLE); //使能RTC备份寄存器
//1、选择RTC时钟源为LSE
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //RTC选择LSE外部低速时钟
RCC_RTCCLKCmd(ENABLE); //使能RTC时钟
//2、打开LSE时钟
RCC_LSEConfig(RCC_LSE_ON);
while(!(RCC_GetFlagStatus(RCC_FLAG_LSERDY)==SET)); //等待LSE时钟校准完成
while(!(RTC_WaitForSynchro() == SUCCESS)); //等待LSE时钟和RTC进行同步完成
RTC_WriteProtectionCmd(DISABLE); //写DISABLE,是失能写保护功能
if(RTC_ReadBackupRegister(RTC_BKP_DR0)!=0xaa) //读备份区,判断是否初始化过
{
while(!(RTC_EnterInitMode() == SUCCESS) ); //使能初始化之前,必须先关闭写保护,等待进入初始化模式完成
//初始化RTC
rtc.RTC_AsynchPrediv= 127; //异步,寄存器已经+1
rtc.RTC_HourFormat=RTC_HourFormat_24; //24小时制
rtc.RTC_SynchPrediv=255; //同步,寄存器已经+1
RTC_Init(&rtc); //初始化RTC
//设置时间
rtcTime.RTC_Hours=hour; //时
rtcTime.RTC_Minutes=minute; //分
rtcTime.RTC_Seconds=second; //秒
RTC_SetTime(RTC_Format_BIN, &rtcTime);
//设置日期
rtcDate.RTC_Year=Year; //年
rtcDate.RTC_Month=month; //月
rtcDate.RTC_Date=day; //日
rtcDate.RTC_WeekDay=WeekDay; //星期
RTC_SetDate(RTC_Format_BIN, &rtcDate);
RTC_WriteBackupRegister(RTC_BKP_DR0, 0xaa); //写入备份区0xaa,记录已经初始化化过一次了
}
RTC_WriteProtectionCmd(ENABLE); //开启写保护功能
}
void RTC_Set_time(u16 hour,u16 minute,u16 second)
{
RTC_TimeTypeDef rtcTime;
RTC_WriteProtectionCmd(DISABLE); //写DISABLE,是失能写保护功能
while(!(RTC_EnterInitMode() == SUCCESS) ); //使能初始化之前,必须先关闭写保护,等待进入初始化模式完成
//设置时间
rtcTime.RTC_Hours=hour; //时
rtcTime.RTC_Minutes=minute; //分
rtcTime.RTC_Seconds=second; //秒
RTC_SetTime(RTC_Format_BIN, &rtcTime);
RTC_WriteProtectionCmd(ENABLE); //开启写保护功能
}
void RTC_Set_date(u16 Year, u16 month, u16 day,u16 WeekDay)
{
RTC_DateTypeDef rtcDate;
RTC_WriteProtectionCmd(DISABLE); //写DISABLE,是失能写保护功能
while(!(RTC_EnterInitMode() == SUCCESS) ); //使能初始化之前,必须先关闭写保护,等待进入初始化模式完成
rtcDate.RTC_Year=Year; //年
rtcDate.RTC_Month=month; //月
rtcDate.RTC_Date=day; //日
rtcDate.RTC_WeekDay=WeekDay; //星期
RTC_SetDate(RTC_Format_BIN, &rtcDate);
RTC_WriteProtectionCmd(ENABLE); //开启写保护功能
}
8 .看门狗
8.1 看门狗简介
此器件具有两个嵌入式看门狗外设,具有安全性高、定时准确及使用灵活的优点。两个看门
狗外设(独立和窗口)均可用于检测并解决由软件错误导致的故障;当计数器达到给定的超
时值时,触发一个中断(仅适用于窗口型看门狗)或产生系统复位。注意:独立看门狗没有中断,但是可以触发中断服务函数。
8.2 看门狗的作用
看门狗,本质上是一个定时器,这个定时器有一个输出端,可以输出复位信号。一般情况下,看门狗定时器是给一个比较大的初始值,然后从这个值开始递减操作,当减到0时候,就会发出复位信号,复位CPU。我们可以在程序正常运行的地方周期性的重置计数器值,只要能在计数器减到0前把值重新设置,这个动作称为喂狗,这样就不会复位了。但是,如果程序有BUG,有可能在某些时候程序脱离正常的运行路线,称为程序跑飞了,这样,定时器由于得不到重新初始化,有机会递减到0,产生复位信号。复位后,程序从头开始执行。所以,看门狗起到了监控CPU运行,防止MCU死机的作用。
总结:看门狗起到了监控CPU运行,防止MCU死机的作用。

8.3 看门狗的选择
独立看门狗用于精度低的场合,窗口看门狗用于精度高的场合。
8.4 独立看门狗
8.4.1 时钟
8.4.2 LS时钟
8.4.3 特性
● 自由运行递减计数器;
● 时钟由独立 RC 振荡器提供(可在待机和停止模式下运行);
● 当递减计数器值达到 0x000 时产生复位(如果看门狗已激活);
8.4.4 预分频
8.5 独立看门狗初始化
#include "iwdg.h" // Device header
void IWDG_Init(u16 PR,u16 RLR)
{
RCC_LSICmd(ENABLE);
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); //关闭写保护
IWDG_SetPrescaler(PR); //预分频
IWDG_SetReload(RLR); //重装载值
IWDG_Enable(); //启动独立看门狗
IWDG_ReloadCounter(); //启动一次喂狗,写入0xAAAA,重装载的值才会被传递到计计数器
}
8.6 窗口看门狗
8.6.1 特点
窗口看门狗和独立看门狗不一样的地方在于它具备中断;
可编程的自由运行递减计数器;
复位条件:当递减计数器值小于 0x40 时复位(如果看门狗已激活);在窗口之外重载递减计数器时复位(如果看门狗已激活)。
中断触发条件:提前唤醒中断 (EWI):当递减计数器等于 0x40 时触发(如果已使能且看门狗已激活)。
8.6.2 原理
除非递减计数器的值在 T6 位变成 0 前被刷新,看门狗电路在
达到预置的时间周期时,会产生一个 MCU 复位。如果在递减计数器达到窗口寄存器值之前
刷新控制寄存器中的 7 位递减计数器值,也会产生 MCU 复位。这意味着必须在限定的时间
窗口内刷新计数器。
8.6.3 窗口看门狗初始化
- 时钟
- 使用的函数
- 分频
- 设置窗口值;
- 使能看门狗中断;
使能看门狗时,需要设置计数值,但是计数值不能大于窗口值 ;
- 程序初始化
- 在中断中喂狗;
8.6.4 源码程序
#include "wwdg.h" // Device header
#include "stdio.h"
int n;
void WWDG_Init(u8 WindowValue,u8 Counter)
{
NVIC_InitTypeDef WWDG_NVIC;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); //使能时钟
WWDG_SetPrescaler(WWDG_Prescaler_8); //8分频 WWDG counter clock = (PCLK1/4096)/8
WWDG_SetWindowValue(WindowValue); //设置窗口值
WWDG_NVIC.NVIC_IRQChannel = WWDG_IRQn;
WWDG_NVIC.NVIC_IRQChannelPreemptionPriority = 0; //抢占为0
WWDG_NVIC.NVIC_IRQChannelSubPriority = 0;//响应为0
WWDG_NVIC.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&WWDG_NVIC);
n=Counter;
WWDG_EnableIT(); //使能窗口看门狗中断
WWDG_Enable(Counter); //使能看门狗,设置计数值,传递大于0x40的值,防止初始化后产生一次复位
}
//窗口看门狗中断
void WWDG_IRQHandler(void)
{
WWDG_SetCounter(n); //设置计数值,喂狗
WWDG_ClearFlag(); //EWIF: 提前唤醒中断标志,清中断标志位
//printf("喂狗了\r\n"); //2-3ms
}
9. RS485
学习该内容的目的:学会使用485实现一主多从的数据通信控制
9.1 概念
学会使用485实现一主多从的数据通信控制;
9.2 物理(硬件)接口标准
按照电平分类可以分成TTL、RS232、RS422、RS485,下面逐一介绍;
9.2.1 TTL电平
高电平:2.4-5V;51单片机工作电压是5V;IO口输出高电平就是5V;
低电平:0~0.4v;
数字电路中,由TTL电子元器件组成电路使用的电平。电平是个电压范围,规定输出高电平>2.4V,输出低电平<0.4V。在室温下,一般输出高电平是3.5V,输出低电平是0.2V。最小输入高电平和低电平:输入高电平>=2.0V,输入低电平<=0.8V,噪声容限是0.4V。
我们之前使用的USART(UART),默认为TTL电平,我们单片机的I/O输入输出的电平是和TTL兼容;USART(UART)按3.3V来说,正常1米之内。
9.2.2 RS232
高电平:电平为逻辑“ 1”时: -3V~-15V;
低电平:电平为逻辑“ 0”时: +3V~+15V;
传输距离:50英尺,一英尺=0.38米;50英尺=15.24米;
传输速度:20Kbps,一收一发模式
9.2.3 RS422
标准:1收10发;传输距离4000英尺=1219.2米
9.2.4 RS485
RS-485 标准是为弥补 RS-232 通信距离短、速率低等缺点而产生的。 RS-485 标准只规
定了平衡发送器和接收器的电特性,而没有规定接插件、传输电缆和应用层通信协议。
发送器和接收器:
高电平:+2-+6V;低电平:-2–6V;
性能参数:
电缆最大距离:4000*0.3048=1219米;传输速度90Kbps;
RS485芯片:作为一种常用的通讯接口器件, RS-485/RS-422 芯片可以在许多半导体公司的“标准接口器件”栏目中“收发器”类元件中找到对应的型号;比如 Sipex 公司(器件前缀为 SP)、
Maxim 公司(器件前缀为 MAX)、 TI 公司(器件前缀为 SN)、 Intersil 公司(器件前缀为 ISL 或
LTC)等各大半导体公司。
9.3 一主一从通信
9.3.1 主机接线
引脚连接
发送数据:DE引脚使能为高电平,可以发送,同时把RE接收关闭,输出高电平;
接收数据:RE引脚使能为低电平,可以接收,同时把DE发送关闭,输出低电平
PG8引脚输出:高电平发送数据;低电平接收数据
9.3.2 丛机接线
9.3.3 主从机接线原理
9.4 主机程序
#include "rs485.h" // Device header
#define RE_DE PGout(8) //发送/接收器共用一个引脚
void RS485_Init(int bps)
{
GPIO_InitTypeDef u2_TXRX,RE;
USART_InitTypeDef u2;
NVIC_InitTypeDef u2_NVIC;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOG,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2);
//PG8-发送/接收器使能
RE.GPIO_Mode =GPIO_Mode_OUT;
RE.GPIO_OType = GPIO_OType_PP;
RE.GPIO_Pin = GPIO_Pin_8;
RE.GPIO_PuPd = GPIO_PuPd_DOWN; //默认下拉
RE.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOG,&RE);
//PA9-发送
u2_TXRX.GPIO_Mode =GPIO_Mode_AF;
u2_TXRX.GPIO_OType = GPIO_OType_PP;
u2_TXRX.GPIO_Pin = GPIO_Pin_2;
u2_TXRX.GPIO_PuPd = GPIO_PuPd_UP; //默认下拉
u2_TXRX.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA,&u2_TXRX); //根据u2_TXRX配置的参数进行初始化
//PA10-接收
u2_TXRX.GPIO_Mode =GPIO_Mode_AF;
u2_TXRX.GPIO_OType = GPIO_OType_PP;
u2_TXRX.GPIO_Pin = GPIO_Pin_3;
u2_TXRX.GPIO_PuPd = GPIO_PuPd_NOPULL; //默认下拉
u2_TXRX.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA,&u2_TXRX);
//USART
u2.USART_BaudRate=bps; //波特率
u2.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //无硬件流
u2.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //发送和接收
u2.USART_Parity=USART_Parity_No; //无校验
u2.USART_StopBits=USART_StopBits_1; //停止位
u2.USART_WordLength=USART_WordLength_8b; //数据位
USART_Init(USART2, &u2); //根据u2配置的参数进行初始化
//记得分开写中断
USART_ITConfig(USART2, USART_IT_RXNE,ENABLE); //使能串口接收中断
USART_ITConfig(USART2, USART_IT_IDLE,ENABLE); //使能串口空闲中断
u2_NVIC.NVIC_IRQChannel = USART2_IRQn;
u2_NVIC.NVIC_IRQChannelPreemptionPriority = 1; //抢占为1
u2_NVIC.NVIC_IRQChannelSubPriority = 1;//响应为0
u2_NVIC.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&u2_NVIC);
USART_Cmd(USART2, ENABLE); //使能串口进行工作
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendbyte(char data)
{
RE_DE=1; //使能485发送
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART2,data);
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待为空,说明发送完毕
RE_DE=0; //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendstring(char *data)
{
RE_DE=1; //使能485发送
while(*data != '\0')//循环发送,直到遇到\0,停止发送
{
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART2,*data++);
}
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待字符串最后一个数据发送完毕
RE_DE=0; //关闭发送,默认接收
}
//接收中断+空闲中断
struct U2_DATA rs485={0,0,0}; //定义结构体变量,同时初始化为0
//接收中断:每收到一个字符数据就会执行一次USART2_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次USART2_IRQHandler中断服务函数
void USART2_IRQHandler(void)
{
if(USART_GetITStatus(USART2, USART_IT_RXNE)==SET) //接收中断,存储数据
{
rs485.buf[rs485.len]=USART_ReceiveData(USART2); //读数据并清除中断标志位
rs485.len++; //自增,为下个数据存储做准备
}else if(USART_GetITStatus(USART2, USART_IT_IDLE)==SET) //空闲中断,接收数据结束
{
USART_ClearITPendingBit(USART2, USART_IT_IDLE); //读SR
USART_ReceiveData(USART2); //读DR
rs485.ok_flag=1; //接收完成
rs485.buf[rs485.len]='\0'; //添加结束符
}
}
9.5 从机程序
#include "rs485.h" // Device header
#include "delay.h"
#define RE_DE PAout(12)
void RS485_Init(int bps)
{
GPIO_InitTypeDef U3_TXRX,re;
USART_InitTypeDef U3;
NVIC_InitTypeDef NVIC_U3;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB, ENABLE); //RCC
re.GPIO_Mode=GPIO_Mode_Out_PP; //复用推挽输出模式 GPIO
re.GPIO_Pin=GPIO_Pin_12; //引脚9
re.GPIO_Speed=GPIO_Speed_50MHz; //最高速50Mhz
GPIO_Init(GPIOA, &re); //根据U1_TXRX配置的参数进行初始化
//PB10发送
U3_TXRX.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出模式 GPIO
U3_TXRX.GPIO_Pin=GPIO_Pin_10; //引脚9
U3_TXRX.GPIO_Speed=GPIO_Speed_50MHz; //最高速50Mhz
GPIO_Init(GPIOB, &U3_TXRX); //根据U1_TXRX配置的参数进行初始化
//PB11-接收
U3_TXRX.GPIO_Mode=GPIO_Mode_IPU; //上拉输入模式
U3_TXRX.GPIO_Pin=GPIO_Pin_11; //引脚10
U3_TXRX.GPIO_Speed=GPIO_Speed_50MHz; //最高速50Mhz
GPIO_Init(GPIOB, &U3_TXRX); //根据U1_TXRX配置的参数进行初始化
//USART
U3.USART_BaudRate=bps; //波特率
U3.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //无硬件流
U3.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //发送和接收
U3.USART_Parity=USART_Parity_No; //无校验
U3.USART_StopBits=USART_StopBits_1; //停止位
U3.USART_WordLength=USART_WordLength_8b; //数据位
USART_Init(USART3, &U3); //根据U1配置的参数进行初始化
NVIC_U3.NVIC_IRQChannel=USART3_IRQn; //设置的NVIC优先级的中断编号
NVIC_U3.NVIC_IRQChannelCmd=ENABLE;
NVIC_U3.NVIC_IRQChannelPreemptionPriority=2; //抢占优先级
NVIC_U3.NVIC_IRQChannelSubPriority=0; //响应优先级
NVIC_Init(&NVIC_U3);
//记得分开写中断
USART_ITConfig(USART3, USART_IT_RXNE,ENABLE); //使能串口接收中断
USART_ITConfig(USART3, USART_IT_IDLE,ENABLE); //使能串口空闲中断
//NVIC_EnableIRQ(37); //使能RS485中断编号
USART_Cmd(USART3, ENABLE); //使能串口进行工作
RE_DE=0; //默认接收数据
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendbyte(char data)
{
RE_DE=1;//使能485发送
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART3,data);
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待为空,发送完成
RE_DE=0; //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendstring(char *data)
{
RE_DE=1; //使能485发送
while(*data != '\0')//循环发送,直到遇到\0,停止发送
{
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART3,*data++);
}
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待字符串最后一个数据发送完毕
RE_DE=0; //关闭发送,默认接收
}
// 原来的
//接收中断+空闲中断
struct U3_DATA rs485={0,0,0}; //定义结构体变量,同时初始化为0
//接收中断:每收到一个字符数据就会执行一次RS485_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次RS485_IRQHandler中断服务函数
void USART3_IRQHandler(void)
{
if(USART_GetITStatus(USART3, USART_IT_RXNE)==SET) //接收中断,存储数据
{
rs485.buf[rs485.len]=USART_ReceiveData(USART3); //读数据并清除中断标志位
rs485.len++; //自增,为下个数据存储做准备
}else if(USART_GetITStatus(USART3, USART_IT_IDLE)==SET) //空闲中断,接收数据结束
{
USART_ClearITPendingBit(USART3, USART_IT_IDLE); //读SR
USART_ReceiveData(USART3); //读DR
rs485.ok_flag=1; //接收完成
rs485.buf[rs485.len]='\0'; //添加结束符
}
}
9.6 一主多从+自定义协议
9.6.1 搭建自定义协议
通讯协议又称通信规程,是指通信双方对数据传送控制的一种约定。约定中包括对数据格式,同步方式,传送速度,传送步骤,检纠错方式以及控制字符定义等问题做出统一规定,通信双方必须共同遵守。
搭建自定义协议的话,那么自己写的程序,就根据自己制度的要求来
9.6.2 常见的协议规则
帧数据(数据包):
常见的数据包结构
- 帧头:一帧数据开始,可以使用多个字节。 假如以一个字节:0XFF —-自己定义。
- 地址信息:跟哪个设备进行通信(类型于IIC器件地址) —0X01 0X02 芯片ID;比如我们现在是一主多从,那么这个地址信息可以用于表示从机的ID编号
- 数据类型:数据类型:如 0x01 –代表发的是字符。 0x02 –代表16进制 —- 类似命令;
- 数据长度:数据长度: — 如果发送的数据大于256个字节,至少用2个字节表示 0x00 0x05;
- 数据块:数据,发送的数据;
- 检验码:求和,或者适应CRC16/CRC8等校验算法;
- 帧尾:一帧数据结束,可以使用多个字节。 假如以一个字节:0XFE —-自己定义。
定义从机协议结构体:
9.6.3 RS485自定义协议+1主多从通信-按键控制-单次数据通信程序
主机:
#include "rs485.h" // Device header
#define RE_DE PGout(8) //发送/接收器共用一个引脚
void RS485_Init(int bps)
{
GPIO_InitTypeDef u2_TXRX,RE;
USART_InitTypeDef u2;
NVIC_InitTypeDef u2_NVIC;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOG,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource2,GPIO_AF_USART2);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource3,GPIO_AF_USART2);
//PG8-发送/接收器使能
RE.GPIO_Mode =GPIO_Mode_OUT;
RE.GPIO_OType = GPIO_OType_PP;
RE.GPIO_Pin = GPIO_Pin_8;
RE.GPIO_PuPd = GPIO_PuPd_DOWN; //默认下拉
RE.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOG,&RE);
//PA9-发送
u2_TXRX.GPIO_Mode =GPIO_Mode_AF;
u2_TXRX.GPIO_OType = GPIO_OType_PP;
u2_TXRX.GPIO_Pin = GPIO_Pin_2;
u2_TXRX.GPIO_PuPd = GPIO_PuPd_UP; //默认下拉
u2_TXRX.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA,&u2_TXRX); //根据u2_TXRX配置的参数进行初始化
//PA10-接收
u2_TXRX.GPIO_Mode =GPIO_Mode_AF;
u2_TXRX.GPIO_OType = GPIO_OType_PP;
u2_TXRX.GPIO_Pin = GPIO_Pin_3;
u2_TXRX.GPIO_PuPd = GPIO_PuPd_NOPULL; //默认下拉
u2_TXRX.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA,&u2_TXRX);
//USART
u2.USART_BaudRate=bps; //波特率
u2.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //无硬件流
u2.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //发送和接收
u2.USART_Parity=USART_Parity_No; //无校验
u2.USART_StopBits=USART_StopBits_1; //停止位
u2.USART_WordLength=USART_WordLength_8b; //数据位
USART_Init(USART2, &u2); //根据u2配置的参数进行初始化
//记得分开写中断
USART_ITConfig(USART2, USART_IT_RXNE,ENABLE); //使能串口接收中断
USART_ITConfig(USART2, USART_IT_IDLE,ENABLE); //使能串口空闲中断
u2_NVIC.NVIC_IRQChannel = USART2_IRQn;
u2_NVIC.NVIC_IRQChannelPreemptionPriority = 1; //抢占为1
u2_NVIC.NVIC_IRQChannelSubPriority = 1;//响应为0
u2_NVIC.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&u2_NVIC);
USART_Cmd(USART2, ENABLE); //使能串口进行工作
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendbyte(char data)
{
RE_DE=1; //使能485发送
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART2,data);
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待为空,说明发送完毕
RE_DE=0; //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendstring(char *data)
{
RE_DE=1; //使能485发送
while(*data != '\0')//循环发送,直到遇到\0,停止发送
{
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART2,*data++);
}
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待字符串最后一个数据发送完毕
RE_DE=0; //关闭发送,默认接收
}
void RS485_SendLenString(char *data,u16 len)
{
RE_DE=1; //使能485发送
while(len)//循环发送,直到遇到\0,停止发送
{
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART2,*data++);
len--;
}
while(!(USART_GetFlagStatus(USART2,USART_FLAG_TC))); //等待字符串最后一个数据发送完毕
RE_DE=0; //关闭发送,默认接收
}
//接收中断+空闲中断
struct U2_DATA rs485={0,0,0}; //定义结构体变量,同时初始化为0
//接收中断:每收到一个字符数据就会执行一次USART2_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次USART2_IRQHandler中断服务函数
void USART2_IRQHandler(void)
{
if(USART_GetITStatus(USART2, USART_IT_RXNE)==SET) //接收中断,存储数据
{
rs485.buf[rs485.len]=USART_ReceiveData(USART2); //读数据并清除中断标志位
rs485.len++; //自增,为下个数据存储做准备
}else if(USART_GetITStatus(USART2, USART_IT_IDLE)==SET) //空闲中断,接收数据结束
{
USART_ClearITPendingBit(USART2, USART_IT_IDLE); //读SR
USART_ReceiveData(USART2); //读DR
rs485.ok_flag=1; //接收完成
rs485.buf[rs485.len]='\0'; //添加结束符
}
}
//Slave s1={0xfd,1,0,0,{0},0,0xfe}; //定义结构体,并初始化
Master M1={0xfd,0,0,0xfe};
//发送数据包
void Master_SendDataPackage(u8 id,u8 cmd)
{
M1.id=id; //呼叫从机1
M1.cmd=cmd; //上传温度、光照、有毒气体
//void RS485_SendLenString(char *data,u16 len)
RS485_sendbyte(M1.head); //帧头
RS485_sendbyte(M1.id); //从机id=0x2c
RS485_sendbyte(M1.cmd); //命令
RS485_sendbyte(M1.end); //帧尾
}
main.c
#include "main.h"
//extern const unsigned char gImage_image[307200];
XPT2046 xy;
RTC_TimeTypeDef time; //时间
RTC_DateTypeDef date; //日期
int main(void)
{
int i;
u8 M1_check=0;
float wendu=0,shidu=0;
u8 key_number=0;
u8 buf[2];
char rtc_buf[64];
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //NVIC 组2,抢2占2
SysTick_init(); //系统滴答延时
LED_Init(); //LED
beep_Init(); //蜂鸣器
KEY_Init();
//KEY_Exti_Init(); //按键外部中断
USART1_Init(115200); //串口初始化115200
//LED1_PWM_Init(8400,100) ; //168分频8400=21000hz=2.1khz 100为比较最大值
//LED2_PWM_Init(8400,100); //168分频8400=21000hz=2.1khz 100为比较最大值
W25Q64_SPI1_Init(); //w25q64初始化
W25Q64_ReadID(buf); //读ID
printf("W25Q64 ID:0x%x 0x%x\r\n",buf[0],buf[1]);
LCD_ILI9486_Init(); //LCD初始化
Touch_Init(); //触摸屏初始化
delay_ms(1000);
touch_adjust(); //触摸屏校准
printf("校准成功\r\n");
RTC_Time_Init(22,8,12,11,18,50,5);
//WWDG_Init(127,125); //窗口值127,计数器值为125,即初始化值,也是喂狗值,不能大于窗口值127,否则也会触发复位
RS485_Init(115200);
//TIM4_Init(8400,9000); //定时0.9S,进行喂狗
//IWDG_Init(IWDG_Prescaler_32,1000); //1S之内要喂狗,预分频为32,32Khz/32=1khz,1S=1khz,
//重装载值1个数为1ms,写入1000表示1000ms内要喂狗,否则会产生复位
// for(i=0;i<15;i++)
// {
// delay_ms(1000);
// if(FONT_FLAG==1) break;
// }
//
// if(FONT_FLAG==1 && i < 10) //开机10S内按下按键,触发下载字库操作,执行下载功能
// {
// W25Q64_Write_GB2312_ASCII(); //下载字库
// }
//LCD_DrawRectangle(10, 150,60,200,RED); //显示一个矩形
while (1)
{
key_number=key_scan();
if(key_number==1)
{
Master_SendDataPackage(1,0xff);
printf("呼叫从机1\r\n");
}else if(key_number==2)
{
Master_SendDataPackage(2,0xff);
printf("呼叫从机2\r\n");
}
//主机使用触摸屏控制,选择采集对应的从机数据并显示
if(rs485.ok_flag==1) //收到从机返回数据
{
printf("主机收到从机%d返回的数据\r\n",rs485.buf[1]);
if(rs485.buf[0] == M1.head && rs485.buf[rs485.len-1] == M1.end) //帧头和帧尾正确
{
M1_check=0; //校验码清0
for(i=3;i<15;i++)
{
M1_check+=rs485.buf[i];
//printf("主机下标%d值=%d\r\n",i,rs485.buf[i]); //收到的数据
//printf("计算和值=%d\r\n",M1_check); //收到的数据
}
//printf("主机计算出来的校验码:%d\r\n",M1_check);
//printf("从机校验码:%d\r\n",rs485.buf[rs485.len-2]);
if(M1_check==rs485.buf[rs485.len-2])
{
printf("校验ok\r\n");
shidu=rs485.buf[3]+(rs485.buf[4]/100.0); //56.000000
wendu=rs485.buf[5]+(rs485.buf[6]/100.0); //28.560000
printf("从机%d 温度:%.2f 湿度:%.2f\r\n",rs485.buf[1],wendu,shidu);
}
}
rs485.ok_flag=0;
rs485.len=0;
memset(rs485.buf,0,256); //清0
}
/*
delay_ms(900);
RTC_GetTime(RTC_Format_BIN, &time);
RTC_GetDate(RTC_Format_BIN, &date);
printf("%d年%d月%d日 %d时 %d分 %d秒\r\n",date.RTC_Year,date.RTC_Month,date.RTC_Date,time.RTC_Hours,time.RTC_Minutes,time.RTC_Seconds);
sprintf(rtc_buf,"20%d年%d月%d日 %02d时%02d分%02d秒",date.RTC_Year,date.RTC_Month,date.RTC_Date,time.RTC_Hours,time.RTC_Minutes,time.RTC_Seconds);
LCD_Show_GB2312_ASCII(100,20,WHITE,RED,rtc_buf);
if(T_PEN==0)
{
get_xpt2046_adjust_xyval(&xy); //获取校准后的坐标值
if((xy.xval >=10 && xy.xval<=60) && (xy.yval >=150 && xy.yval<=200))
{
printf("按下界面按钮\r\n");
RTC_Set_time(15,30,52); //修改时间
}
printf("x轴:%d y轴:%d\r\n",xy.xval,xy.yval);
while(!T_PEN)delay_ms(10);
}
*/
/*
for(i=0;i<100;i++)
{
delay_ms(10);
TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断增加,越来越亮
TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
}
for(i=100;i>=0;i--)
{
delay_ms(10);
TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断减少,越来越暗
TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
}
*/
/*
if(T_PEN==0){
get_xpt2046_adjust_xyval(&xy); //获取校准后的坐标值
if((xy.xval >=10 && xy.xval<=60) && (xy.yval >=150 && xy.yval<=200))
{
printf("按下界面按钮\r\n");
}
printf("x轴:%d y轴:%d\r\n",xy.xval,xy.yval);
while(!T_PEN)delay_ms(10);
}
*/
//触摸屏+LCD作业:
// if(u1.ok_flag==1) //说明串口接收完数据
// {
// printf("%s\r\n",u1.buf); //发送一串数据到电脑回显
// //清0
// u1.ok_flag=0;
// u1.len=0;
// memset(u1.buf,0,sizeof(u1.buf));
// }
/*
for(i=0;i<100;i++)
{
delay_ms(10);
TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断增加,越来越亮
TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
}
for(i=100;i>=0;i--)
{
delay_ms(10);
TIM_SetCompare1(TIM10,i); //修改占空比,低电平占空比不断减少,越来越暗
TIM_SetCompare1(TIM14,i); //修改占空比,低电平占空比不断增加,越来越亮
}
*/
}
}
从机
485.c
#include "rs485.h" // Device header
#include "delay.h"
#define RE_DE PAout(12)
void RS485_Init(int bps)
{
GPIO_InitTypeDef U3_TXRX,re;
USART_InitTypeDef U3;
NVIC_InitTypeDef NVIC_U3;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOB, ENABLE); //RCC
re.GPIO_Mode=GPIO_Mode_Out_PP; //复用推挽输出模式 GPIO
re.GPIO_Pin=GPIO_Pin_12; //引脚9
re.GPIO_Speed=GPIO_Speed_50MHz; //最高速50Mhz
GPIO_Init(GPIOA, &re); //根据U1_TXRX配置的参数进行初始化
//PB10发送
U3_TXRX.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出模式 GPIO
U3_TXRX.GPIO_Pin=GPIO_Pin_10; //引脚9
U3_TXRX.GPIO_Speed=GPIO_Speed_50MHz; //最高速50Mhz
GPIO_Init(GPIOB, &U3_TXRX); //根据U1_TXRX配置的参数进行初始化
//PB11-接收
U3_TXRX.GPIO_Mode=GPIO_Mode_IPU; //上拉输入模式
U3_TXRX.GPIO_Pin=GPIO_Pin_11; //引脚10
U3_TXRX.GPIO_Speed=GPIO_Speed_50MHz; //最高速50Mhz
GPIO_Init(GPIOB, &U3_TXRX); //根据U1_TXRX配置的参数进行初始化
//USART
U3.USART_BaudRate=bps; //波特率
U3.USART_HardwareFlowControl=USART_HardwareFlowControl_None; //无硬件流
U3.USART_Mode=USART_Mode_Tx|USART_Mode_Rx; //发送和接收
U3.USART_Parity=USART_Parity_No; //无校验
U3.USART_StopBits=USART_StopBits_1; //停止位
U3.USART_WordLength=USART_WordLength_8b; //数据位
USART_Init(USART3, &U3); //根据U1配置的参数进行初始化
NVIC_U3.NVIC_IRQChannel=USART3_IRQn; //设置的NVIC优先级的中断编号
NVIC_U3.NVIC_IRQChannelCmd=ENABLE;
NVIC_U3.NVIC_IRQChannelPreemptionPriority=2; //抢占优先级
NVIC_U3.NVIC_IRQChannelSubPriority=0; //响应优先级
NVIC_Init(&NVIC_U3);
//记得分开写中断
USART_ITConfig(USART3, USART_IT_RXNE,ENABLE); //使能串口接收中断
USART_ITConfig(USART3, USART_IT_IDLE,ENABLE); //使能串口空闲中断
//NVIC_EnableIRQ(37); //使能RS485中断编号
USART_Cmd(USART3, ENABLE); //使能串口进行工作
RE_DE=0; //默认接收数据
}
/********************************************************************
函数功能:串口1发送1个字节的字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendbyte(u8 data)
{
RE_DE=1;//使能485发送
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART3,data);
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待为空,发送完成
RE_DE=0; //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_sendstring(u8 *data)
{
RE_DE=1; //使能485发送
while(*data != '\0')//循环发送,直到遇到\0,停止发送
{
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待判断发送寄存器为空
USART_SendData(USART3,*data++);
}
while(!(USART_GetFlagStatus(USART3,USART_FLAG_TC))); //等待字符串最后一个数据发送完毕
RE_DE=0; //关闭发送,默认接收
}
/********************************************************************
函数功能:串口1发送一串字符数据
参数1:data:你要发送的数据
参数2:无
返回值:无
作者:xxx
********************************************************************/
void RS485_SendLenString(u8 *data,u16 len)
{
while(len) //循环发送指定长度的数据
{
//printf("%d\r\n",*data);
RS485_sendbyte( *data++);
len--;
}
}
/*
typedef struct
{
u8 head; //帧头----0xfd
u16 id; //表示从机的编号
u8 cmd; //控制字,比如0xff就是从机上传温度,光照、有毒其它等等数据 0:不采集数据,1:温度 2:光照 3:有毒气体 0xff:所有的
u8 len; //数据长度
u8 buf[256]; //发送的数据
u8 check; //校验码
u8 end; //帧尾
}Slave; //从机协议
*/
Slave s1={0xfd,1,0,{0},0,0xfe}; //定义结构体,并初始化
// 原来的
//接收中断+空闲中断
struct U3_DATA rs485={0,0,0}; //定义结构体变量,同时初始化为0
//接收中断:每收到一个字符数据就会执行一次RS485_IRQHandler中断服务函数
//空闲中断:收完数据之后,串口产生空闲,会自动执行一次RS485_IRQHandler中断服务函数
void USART3_IRQHandler(void)
{
if(USART_GetITStatus(USART3, USART_IT_RXNE)==SET) //接收中断,存储数据
{
rs485.buf[rs485.len]=USART_ReceiveData(USART3); //读数据并清除中断标志位
rs485.len++; //自增,为下个数据存储做准备
}else if(USART_GetITStatus(USART3, USART_IT_IDLE)==SET) //空闲中断,接收数据结束
{
USART_ClearITPendingBit(USART3, USART_IT_IDLE); //读SR
USART_ReceiveData(USART3); //读DR
rs485.ok_flag=1; //接收完成
rs485.buf[rs485.len]='\0'; //添加结束符
}
}
//发送数据包
void RS485_SendDatapackage(void)
{
RS485_sendbyte(s1.head); //发送帧头
RS485_sendbyte(s1.id); //从机ID
RS485_sendbyte(s1.len); //数据长度
RS485_SendLenString(s1.buf,s1.len); //发送指定长度数据
RS485_sendbyte(s1.check); //校验码
RS485_sendbyte(s1.end); //帧尾
}
从机的main.c
#include "stm32f10x.h"
#include "delay.h"
#include "beep.h"
#include "led.h"
#include "key.h"
#include "usart1.h"
#include "cm3_bit.h"
#include "dht11.h"
#include "my_oled.h"
#include "image.h"
#include "w25q16.h"
#include "gz.h"
#include "adc1.h"
#include "dma1.h"
#include "string.h"
#include "rs485.h"
#include "tim4.h"
#include "pwm.h"
#define BEEP PCout(3)
int main(void) //主函数
{
int i;
char buf=0;
float cpu_temp,mq135_val,mq2_val,gz_val; //转换后的CPU温度
u8 size_len=0;
u16 cpu_val=0; //得到CPU的ad值
u16 dma_buf[4]; //保存DMA搬运数据
u8 temp=0;
u8 data_buf[4]={0};
float wendu=0,shidu=0;
u8 wsd_buf[32];
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //组2:2位抢占,2位响应
systick_init(); //系统滴答延时
BEEP_Init(); //beep初始化
//LED_Init(); //LED灯
//KEY_Init(); //普通按键
USART1_Init(115200); //串口1
DHT11_Init(); //温湿度初始化
DHT11_GetTempHumidity(data_buf);
OLED_Init(); //OLED初始化
W25Q16_Init();
// EXTI_KEY_Init(); //外部中断-PA0
// for(i=0;i<15;i++)
// {
// delay_ms(1000);
// if(FONT_FLAG==1) break;
// }
//
// if(FONT_FLAG==1 && i < 10) //开机10S内按下按键,触发下载字库操作,执行下载功能
// {
//
// W25Q16_Write_GB2312(); //下载字库
//
// }
//CPU_MQ2_135_GZ_ADC1Init(); //PA2-MQ2:ADC1—_IN2 PA1-MQ135:ADC1—_IN1
//CPU_DMA1_ADC1Init(&cpu_val); //初始化启动ADC1的MDA1传输
DMA1_ADC1Init(dma_buf); //初始化启动ADC1的MDA1传输
//TIM4_Init(7200,10000); //定时1S
//LED1_PWM_Init(7200,100); //100的意思是,计数100,就会和PWM的比较寄存器进行比较100次,
RS485_Init(115200);
while (1)
{
if(rs485.ok_flag==1) //串口收到数据
{
rs485.ok_flag=0;
if(rs485.buf[1]==s1.id) //说明主机呼叫对应的从机编号
{
printf("从机%d收到主机的呼叫\r\n",s1.id);
if(rs485.buf[0]==s1.head && rs485.buf[3]==s1.end) //对比帧头帧尾
{
if(rs485.buf[2]==0) //不采集数据
{
}else if(rs485.buf[2]==1) //温湿度
{
}else if(rs485.buf[2]==2) //光照
{
}else if(rs485.buf[2]==3) //有毒气体
{
}else if(rs485.buf[2]==0xff) //温湿度+光照+有毒气体
{
//采集温湿度
delay_ms(1500);
temp=DHT11_GetTempHumidity(data_buf);
if(temp==0)
{
//printf("采集校验ok\r\n");
shidu=data_buf[0]+(data_buf[1]/100.0); //56.000000
wendu=data_buf[2]+(data_buf[3]/100.0); //28.560000
printf("从机%d:温度:%.2f 湿度:%.2f\r\n",s1.id,wendu,shidu);
}
shidu=data_buf[0]+(data_buf[1]/100.0); //
wendu=data_buf[2]+(data_buf[3]/100.0); //31.00-------"31.00"
//printf("温度:%.2f 湿度:%.2f\r\n",wendu,shidu);
sprintf(wsd_buf,"温度:%.2f 湿度:%.2f",wendu,shidu); //转换 wd_buf[]="温度:31.00";
OLED_Show_ChineseChar(0,0,wsd_buf); //OLED显示转换数据
//填充温湿度数据到数据包内
s1.buf[0]=data_buf[0];
s1.buf[1]=data_buf[1];
s1.buf[2]=data_buf[2];
s1.buf[3]=data_buf[3];
//填充DMA搬运的cpu温度、mq135、mq2、光照
s1.buf[4]=dma_buf[0]>>8; //填充高位
s1.buf[5]=dma_buf[0];
s1.buf[6]=dma_buf[1]>>8;
s1.buf[7]=dma_buf[1];
s1.buf[8]=dma_buf[2]>>8;
s1.buf[9]=dma_buf[2];
s1.buf[10]=dma_buf[3]>>8;
s1.buf[11]=dma_buf[3];
s1.buf[12]='\0';
s1.len=12; //数据有效长度
//校验码:数据的累积和,得出校验码
s1.check=0;
for(i=0;i<12;i++)
{
s1.check+=s1.buf[i];
}
//printf("从机校验码:%d\r\n",s1.check);
RS485_SendDatapackage(); //发送填充好的数据包
printf("从机%d返回数据完成\r\n",s1.id);
}
}
}
rs485.ok_flag=0;
rs485.len=0;
memset(rs485.buf,0,256); //清0
}
/*
for(i=0;i<100;i++)
{
delay_ms(10);
TIM_SetCompare4(TIM3,i); //修改占空比,低电平占空比不断增加,越来越亮
}
for(i=100;i>=0;i--)
{
delay_ms(10);
TIM_SetCompare4(TIM3,i); //修改占空比,低电平占空比不断减少,越来越暗
}
*/
//作业1:完成PWM控制LED1
//作业2:完成STM32F4的芯片PAck包安装,否则无法建立工程选择芯片
//作业3: 下载STM32F4的新建工程文件,用于新建工程使用,然后寻找里面的文件建立F4的工程
/*
if(u1.ok_flag==1) //串口收到数据 时:20
{
if(u1.buf[0] == *"时" && u1.buf[1] == *("时"+1)) //说明收到修改小时
{
size_len=u1.len-3; //算出是一位数字还是2位数字
if(size_len==1) //说明为一位数字0-9
{
Hour=u1.buf[u1.len-1]-48; //转换为整数
}else{ //两位数
temp=(u1.buf[u1.len-2]-48)*10+(u1.buf[u1.len-1]-48) ; //转换为整数,再合并
if(temp<24)Hour=temp;
}
}else if(u1.buf[0] == *"分" && u1.buf[1] == *("分"+1)) //说明收到修改分钟
{
size_len=u1.len-3; //算出是一位数字还是2位数字
if(size_len==1) //说明为一位数字0-9
{
Min=u1.buf[u1.len-1]-48; //转换为整数
}else{ //两位数
temp=(u1.buf[u1.len-2]-48)*10+(u1.buf[u1.len-1]-48) ; //转换为整数,再合并
if(temp<60)Min=temp;
}
}else if(u1.buf[0] == *"秒" && u1.buf[1] == *("秒"+1)) //说明收到修改秒
{
size_len=u1.len-3; //算出是一位数字还是2位数字
if(size_len==1) //说明为一位数字0-9
{
Sec=u1.buf[u1.len-1]-48; //转换为整数
}else{ //两位数
temp=(u1.buf[u1.len-2]-48)*10+(u1.buf[u1.len-1]-48) ; //转换为整数,再合并
if(temp<60)Sec=temp;
}
}
u1.ok_flag=0;
u1.len=0;
memset(u1.buf,0,256); //清0
}
*/
// delay_ms(1000);
// cpu_temp= ((1.43 - 3.3/4096*dma_buf[0])/0.0043)+25;
// mq135_val= dma_buf[1]/4096.0*100;
// mq2_val= dma_buf[2]/4096.0*100;
// gz_val= 100-(dma_buf[3]/4096.0*100);
//
// printf("CPU:%.2f ℃\r\n",cpu_temp); //
// printf("mq135:%.2f%%\r\n",mq135_val); //
// printf("mq2:%.2f%%\r\n",mq2_val); //
// printf("gz:%.2f%%\r\n",gz_val); //
//DMA作业:使用DMA完成多通道的数据搬运,DMA搬运CPU+光敏+烟雾+有毒气体数据
/*
for(i=0;i<3;i++) adc1buf[i]=get_val(ADC1, i+1);
adc1buf[0]=adc1buf[0]/4096*100;
adc1buf[1]=adc1buf[1]/4096*100;
adc1buf[2]=100-(adc1buf[2]/4096*100);
printf("CPU:%.2f\r\n",get_cpu_val());
printf("有毒气体:%.2f\r\n",adc1buf[0]); //光照强度
printf("烟雾甲烷:%.2f\r\n",adc1buf[1]); //光照强度
printf("光照:%.2f\r\n",adc1buf[2]); //光照强度
*/
//作业1:完成光照强度的OLED 屏幕显示
//作业2:完成烟雾+有毒气体的数据采集
//要求:采用多通道采集
//CPU温度采集----ADC1_IN16
/*
tem=DHT11_GetTempHumidity(data);
if(tem==0)
{
//printf("采集校验ok\r\n");
shidu=data[0]+(data[1]/100.0); //
wendu=data[2]+(data[3]/100.0); //31.00-------"31.00"
//printf("温度:%.2f 湿度:%.2f\r\n",wendu,shidu);
sprintf(wd_buf,"温度:%.2f",wendu); //转换 wd_buf[]="温度:31.00";
sprintf(sd_buf,"湿度:%.2f",shidu); //转换
OLED_Show_ChineseChar(0,0,wd_buf); //OLED显示转换数据
OLED_Show_ChineseChar(0,2,sd_buf);
}
delay_ms(1200);
*/
/*
for(i=0;i<8;i++) //连续显示图片,构成gif
{
OLED_ShowPicture(30,0,94,8,xj[i]); //显示图片
delay_ms(300);
}
*/
/*
OLED_ShowChinese(0,0,0,16);//中
OLED_ShowChinese(18,0,1,16);//景
OLED_ShowChinese(36,0,2,16);//园
OLED_ShowChinese(54,0,3,16);//电
OLED_ShowChinese(72,0,4,16);//子
OLED_ShowChinese(90,0,5,16);//科
OLED_ShowChinese(108,0,6,16);//技
OLED_ShowString(8,16,"ZHONGJINGYUAN",16);
OLED_ShowString(20,32,"2014/05/01",16);
OLED_ShowString(0,48,"ASCII:",16);
OLED_ShowString(63,48,"CODE:",16);
OLED_ShowChar(48,48,t,16);//显示ASCII字符
t++;
if(t>'~')t=' ';
OLED_ShowNum(103,48,t,3,16);
OLED_Refresh();
delay_ms(500);
OLED_Clear();
OLED_ShowChinese(0,0,0,16); //16*16 中
OLED_ShowChinese(16,0,0,24); //24*24 中
OLED_ShowChinese(24,20,0,32);//32*32 中
OLED_ShowChinese(64,0,0,64); //64*64 中
OLED_Refresh();
delay_ms(500);
OLED_Clear();
OLED_ShowString(0,0,"ABC",12);//6*12 “ABC”
OLED_ShowString(0,12,"ABC",16);//8*16 “ABC”
OLED_ShowString(0,28,"ABC",24);//12*24 “ABC”
OLED_Refresh();
delay_ms(500);
OLED_ScrollDisplay(11,4);
*/
//{
//}
/*
GPIO_SetBits(GPIOC,GPIO_Pin_3); //输出高电平--关LED
delay_ms(500);
GPIO_ResetBits(GPIOC,GPIO_Pin_3);
delay_ms(500);
if(u1.ok_flag==1) //说明串口接收完数据
{
printf("%s",u1.buf); //发送一串数据到电脑回显
//清0
u1.ok_flag=0;
u1.len=0;
memset(u1.buf,0,sizeof(u1.buf));
}
*/
/*
//一直要做的事情
// 呼吸灯
for(i=0;i<1500;i++) //渐亮
{
GPIO_ResetBits(GPIOC,GPIO_Pin_5); //输出低电平
GPIO_ResetBits(GPIOB,GPIO_Pin_1); //输出低电平
delay(i);
GPIO_SetBits(GPIOC,GPIO_Pin_5); //输出高电平--关LED
GPIO_SetBits(GPIOB,GPIO_Pin_1); //输出高电平--关LED
delay(1500-i);
}
for(i=0;i<1500;i++) //渐暗
{
GPIO_ResetBits(GPIOC,GPIO_Pin_5); //输出低电平
GPIO_ResetBits(GPIOB,GPIO_Pin_1); //输出低电平
delay(1500-i);
GPIO_SetBits(GPIOC,GPIO_Pin_5); //输出高电平--关LED
GPIO_SetBits(GPIOB,GPIO_Pin_1); //输出高电平--关LED
delay(i);
}
*/
}
}
10、PWM
10.1 定义
脉冲宽度调制(PWM) 是一种数字信号,最常用于控制电路。该信号在预定义的时间和速度中设置为高(5v或3.3v)和低(0v)。通常,我们将PWM的高电平称为1,低电平为0
10.2 主要参数
10.2.1 PWM占空比
PWM信号保持高电平的时间百分比称为占空比。如果信号始终为高电平,则它处于100%占空比,如果它始终处于低电平,则占空比为0%。T1为占空比,T为一个PWM周期。
10.2.2 PWM的频率
频率决定PWM完成一个周期的速度。STM3编译器上面可以选择5MHZ,10MHZ,20MHZ和50MHZ。
10.3 PWM产生的方式
通过32控制板,有两个方式可以产PWM信号,一个是利用普通端口输出,另一个就是使用定时器的PWM端口或者服用IO口
10.3.1 普通IO口和PWM口方式介绍
- PWM端口
STM32 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4路的 PWM 输出,这样,STM32 最多可以同时产生 30 路 PWM 输出。
- 普通IO口
一般能够输出PWM的端口都会在主要功能那一栏出现CHx的标志,而普通定时器没有出现这种标志。如图所示,上面的红框就是普通的定时器,不是专用的PWM端口。
10.4 PWM程序
#include "pwm.h" // Device header
//led1 PF6--TIM10-CH1
void LED1_PWM_Init(u16 psc,u16 arr)
{
TIM_OCInitTypeDef tim10_pwm; //比较输出
TIM_TimeBaseInitTypeDef tim10; //基本定时
GPIO_InitTypeDef led1;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能GPIOC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM10, ENABLE);
GPIO_PinAFConfig(GPIOF,GPIO_PinSource6,GPIO_AF_TIM10); //映射复用为TIM10_CH1
led1.GPIO_Mode=GPIO_Mode_AF; //输出
led1.GPIO_OType=GPIO_OType_PP; //推挽
led1.GPIO_Pin=GPIO_Pin_6;
led1.GPIO_PuPd=GPIO_PuPd_NOPULL; //上拉
led1.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOF,&led1);
tim10.TIM_ClockDivision=TIM_CKD_DIV1; //72M/1=72主频
tim10.TIM_CounterMode=TIM_CounterMode_Up; //向上计数
tim10.TIM_Period=arr; //重装载值
tim10.TIM_Prescaler=psc-1; //预分频
TIM_TimeBaseInit(TIM10, &tim10); //初始化定时器3的配置
tim10_pwm.TIM_OCMode=TIM_OCMode_PWM1;
tim10_pwm.TIM_Pulse=0; //比较寄存器的值,先写0,后面通过手动调用函数修改比较寄存器的值就是修改占空比
tim10_pwm.TIM_OutputState=TIM_OutputState_Enable; //使能输出控制
tim10_pwm.TIM_OCPolarity=TIM_OCPolarity_Low; //低电平有效
TIM_OC1Init(TIM10, &tim10_pwm); //初始化比较通道1
TIM_OC1PreloadConfig(TIM10, TIM_OCPreload_Enable); //使能重装载值比较寄存器
TIM_Cmd(TIM10, ENABLE); //定时器开始工作
}
//led2 PF9--TIM14-CH1
void LED2_PWM_Init(u16 psc,u16 arr)
{
TIM_OCInitTypeDef tim14_pwm; //比较输出
TIM_TimeBaseInitTypeDef tim10; //基本定时
GPIO_InitTypeDef led1;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能GPIOC
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14, ENABLE);
GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14); //映射复用为TIM10_CH1
led1.GPIO_Mode=GPIO_Mode_AF; //输出
led1.GPIO_OType=GPIO_OType_PP; //推挽
led1.GPIO_Pin=GPIO_Pin_9;
led1.GPIO_PuPd=GPIO_PuPd_NOPULL; //上拉
led1.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOF,&led1);
tim10.TIM_ClockDivision=TIM_CKD_DIV1; //168M/1=168m主频
tim10.TIM_CounterMode=TIM_CounterMode_Up; //向上计数
tim10.TIM_Period=arr; //重装载值
tim10.TIM_Prescaler=psc-1; //预分频
TIM_TimeBaseInit(TIM14, &tim10); //初始化定时器3的配置
tim14_pwm.TIM_OCMode=TIM_OCMode_PWM1;
tim14_pwm.TIM_Pulse=0; //比较寄存器的值,先写0,后面通过手动调用函数修改比较寄存器的值就是修改占空比
tim14_pwm.TIM_OutputState=TIM_OutputState_Enable; //使能输出控制
tim14_pwm.TIM_OCPolarity=TIM_OCPolarity_Low; //低电平有效
TIM_OC1Init(TIM14, &tim14_pwm); //初始化比较通道1
TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable); //使能重装载值比较寄存器
TIM_Cmd(TIM14, ENABLE); //定时器开始工作
}
11、W25Q16/64
11.1 简介
W25Q64是为系统提供一个最小空间、最少引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。
W25Q64支持双倍/四倍的SPI,可以储存包括声音、文本、图片和其他数据;芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于5mA,掉电时低于1uA,所有芯片提供标准的封装。
W25Q64的内存空间结构: 一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。
W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。
擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。
W25Q64支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、I/O1(DO)、I/O2(WP)和 I/O3(HOLD)。
SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上8位和16位的并行Flash存储器。
W25Q64支持 JEDEC 标准,具有唯一的 64 位识别序列号,方便区别芯片型号
11.1.1 w25q64芯片介绍
●SPI串行存储器系列
-W25Q64:64M 位/8M 字节
-W25Q16:16M 位/2M 字节
-W25Q32:32M 位/4M 字节
-每 256 字节可编程页
●灵活的4KB扇区结构
-统一的扇区擦除(4K 字节)
-块擦除(32K 和 64K 字节)
-一次编程 256 字节
-至少 100,000 写/擦除周期
-数据保存 20 年
●标准、双倍和四倍SPI
-标准 SPI:CLK、CS、DI、DO、WP、HOLD
-双倍 SPI:CLK、CS、IO0、IO1、WP、HOLD
-四倍 SPI:CLK、CS、IO0、IO1、IO2、IO3
●高级的安全特点
-软件和硬件写保护
-选择扇区和块保护
-一次性编程保护(1)
-每个设备具有唯一的64位ID(1)
●高性能串行Flash存储器
-比普通串行Flash性能高6倍
-80MHz时钟频率
-双倍SPI相当于160MHz
-四倍SPI相当于320MHz
-40MB/S连续传输数据
-30MB/S随机存取(每32字节)
-比得上16位并行存储器
●低功耗、宽温度范围
-单电源 2.7V-3.6V
-工作电流 4mA,掉电<1μA(典型值)
-40℃~+85℃工作
11.2 引脚
如下介绍的是W25Q64编著的SPI接口
11.2.1 SPI片选(/CS)引脚用于使能和禁止芯片操作
CS引脚是W25Q64的片选引脚,用于选中芯片;当CS为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后, 在接收新的指令前,/CS 必须由高变为低电平。上电后,/CS 必须上升到 VCC,在/CS 接上拉电阻可以完成这个操作
11.2.2 串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)
W25Q64、W25Q16 和 W25Q32 支持标准 SPI、双倍 SPI 和四倍 SPI。
标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。
11.2.3 写保护(/WP)
写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用。
11.2.4 串行时钟(CLK)
串行时钟输入引脚为串行输入和输出操作提供时序
11.3 W25Q64标准的SPI操作
W25Q64标准SPI总线接口包含四个信号: 串行时钟(CLK)、片选端(/CS)、串行数据输入(DI)和串行数据输出(DO)。
DI输入引脚在CLK的上升沿连续写命令、地址或数据到芯片内。
DO输出引脚在CLK的下降沿从芯片内读出数据或状态。
W25Q64分别支持SPI总线工作模式0和工作模式3。模式0和模式3的主要区别在于常态时的CLK信号不同;对于模式0来说,当SPI主机已准备好数据还没传输到串行Flash中时,CLK信号常态为低;
设备数据传输是从高位开始,数据传输的格式为8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线clk为高电平。
11.4 W25Q实例代码
#include "w25q64.h" // Device header
void W25Q64_SPI1_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef W25Q64_SPI1;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); //使能GPIOB
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
//PB3:SCK PB4:MISO PB5:MOSI
GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1);
GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF; //输出
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP; //推挽
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5;
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP; //上拉
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
//PB14:CS
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT; //输出
GPIO_InitStructure.GPIO_OType=GPIO_OType_PP; //推挽
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_14;
GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP; //上拉
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_14); //默认输出高电平
//初始化SPI1
W25Q64_SPI1.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; //8分频,168/8=21Mhz
W25Q64_SPI1.SPI_CPHA=SPI_CPHA_2Edge; //第二个时钟沿采集数据
W25Q64_SPI1.SPI_CPOL=SPI_CPOL_High; //默认为高电平
W25Q64_SPI1.SPI_CRCPolynomial= 7; //CRC固定为7
W25Q64_SPI1.SPI_DataSize=SPI_DataSize_8b; //8位数据
W25Q64_SPI1.SPI_Direction=SPI_Direction_2Lines_FullDuplex; //全双工双向
W25Q64_SPI1.SPI_FirstBit=SPI_FirstBit_MSB; //高位先发
W25Q64_SPI1.SPI_Mode=SPI_Mode_Master; //主机模式
W25Q64_SPI1.SPI_NSS=SPI_NSS_Soft; //SPI_NSS_Soft,软件控制,GPIO控制片选拉高拉低
SPI_Init( SPI1, &W25Q64_SPI1);
SPI_Cmd(SPI1,ENABLE); //使能SPI1开始工作
}
//发送一个字节数据
void SPI1_SendByte(char data)
{
while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE))); //等待为空
SPI_I2S_SendData(SPI1,data); //发送数据
}
//接收一个字节数据
char SPI1_ReadByte(void)
{
while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE))); //判断非空
SPI_I2S_ReceiveData(SPI1);
}
/********************************************************************
函数功能:SPI1发送或接收1个字节的字符数据
参数1:data:你要发送的数据
返回值:接收到的数据
说明:你要发送一个字节数据出去的同时,也会接收得到一个无用的字节数据
你要接收一个字节数据同时,也要发送一个无用的字节数据出去
********************************************************************/
char SPI1_SendReadByte(char data)
{
while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE))); //等待为空
SPI_I2S_SendData(SPI1,data); //发送数据
while(!(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE))); //判断非空
SPI_I2S_ReceiveData(SPI1);
}
/********************************************************************
函数功能:W25Q64写使能
参数1:无
参数2:无
返回值:无
作者:xxx
********************************************************************/
void W25Q64_WriteENABLE(void)
{
CS=0; //片选拉低
SPI1_SendReadByte(0x06);//发送写使能指令
CS=1;//片选拉高
}
/********************************************************************
函数功能:读芯片ID
参数1:ID:存储数据的入口地址,第0字节为生产ID,第1字节为器件ID
参数2:无
返回值:无
W25Q16:0xef14
W25Q32:0xef15
W25Q64:0xef16
W25Q128:0xef17
********************************************************************/
void W25Q64_ReadID(u8 *ID)
{
CS=0; //片选拉低
SPI1_SendReadByte(0x90);//发送读器件ID
SPI1_SendReadByte(0x00);//发送24位地址
SPI1_SendReadByte(0x00);//发送24位地址
SPI1_SendReadByte(0x00);//发送24位地址
ID[0]=SPI1_SendReadByte(0xff);//接收一个字节生产ID,的同时发送一个无用数据
ID[1]=SPI1_SendReadByte(0xff);//接收一个字节器件ID,的同时发送一个无用数据
CS=1; //片选拉低
}
/********************************************************************
函数功能:读状态值
参数1:无
参数2:无
返回值:状态的值
作者:xxx
********************************************************************/
u8 W25Q64_ReadBUSY(void)
{
u8 BUSY=0xff;
CS=0; //片选拉低
SPI1_SendReadByte(0x05);//发送读状态指令0x05
BUSY=SPI1_SendReadByte(0xff);//读取状态值
CS=1; //片选拉高
return BUSY;
}
/********************************************************************
函数功能:擦除芯片
参数1:无
参数2:无
返回值:无
说明:擦除时间大概3s
********************************************************************/
void W25Q64_DeleteChip(void)
{
W25Q64_WriteENABLE(); //写使能
CS=0; //片选拉低
SPI1_SendReadByte(0xc7);//发送擦除芯片指令0xc7
CS=1; //片选拉高,结束
while((W25Q64_ReadBUSY() & 1<<0)); //等待BUSY位为0,说明擦除完毕,并退出
}
/********************************************************************
函数功能:块擦除
参数1:无
参数2:无
返回值:无
说明:擦除时间大概3s
********************************************************************/
void W25Q64_DeleteBlock(u8 Block)
{
u32 addr=Block*65536; //算出擦除块的起始地址
W25Q64_WriteENABLE(); //写使能
CS=0; //片选拉低
SPI1_SendReadByte(0xd8);//发送块擦除指令
SPI1_SendReadByte((u8)(addr>>16)); //发送地址的高8位
SPI1_SendReadByte((u8)(addr>>8)); //发送地址的中8位
SPI1_SendReadByte((u8)addr); //发送地址的低8位
CS=1; //片选拉高,结束
while((W25Q64_ReadBUSY() & 1<<0)); //等待BUSY位为0,说明擦除完毕,并退出
}
/********************************************************************
函数功能:擦除一个扇区,一共有32块*16扇区=512扇区
参数1:无
参数2:无
返回值:无
说明:擦除时间大概3s
********************************************************************/
void W25Q64_DeleteSector(u16 Sector)
{
u32 addr=Sector*4096; //算出擦除扇区起始地址
W25Q64_WriteENABLE(); //写使能
CS=0; //片选拉低
SPI1_SendReadByte(0x20);//发送扇区擦除指令
SPI1_SendReadByte((u8)(addr>>16)); //发送地址的高8位
SPI1_SendReadByte((u8)(addr>>8)); //发送地址的中8位
SPI1_SendReadByte((u8)addr); //发送地址的低8位
CS=1; //片选拉高,结束
while((W25Q64_ReadBUSY() & 1<<0)); //等待BUSY位为0,说明擦除完毕,并退出
}
/********************************************************************
函数功能:页编程写入数据,不能跨页编程,没有带擦除内存功能
参数1:data:写入数据的首地址
参数2:addr:数据写入的地址位置
参数3:len:数据写入的个数
返回值:无
说明:最多可以写入256个字节
********************************************************************/
void W25Q64_PageWriteNoTurn(u8 *data,u32 addr,u16 len)
{
while((W25Q64_ReadBUSY() & 1<<0)); //判断芯片是否为忙
W25Q64_WriteENABLE(); //写使能
CS=0; //片选拉低
SPI1_SendReadByte(0x02);//发送页编程指令0x02
SPI1_SendReadByte((u8)(addr>>16)); //发送地址的高8位
SPI1_SendReadByte((u8)(addr>>8)); //发送地址的中8位
SPI1_SendReadByte((u8)addr); //发送地址的低8位
while(len) //循环写入
{
SPI1_SendReadByte(*data++); //发送地址的高8位
len--; //写入一个字节,数量减1
}
CS=1; //片选拉高,结束
while((W25Q64_ReadBUSY() & 1<<0)); //等待芯片为不忙,说明空闲了,写入数据完成
}
/********************************************************************
函数功能:页编程写入数据,可以跨页编程,没有带擦除内存功能
参数1:data:写入数据的首地址
参数2:addr:数据写入的地址位置
参数3:len:数据写入的个数
返回值:无
说明:最多可以写入256个字节
********************************************************************/
void W25Q64_PageWrite_Next_NoDelete(u8 *data,u32 addr,u32 len)
{
u32 address_sum=addr; //起始地址
int i;
u32 ago_num=256-(addr%256); //算出前一页需要填充完的数量
u32 sum=len-ago_num; //写入总数减去前面一页写的数量,剩下需要继续写的总数
u32 zs_Page=sum/256; //得到剩下总数的整倍页数
u32 ys=sum%256; //写完整页,剩下不满一页的余数
//1、先写满前一页
W25Q64_PageWriteNoTurn(data,addr,ago_num);
address_sum+=ago_num; //地址增加
data+=ago_num; //写入的数据要随着地址一起增加
//2、再写整倍页
for(i=0;i<zs_Page;i++)
{
//指针指向数据也要一起跨页
W25Q64_PageWriteNoTurn(data,address_sum,256); //写满页
data+=256;
address_sum+=256; //计算已写的地址数量
}
//3、最后写完整页,剩下不满一页的余数
W25Q64_PageWriteNoTurn(data,address_sum,ys);
}
///********************************************************************
//函数功能:页编程写入数据,可以跨页编程,没有带擦除内存功能
//参数1:data:写入数据的首地址
//参数2:addr:数据写入的地址位置
//参数3:len:数据写入的个数
//返回值:无
//说明:最多可以写入256个字节
//********************************************************************/
//void W25Q64_PageWrite_Next_NoDelete(u8 *data,u32 addr,u32 len)
//{
// u32 address_sum=addr; //起始地址
// int i;
//
// u32 ago_num=256-(addr%256); //算出前一页需要填充完的数量
// u32 sum=len-ago_num; //写入总数减去前面一页写的数量,剩下需要继续写的总数
// u32 zs_Page=sum/256; //得到剩下总数的整倍页数
// u32 ys=sum%256; //写完整页,剩下不满一页的余数
//
// //1、先写满前一页
// W25Q64_PageWriteNoTurn(data,addr,ago_num);
// address_sum+=ago_num; //地址增加
// data+=ago_num; //写入的数据要随着地址一起增加
//
// //2、再写整倍页
// for(i=0;i<zs_Page;i++)
// {
// //指针指向数据也要一起跨页
// W25Q64_PageWriteNoTurn(data,address_sum,256); //写满页
// data+=256;
// address_sum+=256; //计算已写的地址数量
// }
// //3、最后写完整页,剩下不满一页的余数
// W25Q64_PageWriteNoTurn(data,address_sum,ys);
//
//}
/********************************************************************
函数功能:读数据
参数1:data: 读到数据存放的首地址
参数2:addr:从W25Q64读数据的起始地址
参数3:len:读取数据的个数
返回值:无
作者:xxx
********************************************************************/
void W25Q64_ReadData(u8 *data,u32 addr,u16 len)
{
W25Q64_WriteENABLE(); //写使能
CS=0; //片选拉低
SPI1_SendReadByte(0x03);//发送读数据指令0x03
SPI1_SendReadByte(addr>>16); //发送地址的高8位
SPI1_SendReadByte(addr>>8); //发送地址的中8位
SPI1_SendReadByte(addr); //发送地址的低8位
while(len) //循环写入
{
*data++=SPI1_SendReadByte(0xff); //发送地址的高8位
len--; //读取的字节数量,每读一个,数量减1
}
*data='\0'; //添加数据结束符
CS=1; //片选拉高,结束
}
12.触摸屏
12.1实物图
12.2 触摸屏种类分类
12.2.1 电阻式屏幕
特点:定位准确且可以实现单点触摸;电阻式屏幕是一种传感器,它将矩形区域的触摸点的物理位置转换为代表X/Y轴的电压,现在很多的LCD屏幕都是采用了电阻式的触摸屏,这种屏幕可以用5、6、7、8线来产生偏置电压,同时可以读回触摸点的电压。
电阻式触摸屏是一种传感器,基本上是薄膜加上玻璃的结构,薄膜和玻璃相邻的一面上均涂有ITO(纳米铟锡金属氧化物)涂层,ITO具有很好的导电性和透明性。当触摸操作时,薄膜下层的ITO会接触到玻璃上层的ITO,经由感应器传出相应的电信号,经过转换电路送到处理器,通过运算转化为屏幕上的X、Y值,而完成点选的动作,并呈现在屏幕上。
其优缺点也很明显,缺点是他只能支持单点触摸,不能实现多点触发触摸功能;但是由于这个单点触摸的特点,使得它触摸的精度很高。
12.2.2 电感式触摸屏
支持多点触摸,价格偏贵。工业应用最广泛,我们现在市面上的手机都是感应式的触摸屏,它的优缺点是跟电阻式的触摸屏相反,优点是可以实现多点感应,而缺点就是精度没那么高。
12.2.3 红外线式
特点是:价格十分低廉,但其外框易碎,容易产生光干扰,曲面情况下失真,所以一般情况下我们不会选择这样的触摸屏。这个一般用在特殊的工艺。
12.2.4 表面声波式
特点是:可以解决各种缺点,但是屏幕表面如果有水滴和尘土会使触摸屏变的迟钝。
12.3 XPT2046采集触摸AD芯片
12.3.1 触摸屏接口连接原理
12.3.2 XPT2046特性
工作电压范围为2.2V-5.25V,支持1.5V~5.25V的数字IO口,内建2.5V参考电压源;
电源电压测量(0V-6V)内建结温测量功能触摸压力测量;
采用SPI3线控制通信接口且有自动power-down功能;
封装:QPN-16.TSSOP-16和VFBGA-48与TSC2046、AK4182A完全兼容
12.3.3 XPT2046结构原理
12.3.4 XPT2046引脚
屏幕X/Y坐标的输入+/-端
应用引脚
12.3.5 使用到的引脚
看原理图,我们知道,有红框圈起来的引脚要用我们进行配置:
T_MISO PB2 引脚
XPT2046的串行数据输出端。STM32的数据输入端,数据在DCLK的下降沿 移出,当CS高电平时为高阻状态,配置为输入模式,空闲为高。
T_PEN PB1引脚
笔中断输出,配置为输入模式,空闲模式为高电平;
T_CS PC13引脚
片选信号。控制转换时序和使能串行输入输出寄存器,高电平时ADC掉电;
T_MOSI PF11引脚
XPT2046的串行数据输入端,STM32的输出端,当CS为低电平时,数据在DCLK上升沿锁存进来;
T_SCK PB0引脚
XPT2046外部时钟信号输入,STM32的时钟信号输出;
小结
STM32发数据的时候,片选拉低,DCLK拉高,把数据锁存在T_MOSI引脚传输出去;当STM32读数据的时候,片选拉低,DCLK拉低,读取T_MISO电平状态。
12.4 XPT2046控制说明
12.4.1 单端还是差分模式
选择VBAT、 Temp和AUX时可以配置为单端模式,作为触摸屏应用时,可以配置为差分模式,这可有效消除由于驱动开关的寄生电阻及外部的干扰带来的测量误差,提高转换准确
度。
12.4.2 笔中断输出
有人按下屏幕时,触摸屏下拉到地,也就是按下为低电平
12.4.3 控制字节
用于选择采集的是X还是Y轴的AD值,作用:控制字节由 DIN 输入的控制字如表 3 所示,它用来启动转换,寻址,设置 ADC 分辨率,配置和对 XPT2046 进行掉电控
制。
12.5 代码实例
扫描触摸屏
touch:存放读取到的x/y坐标值,返 回 值 : 0:有按下;1:无按下
********************************************************************************/
unsigned char Xpt2046_ScanTouch(Touch_Typedef *touch)
{
Touch_Typedef ad;
unsigned char ucRetVaule;
if(T_PEN == 0)
{ //判断触摸屏有没有被按下
if(T_PEN == 0)
{
Xpt2046_ReadXYAD(&ad);
touch->xval = ad.xval * Kx + Bx;
touch->yval = ad.yval * Ky + By;
ucRetVaule = 0;
}
}
else
{
touch->xval=0xffff;
touch->yval=0xffff;
ucRetVaule = 1;
}
return ucRetVaule;
}
读取X/Y轴AD值,并滤波处理
void Xpt2046_ReadXYAD(Touch_Typedef *touch)
{
unsigned char i, j;
unsigned short adx[5], ady[5];
unsigned short temp;
for(i=0; i<5; i++){ //先采集多次数据
adx[i] = Xpt2046_ReadAD(0xd0);
ady[i] = Xpt2046_ReadAD(0x90);
}
for(i=0; i<5; i++)
{ //先排序,找出最大和最小的值
for(j=0; j<5-i-1; j++)
{
if(adx[j] > adx[j+1])
{
temp = adx[j];
adx[j] = adx[j+1];
adx[j+1] = temp;
}
if(ady[j] > ady[j+1])
{
temp = ady[j];
ady[j] = ady[j+1];
ady[j+1] = temp;
}
}
}
touch->xval = adx[2];
touch->yval = ady[2];
}
读取轴AD值
unsigned short Xpt2046_ReadAD(unsigned char cmd)
{
unsigned char vh, vl;
T_CS = 0; //拉低片选,选中器件,开始通信
Xpt2046_WriteByte(cmd); //发送测量命令
Delay_Us(1); //等到芯片忙结束
vh = Xpt2046_ReadByte();//读取数据的高位
vl = Xpt2046_ReadByte();//读取数据的低位
T_CS = 1; //拉低片选,取消选中,结束通信
return ((vh<<8|vl)>>4); //返回结果,其中只有12位有效
}
触摸屏校准函数
这个可以通用,需要的直接复制就好了
void Xpt2046_TouchAdjust(void)
{
unsigned short lcd_pos[4][2] = {20, 20, 300, 20, 20, 460, 300, 460, };
Touch_Typedef touch_pos[4]; //用来存放x,y的4个AD值
unsigned char i, j;
double len1 = 0.00f, len2 = 0.00f;
Lcd_ShowString(30, 130, "touch adjust start", RED, WHITE, 16);
while(1){
for(i=0; i<4; i++){ //读取4个对应的触摸屏坐标
for(j=0; j<30; j++){ //画一个十字架
Lcd_DrawPoint(lcd_pos[i][0]-15+j, lcd_pos[i][1], RED);
Lcd_DrawPoint(lcd_pos[i][0], lcd_pos[i][1]-15+j, RED);
}
printf("等待校验\r\n");
while(T_PEN == 1); //等待按下触摸屏
Delay_Ms(50); //延时50ms待数据稳定
printf("按下触摸屏\r\n");
Xpt2046_ReadXYAD(&touch_pos[i]);//获得触摸屏测量的x,y轴数值
while(T_PEN == 0); //等待松开手
Delay_Ms(200);
for(j=0; j<30; j++){ //清掉十字架图标
Lcd_DrawPoint(lcd_pos[i][0]-15+j, lcd_pos[i][1], WHITE);
Lcd_DrawPoint(lcd_pos[i][0], lcd_pos[i][1]-15+j, WHITE);
}
}
//校验坐标-计算点击的触摸点是否正确 如果不正确重新校准
//水平两个点之间的距离比较
len1 = (float)sqrt((touch_pos[1].xval-touch_pos[0].xval)*(touch_pos[1].xval-touch_pos[0].xval) \
+ (touch_pos[1].yval-touch_pos[0].yval)*(touch_pos[1].yval-touch_pos[0].yval));
len2 = (float) sqrt((touch_pos[3].xval-touch_pos[2].xval)*(touch_pos[3].xval-touch_pos[2].xval) \
+ (touch_pos[3].yval-touch_pos[2].yval)*(touch_pos[3].yval-touch_pos[2].yval));
if(((len1/len2)<0.95) || ((len1/len2)>1.05)){
continue;
}
//垂直两个点之间的距离比较
len1 = (float)sqrt((touch_pos[2].xval-touch_pos[0].xval)*(touch_pos[2].xval-touch_pos[0].xval) \
+ (touch_pos[2].yval-touch_pos[0].yval)*(touch_pos[2].yval-touch_pos[0].yval));
len2 = (float)sqrt((touch_pos[3].xval-touch_pos[1].xval)*(touch_pos[3].xval-touch_pos[1].xval) \
+ (touch_pos[3].yval-touch_pos[1].yval)*(touch_pos[3].yval-touch_pos[1].yval));
if(((len1/len2)<0.95) || ((len1/len2)>1.05)){
continue; //点击的点不符合要求
}
//对角线两个点之间的距离比较
len1 = (float)sqrt((touch_pos[3].xval-touch_pos[0].xval)*(touch_pos[3].xval-touch_pos[0].xval) \
+ (touch_pos[3].yval-touch_pos[0].yval)*(touch_pos[3].yval-touch_pos[0].yval));
len2 = (float)sqrt((touch_pos[2].xval-touch_pos[1].xval)*(touch_pos[2].xval-touch_pos[1].xval) \
+ (touch_pos[2].yval-touch_pos[1].yval)*(touch_pos[2].yval-touch_pos[1].yval));
if(((len1/len2)<0.95) || ((len1/len2)>1.05)){
continue;
}
//计算校准参数 Kx (Ky)--斜率;Bx(By) --偏移量
//计算x映射 Xlcd = Kx * touch_x + Bx
Kx = (float)(lcd_pos[1][0]-lcd_pos[0][0]) / (touch_pos[1].xval-touch_pos[0].xval);
Bx = lcd_pos[0][0] - Kx*touch_pos[0].xval;
//计算y映射 Ylcd = Ky*touch_y + By
Ky = (float)(lcd_pos[2][1]-lcd_pos[0][1]) / (touch_pos[2].yval-touch_pos[0].yval);
By = lcd_pos[0][1] - Ky*touch_pos[0].yval;
Lcd_Fill(0, 0, 320, 480, WHITE);
Lcd_ShowString(30, 130, "touch adjust OK", RED, WHITE, 16);
printf("校准参数 Ky=%f;Kx=%f;By=%d;Bx=%d;\r\n",Ky, Kx, By, Bx);
Delay_Ms(1000);
Delay_Ms(1000);
Lcd_Fill(0, 0, 320, 480, WHITE);
break;
}
}