STM32-学习笔记

介绍

这篇文章是我入门嵌入式的文章,有些理解可能不足。

是看江协和正点原子的视频,板子是STM32F103C8T6最小系统板。

正点为主:第3讲 STM32学习方法_视频说明_哔哩哔哩_bilibili

江协为辅:STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili

还看了《STM32单片机应用与全案例实践》这本书

这些是我在学习STM32F103C8T6的过程中的练习和总结的个人笔记。

读书笔记在这里:《STM32单片机应用与全案例实践》沈红卫_2017版读书笔记-CSDN博客

正在更新,

大家可以在我的gitee仓库 中下载笔记源文件、梁山派资料等

笔记源文件可以在Notion中导入

目录

0.资料准备

1.[江协] 创建标准库的工程模板

1.解压库函数的压缩包并打开

2.建立工程模板,尝试使用寄存器/标准库来点灯

一、添加Start启动文件夹

二、添加User用户文件夹

三、尝试用寄存器点亮LED灯

***、尝试用库函数来点亮LED灯

四、添加Library文件

五、使用库函数进行点灯操作

2.GPIO的工作原理

3.[正点] 标准库的LED跑马灯

4.[正点]标准库的按键点灯

按键支持连续按的一般思路

按键不支持连续按的一般思路

两种模式合二为一的思路

实现按键点亮对应LED灯

5.[正点]MDK中寄存器地址名称映射的再理解

6.[正点]时钟树系统的了解

STM32(ARM)的时钟为什么要设置的那么复杂

分析时钟树的方法(这里是对F4系列芯片)

时钟分析方法

总结:

常用时钟配置寄存器

7.[正点]SystemInit.c时钟系统初始化文件了解

8.[正点]Systick定时器

Systick定时器基础知识了解

Systick定时器的四个寄存器

Systick相关函数

尝试使用Systick写一个延时函数

9.[正点&江协]IO引脚的复用和映射

什么是端口复用?

芯片的复用功能映射配置

[江协]什么是通信

[江协]常用通信方式以及不同

[江协]什么是串口通信USART

串口是什么

串口的连接注意事项及分类

串口的参数

USART简介及框图介绍

STM32在接受端的天才设计

[正点(但是F103板)]端口的复用功能配置过程(串口举例)

10.[正点]NVIC中断优先级管理

NVIC中断优先级分组

抢占优先级和响应优先级 的区别

NVIC中断优先级设置

11.[江协&正点]串口的学习

[江协]F1芯片编写串口收发数据

准备工作

编写程序

[正点]F4芯片串口收发步骤

[江协]F1芯片串口收发数据包理解与思路

数据包的使用理解

数据包发送思路

数据包接受思路

[江协]F1芯片编写串口收发数据包

HEX数据包的发送与接收

文本数据包的接收



0.资料准备

  1. 芯片包的下载
  2. 数据手册和参考手册
  3. STM32标准外设库下载

1.[江协] 创建标准库的工程模板

这一部分只是创建了一个基础的标准库模板。并没有Hardware等文件

1.解压库函数的压缩包并打开

库函数为:STM32F10x_StdPeriph_Lib_V3.5.0,这个可以去官网下载。

解压后文件夹内容如下

  • Libraries是库函数的文件
  • Project是官方提供的工程示例和模板
  • Utilities是官方做的一个小电路板,存放的用来测评stm32的程序
  • Release_Notes.html是发布文档
  • stm32f10x_stdperiph_lib_um.chm为库函数使用手册

2.建立工程模板,尝试使用寄存器/标准库来点灯

本章建立了工程模板并尝试用寄存器和库函数来进行点灯操作

建立工程模板是为了配置好STM32的环境,并且方便下次去建立工程。不用每次都新建一次。

  1. 建立工程文件是为了方便管理。可以起名为STM32Project
  2. 在Keil上新建工程,起名后 选择芯片型号(这里为STM32F103C8)

一、添加Start启动文件夹

  1. 新建Start文件,用来存放STM32的启动文件,STM32运行时会先运行它们

    启动文件的存放位置位于库函数的压缩包中的Libraries文件中。下面的位置可以参考用

    \\STM32F10x_StdPeriph_Lib_V3.5.0\\Libraries\\CMSIS\\CM3\\DeviceSupport\\ST\\STM32F10x\\startup\\arm

    我们把这里面的所有文件都粘贴到Start文件中。

    往回走,可以在这个目录下找到一些头文件

    \\STM32F10x_StdPeriph_Lib_V3.5.0\\Libraries\\CMSIS\\CM3\\**DeviceSupport**\\ST\\STM32F10x

    • stm32f10x.h - STM32寄存器外设描述文件,是用来描述stm32有哪些寄存器,和他对应的地址
    • system_stm32f10x.c - 这两个system_stm32f10x文件主要是用来配置时钟的(STM32的72MHZ主频就是在这里配置的)
    • system_stm32f10x.h 。

    上面三个同上,也是全部放到Start文件中

    因为STM32是内核和内核外围的设备组成的,并且内核寄存器描述和外围寄存器描述是不在一起的,所以要添加内核寄存器的描述文件

    这次就不是打开D**eviceSupport** 文件了,而是打开内核的**CoreSupport**文件

    • core_cm3.c - 这两个就是内核的寄存器描述文件.h以及配置文件.c
    • core_cm3.h
  2. Keil添加启动文件Start。.s的启动文件只需要添加一个。但他有分类

    所以我们选择_md.s后缀的文件

    添加后再把其他的.c .h后缀的文件都添加进来(要选择筛选为ALL,否则看不全)

  3. 在工程选项中添加文件的路径,否则找不到。 并且这样也有利于移动文件,因为添加后工程是以目录所在的文件位置去找文件,而不是使用绝对路径去找。可以很方便的打包工程发给别人

    1. 打开魔术棒,在C/C++选项中找到Include Paths的框框,点三个点找到Start的文件夹。然后确定。

二、添加User用户文件夹

  1. 在工程模板的文件夹下添加User文件夹,用来存放用户写的代码

  2. 在Keil软件的Start的上一个目录Target 1上右键,点击添加组。然后重命名为Start。 并在魔术棒→ C/C++中,添加好头文件路径 Include Path

  3. Keil软件上右键Start添加新文件,选择.c输入ain.c文件然后选择存放路径到User文件中,否则会默认放到文件夹外的工程目录中。

    (这里我已经在魔术棒上设置好Tab=4个空格和显示空格等配置了。) (并且魔术棒按钮的Target块中选择的是ARMCode编译器为v5.06) (在扳手工具处已经设置字号为14,编码格式为:GB2312)

  4. 在里边输入

    #include "stm32f10x.h"  //头文件
    
    int main()
    {
        while(1)
        {
        
        }
    }
    //注意最后一行得多一行空格,不然编译会报错。
    

    此时编译(编译分为全局编译和单文件编译,全局编译是对所有文件编译,单文件则是对当下的文件进行编译。这里先选择单文件就够了)就可以看到0错误0警告。

  5. 把STINK和系统板插好,然后连接电脑。

  • 3.3V对应3.3V
  • GND对应GND
  • SWDIO对应SWDIO
  • SWCLK对应SWCLK
  1. 点击魔术棒选择Debug,设置Use为STink调试器(ST-Link Debugger),然后点击右边的设置按钮。在flash下载的选项中,把Reset and Run 勾选上,这样每次下载程序后会自动复位并执行,不需要按复位按键了

    (这里我已经下载好虚拟串口的驱动CH340了,不下是发现不了设备的)

  2. 编译一下,可以看到0警告0错误

三、尝试用寄存器点亮LED灯

当弄到现在,创建好User文件夹对于寄存器开发者来说,已经建立好工程模板了。

可以先尝试一下寄存器点灯

这里我点的灯位于GPIOC,

寄存器点灯只需要配置三个寄存器。

  1. 首先是配置RCC寄存器,使能GPIOC的时钟, 要使能GPIOC的时钟。而GPIO是挂载到APB2的总线上的。

    打开STM32F10xxx参考手册(中文)手册后可以看到IOPC是在RCC_APB2ENR总线上(偏移地址:0x18)的第4位控制。并且下面有说明这一项为1就开启。

    所以把这一项写1就可以开启他的时钟了。

    整个寄存器的2进制数据换成16进制就是0 0 0 0 0 0 1 0

    所以写上代码RCC->APB2ENR = 0x00000010;就可以打开GPIOC的时钟了。

  2. 然后第二个寄存器,要配置PC13的端口的模式

    在手册中的通用和复用I/O中可以找到GOIO寄存器的一小节。在里面找到**端口配置高寄存器(GPIOx_CRH)**的一小节。 (0-7的前八位是在低寄存器里,8-15是在高寄存器里配置) 其中的CNF13 和MODE13就是用来配置端口PC13的

    这里我们需要将端口配置为 通用推挽输出模式也就是CNF13的两位要为00, MODE则是设置为输出模式,最大速度为50MHZ,也就是MODE两位要为11

    最后按照16进制,把32位二进制位转换为16进制的数字 也就是把0 0 3 0 0 0 0 0

    所以写上代码GPIOC->CRH = 0x00300000;就配置PC13口为推挽输出模式,速度为50MHz了

  3. 下一步就是配置端口输出数据寄存器

    在手册中的通用和复用I/O中可以找到GOIO寄存器的一小节。 在里面找到**端口输出数据寄存器(GPIOx_ODR)**的一小节。

    这一位写一就是把PC13的输出设置为高电平了。

    把这32位2进制换算为16进制,就是0 0 0 0 2 0 0 0

    所以写上代码GPIOC->ODR = 0x00002000;就配置PC13口为高电平

    因为这个板子的灯是低电平点亮,所以配置为全0就能点亮灯了**GPIOC->ODR = 0x00000000;** 下边那个灯就是我点亮的。

    可以看到寄存器点灯十分的麻烦,得去手册查寄存器。而寄存器那么多,根本不可能记得完。并且我刚才点灯是把除了PC13之外的位都设置成了0。,这样会影响其他端口的正常配置,如果要不影响的话还得用&=或者|=。就会更加麻烦。所以寄存器的操作方式,虽然代码简洁,但是操作起来很不方便

    下面我就要去学习库函数的使用,来用库函数点灯!!

***、尝试用库函数来点亮LED灯

四、添加Library文件

本章添加了库函数到Library文件,并在Keil软件中定义了库函数头文件。

Library是用来存放库函数的,让我们可以使用。

  1. 在工程目录新建Library的文件夹。

  2. 添加库函数到新建的Library文件

    库函数位于库函数压缩包的目录为:\\STM32F10x_StdPeriph_Lib_V3.5.0\\Libraries\\STM32F10x_StdPeriph_Driver\\src 中了。其中STM32F10x_StdPeriph_Lib_V3.5.0 为STM公司推出的标准外设库。

    其中的文件就是库函数的源文件了。

    misc.c 为内核的库函数。其余的都是内核外的外设库函数。 这里就Ctrl+A全选,全部复制到Library文件夹中

    然后再打开固件库的inc文件夹,目录就在上一级。 \\STM32F10x_StdPeriph_Lib_V3.5.0\\Libraries\\STM32F10x_StdPeriph_Driver\\inc

    这个文件夹中放的是库函数的头文件,也把他复制到Library文件夹中。

  3. 接着回到keil软件,在target 1 处右键,添加组,重命名,然后添加文件到组

    但是到这里,对于库函数来说还是不能直接使用的,需要再添加三个文件 它位于固件库目录的:\\STM32F10x_StdPeriph_Lib_V3.5.0\\Project\\STM32F10x_StdPeriph_Template

    可以看到几个文件

    • stm32f10x_conf - 用来配置库函数头文件的包含关系,还有用来参数检查的函数定义,这是所有库函数都需要的
    • 剩下的两个it后缀的是用来存放中断函数的

    把这三个文件放到User的目录中。接着在keil软件的User组中添加这三个文件。并在魔术棒→ C/C++中,添加好头文件路径 Include Path

  4. 完成后,就需要添加库函数的头文件了。

    先对#include "stm32f10x.h"stm32f10x.h 右键,选择打开stm32f10x.h 滑到最下边可以看到这样一段代码:

    #ifdef USE_STDPERIPH_DRIVER
      #include "stm32f10x_conf.h"
    #endif
    

    这是C语言中的条件编译语句,意思是如果我定义了USE_STDPERIPH_DRIVER。下面的这个#include "stm32f10x_conf.h"才有效

    这需要我们点击魔术棒按钮,在C/C++的选项中,在Define框框中输入USE_STDPERIPH_DRIVER 这样才能包含标准库函数。

    (记得把每个组(文件)的路径添加好头文件路径 Include Path,在魔术棒的C/C++ 中)

    可以在KEil软件看到。 User中的文件,我们是可以修改的,而Start和Library文件中的东西都带有小钥匙 是改不了的。我们可以点击魔术棒旁边的小箱子来拖动挑中左边的组的排序位置。

    比如把User放到最下边,其他放上边,这样的话平时不用就可以收起来。

    下面再编译一下就可以看到成功了。 (单文件编译,如果新添加了文件会默认使用全局编译一次。所以比较慢)

五、使用库函数进行点灯操作

本小节使用库函数进行点灯。

库函数本质还是配置寄存器,不过是间接的了。

  1. 第一步还是使能GOIOC的时钟 它位于APB2的外设上。

    库函数中用来开启时钟的函数是RCC_APB2PeriphClockCmd 大概翻译一下就是RCC_APB2 外设 时钟 命令(控制)

    输入完后会提示出来这个函数要输入两个参数。

    可以在编译后按F12或者右键跳到函数定义来看他的参数需要填什么,都会有介绍。

    简介中会说,这个函数是让APB2的时钟使能或者失能的。他的参数可以是下面这些。

    我们这里用的是RCC_APB2Periph_GPIOC 这一项,然后填写到函数的第一个参数就OK

    第二个参数是ENABLE 使能。

    这样就可以使能GPIOC的时钟了。

    通过F12下这个函数,来查看这个函数的内部代码。其实他只是包装了一下。实际上还是配置寄存器的。

  2. 第二步还是配置端口的模式。

    这里用的库函数是GPIO_Init,F12查看定义,可以知道他的两个参数

    第一个参数是选择GPIO。x可以是A-G中的数字 所以第一个参数就是GPIOC。

    第二个参数是一个GPIO_InitTypeDef 的结构体的指针(是指针!)。我们需要先定义一个结构体。结构体的名字根据官方的推荐,最好叫GPIO_InitStructure。

    代码:GPIO_InitTypeDef GPIO_InitStructure;

    然后复制结构体的名字,用 . 操作符来配置结构体内部的变量。

        GPIO_InitTypeDef GPIO_InitStructure;
        GPIO_InitStructure.GPIO_Mode = 
        GPIO_InitStructure.GPIO_Pin = 
        GPIO_InitStructure.GPIO_Speed = 
    

    这里可以现在Mode的一行按F12 ,跳转到Mode参数的定义。

    他说:Mode的值在GPIOMode_TypeDef(这是一个枚举变量)里可以找到。那么就可以在当前文件Ctrl+F搜一下。找到GPIOMode_TypeDef这个枚举变量的位置。 这里我们用到的是GPIO_Mode_Out_PP :通用推挽输出。 然后把这个参数放到Mode = 后就可以了。

    下面就是配置GPIO_Pin的参数了,

    在使用F12跳转的时候,会显示一个框,这是因为他的定义后很多个。 这里需要的是member这一项,双击就可跳转过去了。

    (其实还是刚才GPIO_Mode跳转过去的那个位置)

    他说Pin的值在GPIO_pins_define(这是一个枚举变量)里可以找到

    仍然是Ctrl+F搜一下。

    可以看到这里是#define的宏定义列表

    这里我们需要用到的是GPIO_Pin_13

    仍然是复制粘贴过去

    Speed的参数也是同理

    这里选择GPIO_Speed_50MHz

    致此,我们定义的结构体变量GPIO_InitTypeDef GPIO_InitStructure;的参数就配置完全了。 下面就可以把结构体变量的**地址(是传地址!!)**放到配置GPIO模式的函数GPIO_Init 的第二个参数中了。

        //配置PC13的端口为推挽输出模式,速度为50MHz
        GPIO_InitTypeDef GPIO_InitStructure;                //定义GPIO_Init的第二个参数:结构体变量
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        
        GPIO_Init(GPIOC,&GPIO_InitStructure);//GPIO_Init,配置GPIO模式
    
  3. 第三步:配置GPIO口为高/低电平

    这里用到的函数是 GPIO_SetBits - 设置GPIO为高电平 GPIO_ResetBits - 设置GPIO为低电平

    参数则都是GPIOx,GPIO_Pin_x 要看定义也是同上F12

  4. 编译!!

    代码如下,如果编译的时候报错:

    User\\main.c(15): error:  #268: declaration may not appear after executable statement in block
    GPIO_InitTypeDef GPIO_InitStructure;                //定义GPIO_Init的第二个参数:结构体变量
    

    他的意思这一行的声明位置。C 语言要求变量声明通常应在可执行语句之前。这违反了 C 语言的语法规则,想让我把GPIO_InitTypeDef GPIO_InitStructure; 放到第一行声明。我能听他的?

    解决办法就是:在魔术棒→ C/C++处勾选C99Mode(C语言的C99标准),这样就不会报错了。

        //库函数点灯
        
        //打开GPIOC的时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
        
        //配置PC13的端口为推挽输出模式,速度为50MHz
        GPIO_InitTypeDef GPIO_InitStructure;                //定义GPIO_Init的第二个参数:结构体变量
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        
        GPIO_Init(GPIOC,&GPIO_InitStructure);//GPIO_Init,配置GPIO模式
        //设置PC 13为低电平。点灯。
        GPIO_ResetBits(GPIOC,GPIO_Pin_13);
    

    点灯如下:

2.GPIO的工作原理

GPIO如果只是单独的进行输入输出就太浪费了。所以芯片的GPIO口还可以复用为外设功能引脚(比如串口)

GPIO的4种输入模式

  • 输入浮空 GPIO_Mode_IN_FLOATING
  • 输入上拉 GPIO_Mode_IPU
  • 输入下拉 GPIO_Mode_IPD
  • 模拟输入 GPIO_Mode_AIN

GPIO的4种输出模式

  • 开漏输出(带上拉或者下拉) GPIO_Mode_Out_OD
  • 开漏复用功能(带上拉或者下拉)GPIO_Mode_AF_OD
  • 推挽式输出(带上拉或者下拉) GPIO_Mode_Out_PP
  • 推挽式复用功能(带上拉或者下拉)GPIO_Mode_AF_PP

4种最大输出速度(这里与F1不同)

  • 2M
  • 25M
  • 50M
  • 100M

F4的芯片手册中,GPIo表格只要有FT标识,就代表他可以容忍5V的输入

F4与F1的不同就是这里的上下拉电阻被移动到了保护二极管那边,而不是在里边。

  1. 浮空输入模式:

    既不上拉又不下拉,输入信号直接经过施密特触发器,然后存入寄存器,让芯片读取

  2. 输入上/下拉模式

    区别就是输入电平会经过上拉或者下拉电阻拉高或拉低

  3. 模拟输入

    此时不经过施密特触发器来转换为高低电平。是跳过触发器直接到AD(A是模拟,D是数字)这里

  4. 开漏输出

    输出强低电平。

    输出时,首先是操作位设置寄存器(间接去操作输出数据寄存器)或者输出数据寄存器。然后通过输出控制电路来控制Nmos管是否接地,当输出控制电路输出1时,Nmos为断开,此时的GPIO为高阻态,如果想输出高电平。,就需要配置电阻为上拉。简单来说,开漏输出只可以输出强低电平,高电平得靠外部电阻拉高。显然,这种输出方式就有一个优点,由于高电平完全由外部电阻控制,那此模式下的输出电平是可以通过改变电阻而改变的

  5. 开漏复用功能

    通过复用功能外设来控制输入输出,不是那俩寄存器。其他的都是一样的

  6. 推挽输出

    输出强高低电平。

    输出时,首先是操作位设置寄存器(间接去操作输出数据寄存器)或者输出数据寄存器。然后通过输出控制电路来控制两个NMOS管的通断,来控制输出的高低电平。他也可以去配置上下拉电阻,

  7. 推挽式复用功能

    通过复用功能外设来控制输入输出,不是那俩寄存器。其他的都是一样的

    每组(16个IO口)GPIO端口的寄存器包括:

    • 4个32位配置寄存器
      • 1个端口模式寄存器(GPIOx MODER)
      • 1个端口输出类型寄存器(GPIOx OTYPER)
      • 1个端口输出速度寄存器(GPIOx OSPEEDR)
      • 1个端口上拉下拉寄存器(GPIOx PUPDR)
    • 2个32位数据寄存器
      • 1个端口输入数据寄存器(GPIOx IDR)
      • 1个端口输出数据寄存器(GPIOx ODR)
    • 1个端口置位/复位寄存器(GPIOx BSRR)
    • 1个端口配置锁存寄存器(GPIOx LCKR)
    • 两个复位功能寄存器(低位GPIOx AFRL & GPIOx AFRH) (这个比较重要)

    所有的IO口都可以用作中断的输入

3.[正点] 标准库的LED跑马灯

这里的工程文件,我是用的是江协的工程文件,在其基础上添加了Hardware文件夹和Delay文件夹

并在Keil中添加和保存文件相对路径。

代码如下:

main.c:

#include "stm32f10x.h"                  // Device header
#include "led.h"
#include "delay.h"//直接拿的延时函数
int main()
{

    
    LED_Init();//初始化LED

    while(1)
    {
        GPIO_SetBits(GPIOB,GPIO_Pin_15);//PB15为高电平,熄灭
        GPIO_ResetBits(GPIOB,GPIO_Pin_12);//PB12为低电平,点亮
        Delay_ms(500);//延时500ms
        GPIO_SetBits(GPIOB,GPIO_Pin_12);//PB12为高电平,熄灭
        GPIO_ResetBits(GPIOB,GPIO_Pin_13);//PB13为低电平,点亮
        Delay_ms(500);//延时500ms
        GPIO_SetBits(GPIOB,GPIO_Pin_13);//PB13为高电平,熄灭
        GPIO_ResetBits(GPIOB,GPIO_Pin_14);//PB14为低电平,点亮
        Delay_ms(500);//延时500ms
        GPIO_SetBits(GPIOB,GPIO_Pin_14);//PB14为高电平,熄灭
        GPIO_ResetBits(GPIOB,GPIO_Pin_15);//PB15为低电平,点亮
        Delay_ms(500);//延时500ms
    }
}

LED.c

#include "led.h"
#include "stm32f10x.h"

void LED_Init(void)
{
    //跑马灯引脚为PB 12 13 14 15,LED为低电平有效
    
        
    //F4的芯片在这里还需要配置上下拉电阻PuPd 以及 推挽或者开漏Otype
    
    //打开GPIOB的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    //定义GPIO_Init的第二个参数:结构体变量
    GPIO_InitTypeDef GPIO_InitStructure;                
    //↑在C99下不需要放到第第一行
    
    
    //下面的GPIO是分开配置的,其实可以写成A||B.这样就不用写那么多次了。这里我就不改了
    
    
    //PB12
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    //配置PC13的端口为推挽输出模式,速度为50MHz
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);//GPIO_Init,初始化GPIO B,PB12的GPIO
    GPIO_SetBits(GPIOB,GPIO_Pin_12);//设置GPIO为高电平。LED为熄灭
    //
    
   //PB13
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_13);
    
   //PB14
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_14);
    
    
   //PB15
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_15);

}

LED.h

#ifndef __LED_H //条件编译

#define __LED_H

void LED_Init(void);//初始化LED

#endif

Delay.c就不写上去了。

结果如图

4.[正点]标准库的按键点灯

按键检测使用的是gpio的输入功能。函数为

KEY1 GPIO_ReadInputDataBit(GPIO_B,GPIO_PIN_11)//读取 GPIOB 11引脚的电平

按键支持连续按的一般思路

支持连续按,每次检测只要是按下的状态都会返回有效值 思路如下:

uint8_t KEY_Scan(void)
{
    if(KEY按下)
    {
        delay(10)//延迟10ms,消抖
        if(KEY按下)//确定按下
        {
            return 有效值
        }
    }
    return 0;//否则返回无效值
}

按键不支持连续按的一般思路

不支持连续按,按下按键后只要不松开就不会返回有效值 思路如下: 使用了static静态修饰变量

  1. 存储持续性:static 修饰的局部变量具有静态存储持续性,它在程序的整个运行期间都存在,而不是在函数调用结束时被销毁。
    • 例如,每次函数调用结束后,static 局部变量的值会被保留,下次函数调用时会继续使用上次修改后的值。
  2. 初始化:只在第一次函数调用时进行初始化。
  • 假设一个函数被多次调用,但static 局部变量只会在第一次调用时被初始化为指定的值,后续调用不会再次初始化。
  1. 作用域:作用域仍然局限在声明它的函数内部,在函数外部无法直接访问。
uint8_t KEY_Scan(void)
{
    static key_up = 1//=1表示上一个状态为未被按下
    if(key_up && KEY按下)//如果上个状态未被按下,并且现在KEY按下,那么就进入。
    {
        delay(10)//延迟10ms,消抖
       
        if(KEY按下)//确定按下
        {
            key_up = 0;//标记KEY按下
            return 有效值
        }
    }
    else if (KEY没有按下)
    {
        key_up = 1;//记录上个状态没有按下
    }
    return 0;//否则返回无效值
  1. 存储持续性:static 修饰的局部变量具有静态存储持续性,它在程序的整个运行期间都存在,而不是在函数调用结束时被销毁。
    • 例如,每次函数调用结束后,static 局部变量的值会被保留,下次函数调用时会继续使用上次修改后的值。
  2. 初始化:只在第一次函数调用时进行初始化。
    • 假设一个函数被多次调用,但static 局部变量只会在第一次调用时被初始化为指定的值,后续调用不会再次初始化。
  3. 作用域:作用域仍然局限在声明它的函数内部,在函数外部无法直接访问。

两种模式合二为一的思路

根据传入的mode值来强制使Key up为1

uint8_t KEY_Scan(uint8_t mode)
{
    static key_up = 1//=1表示上一个状态为未被按下
    if(mode == 1)//如果mode为1
    {
        key_up = 1;//那么key强制为1.支持连续按。
    }
    if(key_up && KEY按下)//如果上个状态未被按下,并且现在KEY按下,那么就进入。
    {
        delay(10)//延迟10ms,消抖
       
        if(KEY按下)//确定按下
        {
            key_up = 0;//标记KEY按下
            return 有效值
        }
    }
    else if (KEY没有按下)
    {
        key_up = 1;//记录上个状态没有按下
    }
    return 0;//否则返回无效值
}

实现按键点亮对应LED灯

这里已经在头文件内定义了宏,

#define KEY1 GPIO_ReadInputDataBit(GPIO_B,GPIO_PIN_11)//读取 GPIOB 11引脚的宏

点灯图如下:

key.文件如下

#include "key.h"
#include "stm32f10x.h"

//初始化GPIO
//按键1 2是低电平检测。所以模式配置为上拉输入
void KEY_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能APB2总线的GPIOC 时钟
    
    GPIO_InitTypeDef GPIO_InitStructure;
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_0;//KEY 1  2 对应引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);//初始化GPIOB 11  0引脚
    
}

//按键检测
uint8_t KEY_Scan(uint8_t mode)
{
    static uint8_t Key_up = 1;//初始化keyup = 1,未被按下
    if(mode == 1)
    {
        Key_up = 1;//如果启动了连续按下模式,那么就强制定义上一次状态为未被按下
    }
    if(Key_up && (KEY1||KEY2))//上一次未被按下,现在KEY1或2按下。那么就有效
    {
        Delay_ms(50);//去抖动
        Key_up = 0;//赋值为0,标记按下
        if     (KEY1)
        {
            return 1;
        }
        else if(KEY2)
        {
            return 2;
        }    
        }
    else if(KEY1 == 0 && KEY2 == 0)
    {
        Key_up = 1;//未被按下,那么赋值为1,标记未被按下。
    }
    return 0;
}

Key.h文件如下

#ifndef __KEY_H
#define  __KEY_H

#include "stm32f10x.h"
#include "Delay.h"

//低电平有效,读取到低电平返回1
#define KEY1 (!(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) ))//读取 GPIOB 11引脚的宏
#define KEY2 (!(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)  ))//读取 GPIOB 11引脚的宏

//初始化KEY引脚
void KEY_Init(void);

//按键扫描  1为支持连续按,0为不支持
uint8_t KEY_Scan(uint8_t mode);

#endif

LED.c文件如下

#include "led.h"

//初始化LED
void LED_Init(void)
{
    //跑马灯引脚为PB 12 13 14 15,LED为低电平有效
    
        
    //F4的芯片在这里还需要配置上下拉电阻PuPd 以及 推挽或者开漏Otype
    
    //打开GPIOB的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

    //定义GPIO_Init的第二个参数:结构体变量
    GPIO_InitTypeDef GPIO_InitStructure;                
    //↑在C99下不需要放到第第一行
    
    
    //下面的GPIO是分开配置的,其实可以写成A||B.这样就不用写那么多次了。这里我就不改了
    
    
    //PB12
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    //配置PC13的端口为推挽输出模式,速度为50MHz
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);//GPIO_Init,初始化GPIO B,PB12的GPIO
    GPIO_SetBits(GPIOB,GPIO_Pin_12);//设置GPIO为高电平。LED为熄灭
    //
    
   //PB13
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_13);
    
   //PB14
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_14);
    
    
   //PB15
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_15);
    
}

//熄灭LED
void LED_OFF(uint8_t num)
{
    if(num == 0)
    {
        return;
    }
    if(num == 1)
    {
        GPIO_SetBits(GPIOB,GPIO_Pin_12);
    }
    else if(num == 2)
    {
        GPIO_SetBits(GPIOB,GPIO_Pin_13);
    }
    else if(num == 3)
    {
        GPIO_SetBits(GPIOB,GPIO_Pin_14);
    }
    else if(num == 4)
    {
        GPIO_SetBits(GPIOB,GPIO_Pin_15);
    }
}

//点亮LED
void LED_ON(uint8_t num)
{
    if(num == 0)
    {
        return;
    }
    if(num == 1)
    {
        GPIO_ResetBits(GPIOB,GPIO_Pin_12);
    }
    else if(num == 2)
    {
        GPIO_ResetBits(GPIOB,GPIO_Pin_13);
    }
    else if(num == 3)
    {
        GPIO_ResetBits(GPIOB,GPIO_Pin_14);
    }
    else if(num == 4)
    {
        GPIO_ResetBits(GPIOB,GPIO_Pin_15);
    }
}

//反转LED
void LED_Flip(uint8_t num)
{
    if(num == 0)
    {
        return;
    }
    else if(num == 1)
    {
        if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_12) == Bit_SET)
        {
            GPIO_ResetBits(GPIOB,GPIO_Pin_12);
        }
        else
        {
            GPIO_SetBits(GPIOB,GPIO_Pin_12);
        }
    }
    else if(num == 2)
    {
        if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_13) == Bit_SET)
        {
            GPIO_ResetBits(GPIOB,GPIO_Pin_13);
        }
        else
        {
            GPIO_SetBits(GPIOB,GPIO_Pin_13);
        }
    }
    else if(num == 3)
    {
        if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_14) == Bit_SET)
        {
            GPIO_ResetBits(GPIOB,GPIO_Pin_14);
        }
        else
        {
            GPIO_SetBits(GPIOB,GPIO_Pin_14);
        }
    }
    else if(num == 4)
    {
        if(GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_15) == Bit_SET)
        {
            GPIO_ResetBits(GPIOB,GPIO_Pin_15);
        }
        else
        {
            GPIO_SetBits(GPIOB,GPIO_Pin_15);
        }
    }
}

LED.h文件如下

#ifndef __LED_H //条件编译
#define __LED_H

#include "stm32f10x.h"

//初始化LED
void LED_Init(void);

//点亮LED
void LED_ON(uint8_t num);

//熄灭LED
void LED_OFF(uint8_t num);

//反转LED
void LED_Flip(uint8_t num);
#endif

5.[正点]MDK中寄存器地址名称映射的再理解

举个栗子就比较明白了。

比如(这里的外设是不准确的,具体要看手册里的。)

外设基地址为 0x01000000 APB1的外设基地址为0x00100000

  • GPIOA外设是挂载在AHB1上的。 GPIOA地址为 0x00110000

APB2的外设基地址为0x00200000

  • I2C外设是挂载在AHB2上的。 I2c的地址为 0x00210000

看图大概就是这样子

每一级往下都会有一个偏移量。是相对于他的母目录的地址的偏移量

6.[正点]时钟树系统的了解

STM32(ARM)的时钟为什么要设置的那么复杂

为什么ARM的芯片不能像51单片机就一样,全部都弄成一个时钟呢?

  1. 有利于省电 51单片机是很早之前比较广泛应用,当时对芯片的功耗高低的要求不高 而STM32作为新型的芯片,他的内核是Cortex - M3的内核。他对于功耗的要求就比较高。这里的高不是功耗要变高,而是对功耗控制的精度变高。不同的外设用不同的频率。因为频率越高。功耗也就越高
  2. 不同外设的频率不相同 使用不同外设,如果用相同的频率。那么外设的抗干扰能力就会变弱。功耗也会变大。

分析时钟树的方法(这里是对F4系列芯片)

时钟分析方法

要分析之前,首先要知道梯形符号是什么意思. 梯形符号叫做**“选择器”** 他可以选择多个时钟的其中一条来输出

  1. LSI 低速内部时钟 L - low低速 S - speed 速度 I - interior内部

    频率为32KHZ,是内部的LC振荡器,不太稳定。一般是给独立看门狗做时钟的。(看门狗也需要使能看门狗的使能位。)

  2. LSE 低速外部时钟

    E - external 外部 这里是我们外接的(低速)晶振时钟。它的稳定性就很高了。

  3. HSI 高速内部时钟 16Mhz 。 内部的,不稳定

  4. PLLCLK 锁相环时钟输出 有俩,第一个是主用的,第二个是专用的。 其中的xN是倍频器。 /P是分频器。 /Q是USB模块用的。/R主用的没用到。

    为什么要有一个专用的呢?因为对于I2S的时钟对频率要求非常高,所以是专用的。

    可以看到,主PLL锁相环输出的系统时钟。可以经过选择器。 AHB与分频器给很多的外设 AHB的信号,又能经过APBX的与分频器,又能产生很多时钟。 APBX就是给相关外设用的。

  5. HSE 高速外部时钟(可以接4-26M的外部时钟)

  6. MCO1MCO2 这两个是可以经过分频器和选择器来输出到芯片引脚上的

总结:

  1. STM32 有5个时钟源:HSI、HSE、LSI、LSE、PLL.

    1. HSI是高速内部时钟,RC振荡器,频率为16MHZ,精度不高。可以直接作为系统时钟或者用作PLL时钟输入。
    2. HSE是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为4MHz~26MHZ。
    3. LSI是低速内部时钟,RC振荡器,频率为32KH2,提供低功耗时钟。主要供独立看门狗和自动唤醒单元使用。
    4. LSE是低速外部时钟,接频率为32.768kHz的石英晶体。RTCPLL为锁相环倍频输出。STM32F4有两个PLL
      1. 主PLL(PLL)由HSE或者HSI提供时钟信号,并具有两个不同的输出时钟
        1. 第一个输出PLLP用于生成高速的系统时钟(最高168MHz)
        2. 第二个输出PLLQ用于生成USBOTGFS的时钟(48MHZ),随机数发生器的时钟和SDIO时钟。
      2. 专用PLL(PLLI2S)用于生成精确时钟,从而在12S接口实现高品质音频性能
  2. .系统时钟SYSCLK可来源于三个时钟源:

  3. HSI振荡器时钟

  4. HSE振荡器时钟

  5. PLL时钟

常用时钟配置寄存器

  • RCC时钟控制寄存器 RCC_CR

    主要用来配置、使能时钟源。就绪时钟源的

  • RCC PLL配置寄存器 RCC_PLLCFGR

    是用来配置PLL锁相环输出中的P、Q、R等值。

  • RCC 时钟配置寄存器 RCC_CFGR

    用来配置分频系数以及时钟源。(梯形以及方块里的/M的配置)

  • RCC AHBX外设时钟使能寄存器 RCC_ AHBXENR

    使能一些外设的时钟

  • RCC APB1、APB2 外设时钟使能寄存器 RCC_APBXENR

    使能一些外设的时钟

7.[正点]SystemInit.c时钟系统初始化文件了解

在系统初始化之后,是先调用Systemlnit函数,然后才调用main函数的。(这点在.s的启动文件哪里,可大概看到汇编指令是先执行sys再执行main的)

这个文件会打开时钟以及复位一些东西,具体可以去手册的RCC和SystemInit.c文件中对应的去看。把16进制翻译为2进制后,找到寄存器的对应位就OK。这样就知道这些是什么作用了。

这个建议新手不要改

8.[正点]Systick定时器

Systick定时器基础知识了解

  • Systick定时器,是一个简单的定时器,对于CM3.CM4内核芯片,都有Systick定时器
  • Systick定时器常用来做延时,或者实时系统的心跳时钟这样可以节省MCU资源,不用浪费一个定时器。比如UCOS中,分时复用,需要一个最小的时间戳,一般在STM32+UCOS系统中,都采用Systick做UCOS心跳时钟:
  • Svstick定时器就是系统滴答定器一个24 位的倒计数定时器, 计到0时,将从RELOAD寄存器中自动重装载定时初值。只要不把它在SvsTick控制及状态寄存器中的使能位清除,就永不停息,即使在睡眠模式下也能工作:
  • SysTick定时器被捆绑在NVIC中,用于产生SYSTICK异常(异常号:15) 意思就是它能够产生中断。 Systick中断的优先级也可以设置

Systick定时器的四个寄存器

  • SysTick控制和状态寄存器 - CRTR

    可以配置使能、是否产生中断(异常请求)、配置内外部时钟源 以及在一次循环之后。可以读取的位段16的值。(读取后复位)

    配置的函数:SysTick_CLKSourceConfig();

  • SysTick 自动重装载初值寄存器 - LOAD 放重装时放的值

  • SysTick 当前值寄存器 - VAL 把重装值复制过来,然后每个时钟周期-1 减到零就再次重装

  • SysTick 校准值寄存器

    不太重要。

Systick相关函数

固件库中在 misc.c文件中

SysTick_CLKSourceConfig(); 时钟源选择

SysTick_Config(uint32_t ticks); 初始化Systick,时钟为HCLK。并开启中断

SYstick中断服务函数

void SysTick_Handler(void);

尝试使用Systick写一个延时函数

  1. 把定义好Systick的重装值。

    这里使用到了中断与Systick的 LOAD 寄存器

    // 延时函数初始化
    void Delay_Init(void)
    {
        // 配置 SysTick 为 1us 中断一次
        if (SysTick_Config(SystemCoreClock / 1000000))
        {
            while (1);  // 配置错误,进入死循环
        }
    }
    

    SysTick_Config是SysTick的时钟源选择函数。 起内部的代码是

    static __INLINE uint32_t SysTick_Config(uint32_t ticks)
    { 
      if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
                                                                   
      SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
      NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
      SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
      SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                       SysTick_CTRL_TICKINT_Msk   | 
                       SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
      return (0);                                                  /* Function successful */
    }
    

    在判断是否超过最大值后,这个函数会把你传入的值放到SYStick的LOAD寄存器(重装寄存器)中。 然后设置优先级为最高 并且将VAL寄存器(当前值寄存器)置零。 噔噔蹬蹬….

    SystemCoreClock 是 STM32 标准库中定义的一个全局变量,用于表示系统的核心时钟频率。使用了条件编译的命令。这里默认为F1的72M

    那么SysTick_Config(SystemCoreClock / 1000000) 的意思就是设置系统滴答定时器的重装值为为7200(72 000 000000/1000000)。

    因为晶振的速度为72M,也就是一秒钟有72M个高电平。

    所以晶振震荡7200次花费的时间就是1us。

    所以可以这样去判断

    // 延时函数
    void Delay_us(uint32_t us)
    {
        TimingDelay = us;
        while (TimingDelay!= 0);
    }
    
    // SysTick 中断处理函数
    void SysTick_Handler(void)
    {
        if (TimingDelay!= 0)
        {
            TimingDelay--;
        }
    }
    

    其中的中断函数void SysTick_Handler(void),是库函数固定的,必须要用这个名字。他是SysTick的固定中断服务函数。

    void Delay_us(uint32_t us) 是定义的us函数

    举例子说,想Delay_us 函数中传参:50.

    那么全局变量TimingDelay == 50

    然后就会进入到while (TimingDelay!= 0); 命令中一直等待。此时就在等待中断处理函数把全局变量TimingDelay 减为0.然后跳出函数。此时程序就能继续运行了。

    在中断处理函数中,每当重装值7200 减 到 0 之后。就会触发一次中断,(也就是执行一次 SysTick_Handler()函数。)全局变量TimingDelay就会减一。当他捡到0之后就不会再减了。

    要实现1ms延时。也很简单。只需要把传入的值x1000就OK啦

    // 毫秒级延时函数
    void Delay_ms(uint32_t ms)
    {
        Delay_us(ms * 1000);
    }
    

    这次用这个Delay函数来实现一下闪烁灯试试.完全Ok。

    (如果在编译的时候报错,根据报错信息。大概率要去stm32f103x_it.h找到void SysTick_Handler(void)函数然后注释掉。不然会出现多次定义。头文件。)

    int main()
    {
    
        KEY_Init();//初始化KEY
        LED_Init();//初始化LED
        Delay_Init();//初始化延时函数
        
        while(1)
        {
            LED_ON(1);
            Delay_ms(500);
            LED_OFF(1);
            Delay_ms(500);
        }
    }
    

9.[正点&江协]IO引脚的复用和映射

什么是端口复用?

STM32有很多的内置外设,这些外设的外部引脚都是与GPIO复用的。 也就是说,一个GPIO如果可以复用为内置外设的功能引脚,那么当这个GPIO作为内置外设使用的时候,就叫做复用。

举个栗子: 比如串口1的俩引脚是PA9和PA10。那么如果PA9和PA10端口不用做串口(USART),二用作复用功能的串口1的发送接收引脚的时候,就叫做端口复用

查端口的表在手册可以看到。

每个引脚都会有一个复用器(其实是时钟树那种的梯形选择器),选择器可以选择连接对应的复用功能引脚。

端口复用映射是通过AFRL或者AFRH来配置的

0-7八个寄存器是寄存器AFRL控制的。每四位控制一个GPIO口。 同理AFRH也是一样

首先参考复用端口示意图,然后去配置对应的端口寄存器。

芯片的复用功能映射配置

引脚有不同的作用,

  1. 系统功能: 将 I/O 连接到 AF0,然后根据所用功能进行配置(比如):

    • JTAG/SWD:在各器件复位后,会将这些引脚指定为专用引脚,可供片上调试模块立即使用(不受 GPIO 控制器控制)。
    • RTC REFIN:此引脚应配置为输入浮空模式。
    • MCO1 和 MCO2:这些引脚必须配置为复用功能模式
  2. GPIO 在 GPIOX_MODER 寄存器中将所需 I/0 配置为输出或输入。

  3. 外设复用功能

    对于ADC和DAC,需要再GPIOx_MODER寄存器把IO配置为模拟通道,

    对于其他外设

    • 在 GPIOx_MODER 寄存器中将所需 I/0 配置为复用块能
    • 通过 GPIOX OTYPER、GPIOX PUPDR 和 GPIOx OSPEEDER 寄存器,分别选择类型、上拉/下拉以及输出速度
    • 在 GPIOX AFRL 或 GPIOX AFRH 寄存器中,将 I/0 连接到所需 AFx

[江协]什么是通信

通信的目的:将一个设备的数据传到另一个设备,扩展硬件系统

通信协议:定制通信的规则,通信双方按照协议规则进行数据收发

[江协]常用通信方式以及不同

名称引脚双工时钟电平设备
USARTTX、RX全双工异步单端点对点
I2CSCL、SDA半双工同步单端多设备
SPISCLK、MOSI、MISO、CS全双工同步单端多设备
CANCAN_N、CAN_L半双工异步差分多设备
USBDP、DM半双工异步差分点对点

[江协]什么是串口通信USART

串口是什么

  • 串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信
  • 单片机的串口可以使单片机与单片机、单片机与电脑、单片机与名式各样的模块互相通信,极大地扩展了单片机的应用范围,增强了单片机系统的硬件实力
  • 串口一般使用异步通信,所以一般要双方约定一个通信速率

串口的连接注意事项及分类

  • 简单双向串口通信有两根通信线(发送端TX和接收端RX)
  • TX与RX要交叉连接
  • 当只需单向的数据传输时,可以只接一根通信线
  • 当电平标准不一致时,需要加电平转换芯片
    • 电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压与数据的对应关系,1串口常用的电平标准有如下三种:
      • TTL电平:+3.3V或+5V表示1,0V表示0
      • RS232电平:-3~-15V表示1,+3~+15V表示0(一般在大型机器上使用)
      • RS485电平**:两线压差**+2~+6V表示1,-2~-6V表示0(差分信号) (可以传输上千米。上面俩就几十米)

串口的参数

  1. 波特率: 串口通信的速率 串口一般使用异步通信,所以一般要双方约定一个通信速率
  2. 起始位: 标志一个数据帧的开始,固定为低电平 高速接收设备我要发送数据了。
  3. 数据位: 数据帧的有效载荷,1为高电平,0为低电平,低位先行
  4. 校验位: 用于数据验证,根据数据位计算得来 无校验、奇校验和偶校验 奇校验就是通过校验位保证数据位和校验位为奇数个1. 偶校验就是通过校验位保证数据位和校验位为偶数个1. (更复杂的话可以去了解CRC校验)
  5. 停止位: 用于数据帧间隔,固定为高电平

USART简介及框图介绍

USART通用同步/异步收发器 Universal Synchronous/Asynchronous Receiver/Transmitter

UART是异步收发器。一般很少用。S只是多了一个时钟输出而已。

它只支持时钟输出,不支持时钟输入。(或许是为了别的通信协议设计)

我们学习的主要是异步通信。

USART是STM32内部集成的硬件外设。

STM32F103C8T6的USART资源有 USART1、USART2、USART3

其中USART1为APB1总线上、USART2、USART3在APB2总线上挂载

USART框图

左边的IRDA之类的是别的通信协议要用的,这里不做了解

只了解一下串口通信。

TX数据发送,是由发送移位寄存器发送的

同理,RX数据接收,是通过接受移位寄存器接受的。

最上边的发送(TDR)\接收(RDR)数据寄存器。发送或者接受的字节数据都存在这里。 这是俩寄存器,共用同一个地址,在程序上就用一个数据寄存器DR表示。

TDR是只写的,RDR是只读的。

他们下边的发送移位寄存器,和接收移位寄存器,是把一个字节的数据,一位一位的移出去。

如果此时进行了写操作,写入的字节会存放在发送寄存器TDR中,在等待发送移位寄存器的数据移位完毕后,会立刻把这个数据赋值(送)到移位寄存器中。然后置一个标志位 1 ,我们只需要检查这个发送数据寄存器TDR置的TEX标志位,TEX是否为1.只要为1 就可以在TDR写入下一个数据了。 移位寄存器会收到发送端控制的控制,向右一位一位的把数据输出到TX引脚。向右移位。也就是低位先行

接收端也是类似,也是向右移位。当接收移位寄存器满了之后,就会一下子把数据转移到接受数据寄存器RDR。转移的过程中,也会 置一个RXNE标志位,。当RXNE为1时,就可以把数据读走了 。

剔除帧头帧尾是电路帮我们自动删除了。

唤醒单元是用多设备串口通信用的。 硬件流控也是交叉连接,用来控制双方数据停止继续的,

右边的SCLK是会每发送一个字节就输出一下高电平。可以在别的硬件上做自适应的波特率检测。

再往下就是波特率发生装置,其实就是个分频器。他们的频率不同。

简化后其实就是这样子

STM32在接受端的天才设计

STM32在接收时,为了应对噪声。有一个很好的处理方法。对高电平时一次采样,但如果突然检测到下降沿(也就是起始位)

就会立马开始进行16次采样。3次一组。两组一次。这两组如果全都是0,就代表起始位确实开始了。不是噪声。就可以开始采样了。 但是如果发现某组有一个1,俩0.就代表他有些许的噪声,但是不会做处理,这时如果有俩组通过,也算他检测到起始位了。不过会有一个噪声标志位,NE。会提醒你一下,数据我收到了。但是你悠着点用。

再其次就是有某组为俩0,那么这组就废了。继续开始下一组,直到连着两组通过。就算成功!

如果通过,他就会在第7 8 9 次采样的时候为有效采样。也是遵循2:1的原则。 7 8 9次检测也是刚刚好在最中间,非常好!

[正点(但是F103板)]端口的复用功能配置过程(串口举例)

以PA9、PA10配置为串口为例

  1. 使能端口GPIOA的时钟

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

  2. 使能复用外设时钟

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);

  3. 把端口模式配置为复用功能(除了ADC和DAC都得这么配置) F1这里与F4的不太一样 F4的是GPIO_PinAFConfig(GPIOA,GPIO_PinSource9)。并且每个端口都需要配置复用。 **但是在F103中,**只需要把输入引脚RX配置成上拉输入或者浮空输入。 把输出引脚TX配置成推挽输出就可以了。

  4. 配置GPIOX_AFRL寄存器或者GPIO_AFRH寄存器,把IO连接到所需要的外设 这里也与F4不同,这里只需要直接配置USART就可以了。然后使能USART的串口

    参考代码如下:

    //初始化
    void Serial_Init(void)
    {
        //使能A9 A10所在的GPIO
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
        
        //使能A9 A10的复用外设时钟
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
        
        //配置PA9端口为复用推挽输出
        GPIO_InitTypeDef GPIO_InitStructure;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽模式
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;       //9
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
        //这里对于F4芯片可以单独 分开的设置复用以及上下拉。不用分开配置,
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//复用推挽模式
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;       //9
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
        
        USART_InitTypeDef USART_InitStructure;
        USART_InitStructure.USART_BaudRate = 9600;//波特率。会自动算好填入BRR寄存器
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制、不使用.
        USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//用| 使用两个功能
        USART_InitStructure.USART_Parity = USART_Parity_No; //校验位。
        USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位1位。
        USART_InitStructure.USART_WordLength = USART_WordLength_8b; //不需要校验,所以字长选择8位字长。
        USART_Init(USART1,&USART_InitStructure);
        
        USART_Cmd(USART1,ENABLE);
    }
    

10.[正点]NVIC中断优先级管理

NVIC中断优先级分组

不同的系列芯片,他的中断个数和中断可编程优先级的是不一样的。 中断分为内核 中断和可屏蔽中断。

在手册中可以在“中断和事件”中查找到中断的列表

分组配置是在寄存器SCB - >AIRCR中配置的。

这个寄存器是位于8 9 10位的。可以有以下分类

AIRCR[10:8]IP Bit[7:4]分配情况分配结果
01110:40位抢占优先级,4位相应优先级
11101:31位抢占优先级,3位相应优先级
21012:22位抢占优先级,2位相应优先级
31003:13位抢占优先级,1位相应优先级
40014:04位抢占优先级,0位相应优先级

每个中断有一个寄存器, ip bit 他的定位是4 5 6 7四个位

他决定了有几个位是用来设置抢占优先级、有几个是用来响应优先级。

  • 抢占优先级:数字越低级别越高。高级别的抢占优先级的中断可以打断正在进行中断的低级别的抢占优先级的中断
  • 相应优先级:数字越低级别越高,!**在抢占优先级相同的情况才会生效。**此时若两个中断同时发生,那么高响应优先级的会优先执行。但若已经有相同抢占优先级的中断在运行了。是不会去打搅的。(也就是说在相同抢占优先级,并且同时请求中断的时候。响应优先级才会生效。)

抢占优先级和响应优先级 的区别

  • 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。
  • 抢占优先级粕同的中断,高响应优先级不可以打断低响应优先级的中断。
  • 当两个中断同时发生的情况抢占优先级相同的中断,下,哪个响应优先级高,哪个先执行。
  • 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行;

一般系统只会设置一次分组。 不然会发生混乱

NVIC中断优先级设置

中断优先级分组函数:void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) 在misc.c的文件中 他实际操作的就是SCB->AIRCR 寄存器

可以在misc.h的头文件看到这个

#define NVIC_PriorityGroup_0         ((uint32_t)0x700) /*!< 0 bits for pre-emption priority
                                                            4 bits for subpriority */
#define NVIC_PriorityGroup_1         ((uint32_t)0x600) /*!< 1 bits for pre-emption priority
                                                            3 bits for subpriority */
#define NVIC_PriorityGroup_2         ((uint32_t)0x500) /*!< 2 bits for pre-emption priority
                                                            2 bits for subpriority */
#define NVIC_PriorityGroup_3         ((uint32_t)0x400) /*!< 3 bits for pre-emption priority
                                                            1 bits for subpriority */
#define NVIC_PriorityGroup_4         ((uint32_t)0x300) /*!< 4 bits for pre-emption priority
                                                            0 bits for subpriority */

中断参数初始化函数void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级
    NVIC_InitTypeDef NCIC_InitStructure;
    NCIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//设置中断通道
    NCIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NCIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//设置抢占优先级
    NCIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//设置响应优先级
    NVIC_Init(&NCIC_InitStructure);//使能中断通道

如果后续需要挂起、解挂,查看中断当前状态,分别调用相应的函数就OK

11.[江协&正点]串口的学习

[江协]F1芯片编写串口收发数据

这里使用的是STM32F103C8T6的最小系统板

他的USART的串口1是PA9和PA10。

PA9为TXD、PA10为RXD。

准备工作

1.接线

使用到的是USB转串口模块。用到的引脚有RXD、TXD、GND

RXD是发送数据到STM32的TXD(PA9)

TXD是接受数据从STM32的RXD(PA10)

GND是为了有电平的参考。要共地。

2.新建工程

这里不详细说了。我添加了一个新的文件给USART用。

这里的编码格式(在右上角的扳手哪里),选择的个GB2312.这个格式在串口收发的时候可以直接看到汉字。而UTF-8需要配置一些东西(在魔术棒里写一行字)

并且开启了魔术邦德Use MCicroLIB。 这是Keil为嵌入式优化的一个精简库。是用来移植Printf用的。

编写程序

由端口的复用功能配置大概可以了解。

其实对于F103端口,他的开启串口1的方式和GPIO的差不太多。

用到的函数在rcc.h和usart.h里。都可以找到。、

这里是先说明了如何接收,然后再说了如何发送。 发送部分分为两种:查询法,中断法 查询则是通过读之前说过的输入寄存器的RXNE寄存器的值来说是够了。

但是对于中断法,需要去配置NVIC代码前期不全。到最后我会补全针对中断的代码。

主模块serial.c

  1. 使能串口所在的GPIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

  2. 使能想开启的外设时钟:USART1。(他挂载在APB2总线)

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);

  3. 分别配置PA9(TXD,发送)、PA10(RXD,接收)为推挽输出和浮空输入(或上拉输入)。

     		//配置PA9端口为复用推挽输出
        GPIO_InitTypeDef GPIO_InitStructure;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽模式
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;       //9
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
    
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//复用推挽模式
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;       //9
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_Init(GPIOA,&GPIO_InitStructure);//初始化
    
  4. 指定启用USART的某个外设(这点跟GPIO那部分的结构体配置很像) 这里需要注意,指定USART的模式的时候,需要用 | 来开启RX和TX两个功能

        USART_InitTypeDef USART_InitStructure;
        USART_InitStructure.USART_BaudRate = 9600;//波特率。会自动算好填入BRR寄存器
        USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控制、不使用.
        USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//用| 使用两个功能
        USART_InitStructure.USART_Parity = USART_Parity_No; //校验位。
        USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位1位。
        USART_InitStructure.USART_WordLength = USART_WordLength_8b; //不需要校验,所以字长选择8位字长。
        USART_Init(USART1,&USART_InitStructure);
        
        USART_Cmd(USART1,ENABLE);
    
  5. 发送字节

    在usart.h文件中,有一条发送字节的指令USART_SendData(USART1,Byte);

    可以这样去使用他

    其中的USART_GetFlagStatus 函数,是读取某个串口中,TXE寄存器(发送移位寄存器移位完成之后,会将寄存器的TXE标志位记为1,这点在[第九大章、[江协]什么是串口通信USART。中有详细提及。)是否为0.如果为0就等待,等待完成之后才发送下一字节。

    void Serial_SendByte(uint16_t Byte)
    {
        USART_SendData(USART1,Byte);
        while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);//如果写标志位没有置1(没有发送完),那么就等
    }
    
  6. 发送数组

    这点其实是在发送字节的函数上再次包装, 通过for循环来输出数组中的每位。

    因为数组在函数中不能求长度(因为此时的数组是一个地址。储存他的是指针变量)

    void Serial_SendArray(uint8_t* Array,uint16_t len)
    {
        uint16_t i = 0;
        for(i = 0; i < len; i++)
        {
            Serial_SendByte(Array[i]);
        }
    }
    
  7. 发送字符串。

    也是在发送字节 的函数上包装,不同的是不用长度了。因为字符串有\0结束标志位

    //发送字符串
    void Serial_SendString(char* String)//有结束的\\0 所以不用再传长度了,
    {
        uint16_t i = 0;
        for(i = 0; (String[i] != '\\0'); i++)
        {
            Serial_SendByte(String[i]);
        }
    }
    
  8. 发送字符形式的数字。

    这点用到的是num先/ 再 %求出个十百千万的位。然后逐字输出。

    先求最大的位。然后+上‘0‘的ASCII码。就能输出字符形式的数字啦。

    这里自己封装了一个pow求次方的函数

    
    //求次方函数
    uint32_t Serial_Pow(uint32_t X,uint8_t Y)
    {
        int Num = 1;
        while(Y--)
        {
            Num *= X;
        }
        return Num;
    }
    
    //发送字符形式的数字
    void Serial_SendNumber(uint32_t Number,uint8_t len)
    {
        //从高位像低位取数字然后输出
        uint8_t i = 0;
        for(i = 0; i < len; i++)
        {
            Serial_SendByte(Number / Serial_Pow(10,len - i - 1) %10 + '0');
        
        }
    }
    
  9. 移植printf。。

    这里需要包含两个头文件。“stdio.h” “stdarg.h”

    用到了可变参数这玩意

    不太懂,没学过。直接抄过来吧..

    //下边没学过,直接搬过来的。
    /**
      * 函    数:使用printf需要重定向的底层函数
      * 参    数:保持原始格式即可,无需变动
      * 返 回 值:保持原始格式即可,无需变动
      */
    int fputc(int ch, FILE *f)
    {
    	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
    	return ch;
    }
    
    /**
      * 函    数:自己封装的prinf函数
      * 参    数:format 格式化字符串
      * 参    数:... 可变的参数列表
      * 返 回 值:无
      */
    void Serial_Printf(char *format, ...)
    {
    	char String[100];				//定义字符数组
    	va_list arg;					//定义可变参数列表数据类型的变量arg
    	va_start(arg, format);			//从format开始,接收参数列表到arg变量
    	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
    	va_end(arg);					//结束变量arg
    	Serial_SendString(String);		//串口发送字符数组(字符串)
    }
    
    

头文件serial.h

#ifndef __SERIAL_H 

#include "stm32f10x.h"
#include "stdio.h"
#include "stdarg.h"

#define __SERIAL_H 

//初始化串口
void Serial_Init(void);

//发送字节
void Serial_SendByte(uint16_t Byte);

//发送数组
void Serial_SendArray(uint8_t* Array,uint16_t len);

//发送字符串
void Serial_SendString(char* String);

//发送字符形式的数字
void Serial_SendNumber(uint32_t Number,uint8_t len);

//移植printf
void Serial_Printf(char *format, ...);

#endif

这时候,我们只需要在主函数中初始化后,就可以发送数据了

如果要接受数据,有两种方法。

第一种方法:查询法

则需要读取RXNE(接收移位寄存器,当收的东西都移位完成之后,会把RXNE位 置1.在被读取时清零。(RXNE是位5) 那么我们就判断他是否为1.然后存起来输出就OK 拉。

代码为:

uint8_t RxData;
    while(1)
    {
        if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
        {
            RxData = USART_ReceiveData(USART1);
            Serial_SendByte(RxData);
        }
    }
}

这个就是查询法的串口接收程序,程序比较简单是可以考虑这个的。

第二种方法:中断法

如果要使用这个方法,我们要在初始化函数中添加中断的代码

流程是:

  1. 开启USART1的RXNE标志位到NVIC的输出。

    USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

  2. 配置NVIC

        USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
        
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
        NVIC_InitTypeDef NCIC_InitStructure;
        NCIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
        NCIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
        NCIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
        NCIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//优先级
        NVIC_Init(&NCIC_InitStructure);
        
    

之后我们就可以在中断函数中使用这个中断了。在IT.h中可以找到。是:USART1_IRQHandler

那么函数内可以这么写

当中断触发,就再次判断是否是RXNE位为1,为真则读取输入寄存器的值,然后赋值给全局变量Serial_RxData,然后把全局变量标志位定义为1.然后手动清除标志位。并自动把全局标志位恢复为0Serial_GetRxfalg

对于获取寄存器的值,也封装了一个函数。Serial_GetRxData 。方便获取

uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

uint8_t Serial_GetRxfalg(void)
{
    if(Serial_RxFlag == 1)
    {
        Serial_RxFlag = 0;
        return 1;
    }
    return 0;
}

uint8_t Serial_GetRxData(void)
{
    return Serial_RxData;
}

void USART1_IRQHandler(void)//中断
{
    if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) == SET)
    {
        Serial_RxData = USART_ReceiveData(USART1);//读取
        Serial_RxFlag = 1;
        USART_ClearITPendingBit(USART1,USART_FLAG_RXNE);//手动清除标志位
    }

}

最后的主函数也几乎没变,

	    uint8_t RxData;
    while(1)
    {
        if(Serial_GetRxfalg() == 1)
        {
            RxData = Serial_GetRxData();
            Serial_SendByte(RxData);
        }
    }

结果如下:

[正点]F4芯片串口收发步骤

我没F4芯片,所以就记一下步骤 F4多了一个引脚复用映射的函数。把引脚映射为串口 并且在GPIO初始化时,使用的模式不是F1的推挽输出和输入了。而是启用了复用功能 其他的都一样哒.

  1. 串口时钟使能: RCC APBxPerphClockCmd(); GPIO时钟使能: RCCAHB1PerphClockcmd();

  2. 引脚复用映射: GPIO PinAFConfig();

  3. GPIO端口模式设置: GPI0 Init(); 模式设置为GPIO Mode AF4

  4. 串口参数初始化:USART Init();

  5. 开启中断并且初始化NVIC (如果需要开启中断才需要这个步骤) NvIc Init(), USART ITConfig0;

  6. 使能串曰:USART Cmd();

  7. 编写中断处理函数:USARTx IRQHandler();

  8. 串口数据收发:void USART SendData();//发送数据到串口,

    DRuint16 tUSART ReceiveData();/接受数据从DR读取接受到的数据

  9. 串口传输状态获取: FlagStatus USART GetFlagStatus(), void USART CleanTPendingBit();

[江协]F1芯片串口收发数据包理解与思路

数据包的使用理解

数据包的作用:把一个个字节打包起来,方便我们进行多字节的数据通信

比如传输位置的XYZ,如果不封装起来。接收方就不知道那个是X,Y ,Z

  • **对于Hex数据包来说,**如果载荷会出现和包头包尾重复的情况。应选择固定包长。这样可以避免接受错误,不然很容易乱套。 如果确定不会出现包头和尾与载荷的重复,那么可以使用可变的包长。因为包头包尾是唯一的

  • Hex的优点是,传输最直接,解析数据简单。适合用类似于串口通信的陀螺仪,温湿度传感器等等。 缺点则是灵活性不好。载荷容易和包头包尾重复

  • 对于文本数据包(其实每个文本字符的背后,都是一个字节的Hex数据包。不过是把他们编码和译码了) 由于译码后,产生的字符。所以我们可以用很多的字符来作为包头和包尾。所以文本是比较灵活的

  • 文本数据包的优点是,数据只管易理解。非常灵活。适合输入指令进行人机交互的场合。比如蓝牙模块常用的AT指令、CNC和 3D打印机常用的G代码。 缺点则是解析效率低,比如发送个数据包100.看着是100但实际上是文本数据1 0 0 还要把文本转换成数据才能得到100.。

    所以,我们要根据实际场景来选择和设计数据包的格式

数据包发送思路

对于数据包发送,我们是自主可控的。是很简单的,这里大概写个代码

数据包接受思路

数据包接收这里相较于发送比较难

这里尝试的是使用的是固定包长HEX数据包的接收方法。

以及尝试使用可变包长的文本数据包的接受方法。

HEX固定包长数据包接收

在接受数据的时候 ,数据是一个字节一个字节传输进来的。

每接受一次字符,程序就会进一次中断,在中断函数中可以拿到这个字节。

所以每拿到一个数据都是一个独立的过程。

但对于数据包来说,数据是有前后关联性的。

所以。要制作一个数据包接受,就要有 状态机 的思维

设计数据包,画一个状态转移图是必要的

对于上图,固定包长的HEX数据包。

我们定义了三个状态,等待包头、收到数据、等待包尾

用标志位S来表示当前在什么状态

这里的包头定义的是0xFF 。包尾定义的是0xFE。

在不同的状态,执行不同的程序。

文本的可变包长数据包接收

如上图,这里定义的包尾是\r和\n。如果不用俩包尾也行,这样更保险。 @是包头

[江协]F1芯片编写串口收发数据包

HEX数据包的发送与接收

在串口收发数据的中断函数部分上进行更改

定义包头和包尾为EE和EF

  1. 删除了读取单字节的函数,因为中断部分要用来读取数据包的每个字节

  2. 添加了两个数组: uint8_t Serial_TxPacket[4] = {0}; 定义发送数据包数组 uint8_t Serial_RxPacket[4] = {0}; 定义接收数据包数组 uint8_t Serial_RxFlag; 仍是接收到数据(包)的标志位

  3. 发送数据包部分很简单,只需要这段代码 先发送包头,再调用发送数组的函数发送数据包。再发送包尾

    //串口发送数据包
    void Serial_SendPacket(void)
    {
        Serial_SendByte(0xFF);
        Serial_SendArray(Serial_TxPacket, 4);
        Serial_SendByte(0xFE);
    }
    
  4. 接收部分则用到了 状态机 机的思想。

    static uint8_t RxState = 0; 是状态机的状态位置。他的值决定了下次进入中断(收到字节)后对数据的处理

    static uint8_t pRxPacket = 0; 是定义下次接收的数据放到缓存数组的那个下标的位置。(其实跟指针差不多,但他不是指针。他就是个记录下标的)

    大概流程为

    1. 若接收到包头:0xFF。则进入第二状态(置标志位为1):存放4位数据 然后重置数据缓存存放位置的下标变量pRxPacket 准备放东西
    2. 开始放,直到放了4个之后,进入下一状态(置标志位为2)。
    3. 检查是否接收到0xFE,然后重新把标志位置0,然后把接收到数据包标志位置1 告诉大家,我的数据包填满了。可以来取了。
    //USART1中断函数
    void USART1_IRQHandler(void)
    {
        static uint8_t RxState = 0;     //定义表示当前状态机状态的静态变量
        static uint8_t pRxPacket = 0;   //定义表示当前接收数据位置的静态变量
        if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)        //判断是否是USART1的接收事件触发的中断
        {
            uint8_t RxData = USART_ReceiveData(USART1);             //读取数据寄存器,存放在接收的数据变量
            
            //使用状态机的思路,依次处理数据包的不同部分
            
            //当前状态为0,接收数据包包头
            if (RxState == 0)
            {
                if (RxData == 0xFF)         //如果数据确实是包头
                {
                    RxState = 1;            //置下一个状态
                    pRxPacket = 0;          //数据包的位置归零。准备开始放东西
                }
            }
            //当前状态为1,接收数据包数据
            else if (RxState == 1)
            {
                Serial_RxPacket[pRxPacket] = RxData;    //将数据存入接收缓存数组的指定位置
                pRxPacket ++;               //数据包的位置自增
                if (pRxPacket >= 4)         //如果收够4个数据
                {
                    RxState = 2;            //置下一个状态
                }
            }
            //当前状态为2,接收数据包包尾
            else if (RxState == 2)
            {
                if (RxData == 0xFE)         //如果数据确实是包尾部
                {
                    RxState = 0;            //状态归0
                    Serial_RxFlag = 1;      //接收数据包标志位置1,成功接收一个数据包
                }
            }
            
            USART_ClearITPendingBit(USART1, USART_IT_RXNE);     //清除移位寄存器移位完成的RXNE标志位
        }
    }
    

    这里我在main函数的测试程序是:

    使用按键,每次按下数组位每位+1.然后显示在OLED屏幕上,

    每次发送,也会被接受,然后显示在OLED上。

    #include "stm32f10x.h"                  // Device header
    #include "led.h"
    #include "delay.h"
    #include "Key.h"
    #include "Serial.h"
    #include "oled.h"
    int main()
    {
    
        KEY_Init();//初始化KEY
        LED_Init();//初始化LED
        Delay_Init();//初始化延时函数
        OLED_Init();//初始化OLED;
        
        Serial_Init();//初始化串口收发数据包
        
        OLED_ShowString(1,1,"TX Packet:");//在OLED1行2列显示发送的数据包
        OLED_ShowString(3,1,"RX Packet:");//在OLED1行4列显示接受的数据包
        
        //初始化数组内的内容
        Serial_TxPacket[0] = 0x01;
        Serial_TxPacket[1] = 0x02;
        Serial_TxPacket[2] = 0x03;
        Serial_TxPacket[3] = 0x04;
        
        while(1)
        {
            if(KEY_Scan(0) == 2)//如果按键2按下,那么对发送的数据包所有字节++。然后显示发送
            {
                
                Serial_TxPacket[0]++;
                Serial_TxPacket[1]++;
                Serial_TxPacket[2]++;
                Serial_TxPacket[3]++;
                //显示
                OLED_ShowHexNum(2,1,Serial_TxPacket[0],2);
                OLED_ShowHexNum(2,4,Serial_TxPacket[1],2);
                OLED_ShowHexNum(2,7,Serial_TxPacket[2],2);
                OLED_ShowHexNum(2,10,Serial_TxPacket[3],2); 
                //发送数据包
                Serial_SendPacket();
            }
    
            
            
            //接收到的数据放到OLED上显示
            if(Serial_GetRxFlag() == 1)
            {
                OLED_ShowHexNum(4,1,Serial_RxPacket[0],2);
                OLED_ShowHexNum(4,4,Serial_RxPacket[1],2);
                OLED_ShowHexNum(4,7,Serial_RxPacket[2],2);
                OLED_ShowHexNum(4,10,Serial_RxPacket[3],2);
            }
        
    
        }
    }
    
    

    程序结果如下:

    俩是可以同时显示的

文本数据包的接收

文本数据包的接收是在HEX数据包的基础上改进的。

因为发送文本数据包使用printf或者自己写的SendString就可以了 不然挺麻烦的。所以这里就写一个文本数据包的接受就可以了。

这里的数据头为@,数据结尾是\r \n

为什么是\r和\n呢。我个人觉得是因为在你发送一段文本的时候,他后面默认带的就是\r\n。

要制作文本接受。要先处理一下HEX的代码

首先把不用的函数和定义的常量都删掉。

比如uint8_t Serial_TxPacket[4] = {0}; 定义发送数据包数组 这行话(记得把头文件里的声明也去除掉。), 还有这段函数

//串口发送数据包
void Serial_SendPacket(void)
{
    Serial_SendByte(0xFF);
    Serial_SendArray(Serial_TxPacket, 4);
    Serial_SendByte(0xFE);
}

然后就要开始改造HEX数据包了!

首先,我们接收的是字符串了。不是数组,并且字符串的长度是可变的。所以可以把原本的接收数组缓存区的uint8_t Serial_RxPacket[4] = {0}; 改为char Serial_RxPacket[100]= {0}; 这样我们就有足够的空间去存放未知的字符串了。

剩下的就是去修改中断函数里的状态机部分了

在接受字符串包的时候。首先要判断@。然后需要先判断是否遇到了结束标志。再进行存放字符,因为我们不知道现在这个字符是不是包尾标志。在遇到\r之后,需要再判断一下。\n 如果\n有效,。那么就可以把接收到包的位置1。

代码如下,几乎是没怎么变的。

//USART1中断函数
void USART1_IRQHandler(void)
{
    static uint8_t RxState = 0;     //定义表示当前状态机状态的静态变量
    static uint8_t pRxPacket = 0;   //定义表示当前接收数据位置的静态变量
    if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)        //判断是否是USART1的接收事件触发的中断
    {
        uint8_t RxData = USART_ReceiveData(USART1);             //读取数据寄存器,存放在接收的数据变量
        
        //使用状态机的思路,依次处理数据包的不同部分
        
        //当前状态为0,接收数据包包头
        if (RxState == 0)
        {
            if (RxData == '@')         //如果数据确实是包头
            {
                RxState = 1;            //置下一个状态
                pRxPacket = 0;          //数据包的位置归零。准备开始放东西
            }
        }

        
        //当前状态为1,接收数据包数据
        else if (RxState == 1)
        {
            if(RxData == '\\r')
            {
                RxState = 2;
            }
            else
            {
                Serial_RxPacket[pRxPacket] = RxData;    //将数据存入接收缓存数组的指定位置
                pRxPacket ++;               //数据包的位置自增
            }
            
        }
        //当前状态为2,接收数据包包尾
        else if (RxState == 2)
        {
            if (RxData == '\\n')         //如果数据确实是包尾部
            {
                RxState = 0;            //状态归0
                Serial_RxPacket[pRxPacket] = '\\0';//添加字符串结束标志位\\0
                Serial_RxFlag = 1;      //接收数据包标志位置1,成功接收一个数据包
            }
        }
        
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);     //清除移位寄存器移位完成的RXNE标志位
    }
}

验证仍然是同上,就不演示了

现在既然接收到了字符串数据,我们就可以利用C语言自带库中的String.h这个头文件中的strcmp这个字符串对比的指令(相同为0)

就可以实现点灯操作了!

代码参考:

#include "stm32f10x.h"                  // Device header
#include "led.h"
#include "delay.h"
#include "Key.h"
#include "Serial.h"
#include "oled.h"
#include "string.h"
int main()
{

    KEY_Init();//初始化KEY
    LED_Init();//初始化LED
    Delay_Init();//初始化延时函数
    OLED_Init();//初始化OLED;
    
    Serial_Init();//初始化串口收发数据包
    
    OLED_ShowString(1,1,"Result:");//在OLED1行2列显示当前命令执行结果
    OLED_ShowString(3,1,"RX Packet:");//在OLED1行4列显示接受的数据包
    
    //初始化数组内的内容
    
    while(1)
    {
        //接收到的数据放到OLED上显示
        if(Serial_GetRxFlag() == 1)//接收到数据
        {
            OLED_ShowString(4, 1, "                 ");
            OLED_ShowString(4, 1,Serial_RxPacket);
            
            
            if( strcmp(Serial_RxPacket,"LED_ON(1)" ) == 0)//如果=0则代表字符串相等
            {
                LED_ON(1);//执行命令并显示
                OLED_ShowString(2,1,"LED1_ON_OK");
            }
            else if( strcmp(Serial_RxPacket,"LED_ON(2)" ) == 0)//如果=0则代表字符串相等
            {
                LED_ON(2);//执行命令并显示
                OLED_ShowString(2,1,"LED2_ON_OK");
            }
            else if( strcmp(Serial_RxPacket,"LED_OFF(1)" ) == 0)//如果=0则代表字符串相等
            {
                LED_OFF(1);//执行命令并显示
                OLED_ShowString(2,1,"LED1_OFF_OK");
            }
            else if( strcmp(Serial_RxPacket,"LED_OFF(2)" ) == 0)//如果=0则代表字符串相等
            {
                LED_OFF(2);//执行命令并显示
                OLED_ShowString(2,1,"LED2_OFF_OK");
            }
            else
            {
                OLED_ShowString(2,1,"ERROR_CMD");
            }
        }
    }
}

结果如图:

12.[正点&江协]外部中断

[正点&江协]什么是外部中断,他的特点是什么

在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行

中断分为内核的中断和外设的中断

中断是由NVIC根据优先级。去判断然后根据优先级告诉CPU

stm32的GPIO口可以当做外部中断的输入

STM32F4的中断控制器支持22个外部中断/事件请求

那么外部中断就是EXTI了

EXTI可以检测电平信号。(上升,下降。双边沿、软件触发)

EXTI线0-15:对应外部IQ口的输入中断 EXTI线16:连接到PVD输出。 EXTI线17:连接到RTC闹钟事件。 EXTI线18:连接到USB OTG FS唤醒事件。 EEXTI线19:连接到以太网唤醒事件 XTI线20:连接到USB OTG HS(在FS中配置)唤醒事件:XTI线21:连接到RTC入侵和时间戳事件。 EEXTI线22:连接到RTC唤醒事件。

由此我们大概可以了解到,STM32的供GPIo使用的中断线是有限的。

需要有终端线才能产生中断请求。

GPIO与中断线的映射关系就是这次的重点

同一时间只能有一个pin口映射到中断线

因为GPIOA 的1和GPIOB的1….他们的引脚是在一个映射器上的。

对于每个中断线,可以设置相应的触发方式,比如上升沿下降沿边沿触发之类的。还包是否要开启等等。

那么,16个中断线就要有16个中断服务函数呢? 不 在F4系列中,只有7个可使用的中断服务函数。 F4中,外部中断线5-9分配分配一个、10-15分配一个中断向量。他们都是共用一个中断函数,

所以我们只能是用EXTI 0 1 2 3 4 9_5 15_10这七个。

也就是说,gpio0只能映射到EXTI0 GPIO3 只能映射到EXTI3 而 GPIO6需要映射到EXTI9_5

对于F103c8t6.同一个pin他的连接是下边这样(图中都是pin16)所以也由此可知,对于同一个pin、是不能同时发生中断请求的。

AFIO是一个数据选择器。它可以把前边16个GPIO口的其中一个连接到EXTI外部中断中

通道数量可以看到。一共有16个GPIO_Pin口+PVD+RTC+USB+ETH。其余四个其实是来蹭网的。PVD是外加PVD输出,RTC为闹钟、USB唤醒USB、以太网唤醒ETH。

所以EXTI有20个输入信号。

如图,再EXTI外部中断连接到NVIC时,把9-5 15-10 goio放到了一个通道里。 所以在用的时候需要判断一下到底是谁弄的

其他相应,则是之前说的事件响应。

AFIO的作用不止于此,我们之前用的 端口重映射(把普通的gpio映射为他复用的功能)。也是他干的。

[江协]外部中断的再理解

如图,是EXTI的框图,在触发终端的二时候,请求挂起寄存器就会置一。就相当于中断的标志位

再往后走,可以看到 请求挂起寄存器和中断屏蔽寄存器,共同进入一个与门。 所以当中断屏蔽寄存器为0时,中断就失能了。如果他们都为1,则进入NVIC进行下一步

事件的也是一样。(二十根线都是这样子的。)

[江协]外部中断的适用场景

外部中断的应用场合 是 “单片机事先不知道我们要去操作,但如果一操作就需要处理的突发事件,并且信号的来源是外部的,stm32不知道什么时候来,并且信号速度很快,要迅速执行”

[江协]外部中断EXTI函数作用介绍

  • void EXTI_DeInit(void); 把EXEI配置清除,恢复成上电默认状态

  • void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); 根据结构体中的参数配置EXTIi外设

  • void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct); 把参数传递的结构体变量赋一个默认值

  • void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line); 软件触发外部中断

  • FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line); 获取指定的标志位是否被置一(建议主函数中使用)

  • void EXTI_ClearFlag(uint32_t EXTI_Line); 对指定的标志位清除(议主函数中使用)

  • ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);

    获取中断标志位是否被置一(只能在中断中使用)

  • void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

    对中断标志位是清除(只能在中断中使用)

[江协]NVIC函数作用介绍

  • void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

    中断分组用,参数为中断分组的方式

  • void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);

    根据结构体里的配置来初始化NVIC

  • void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);

    设置中断向量表,

  • void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);

    系统低功耗配置

  • void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);

    之前讲滴答定时器用过了。

[江协]编写:红外传感器计次

代码如下,已经解释的很全了

#include "stm32f10x.h"                  // Device header

//引脚为A2  

//计次变量
uint16_t CountSensor_Count = 0;

//初始化
void CountSensor_Init(void)
{
    //开启A2的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    //开启挂载在APB2外设的AFIO外设时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
    //EXTI时钟和NVIC的时钟不用手动打开。
    
    //配置GPIO
    GPIO_InitTypeDef GPIO_InitStructrue;
    GPIO_InitStructrue.GPIO_Mode = GPIO_Mode_IPU;        //上拉输入模式
    GPIO_InitStructrue.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructrue.GPIO_Speed = GPIO_Speed_50MHz;    
    GPIO_Init(GPIOA,&GPIO_InitStructrue);
    
    //配置AFIO (F1中在GPIO.h文件中)(目的是为了把GPIOA的PIN2引脚映射到AFIO中。)
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource2);//这里需要根据PIn和GPIOx来选择

    
    //配置EXTI
    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line2;          //选择中断线路,这里是PIN2 所以为2   
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;           //是否使能指定的中断线路
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断或响应模式   
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//上升或下降或边沿触发
    EXTI_Init(&EXTI_InitStructure);
    
    //配置NVIC(因为NVIC属于内核,所以被分配到内核的杂项中去了,在misc.c)
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置抢占和响应优先级
     
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;       //在stm32f10x.h文件里。让你找IRQn_Type里的一个中断通道。这里使用的是md的芯片(如果引脚是15-10或者9-5则需要去找对应的那个)这里我是PIN2.所以找2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;         //是否使能指定的中断通道
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//抢占优先级(这里可以看表。看范围,)
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;       //指定响应优先级
    NVIC_Init(&NVIC_InitStructure);    
    
   
    
}

//获取当前的计数
uint16_t CountSenSor_Get(void)
{
    return CountSensor_Count;
}

//在STM32中,中断的函数都是固定的。他们在启动文件中存放xxx.s 
//以IRQHandler结尾的就是中断函数的名字。
//在这里需要找到对应的中断函数,我这里是2
void EXTI2_IRQHandler(void)//中断函数是无参无返回值的。中断函数必须写对,写错就进不去
{
    //在进入中断后,一般要判断一下这个是不是我们想要的那个中断源触发的中断。
    //但是在这里。我是GPIOA的PIN2引脚,所以不用写。
    //如果是5-9 10-15的引脚。他们EXTI到NVIC是几个共用的。
    //所以需要根据EXTI输入时的16根引脚。来判断是16根引脚的那一根发送的中断请求。
    //这里规范写的话需要加上去
    //查找标志位函数在exit.h中。
    if(EXTI_GetITStatus(EXTI_Line2) == SET)//第一个参数是行数.判断这个线的标志位是不是== SET。是则是我们想要的
    {
    
        CountSensor_Count++;
        EXTI_ClearITPendingBit(EXTI_Line2);//中断结束后,要调用清除标志位的函数。如果你不清除,程序会一直进入中断
    }
}

.h文件如下

#ifndef __COUNTSENSOR_H
#define __COUNTSENSOR_H

//初始化旋转编码计次
void CountSensor_Init(void);

//获取当前计数值
uint16_t CountSenSor_Get(void);

#endif

结果:

12.通用定时器

  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值