STM32F1x固件库函数学习笔记(一)

说明:本文所有例程使用STM32F103C8T6单片机运行无误

一、基础知识

1、什么是STM32

ST是意法半导体,M是Microelectronics(微控制器),32表示32位。合起来理解是:STM32就是ST公司开发的32位微控制器。

微控制器和微处理器区别:
最主要的区别在硬件结构上,微处理器就是一个单芯片的CPU,而微控制器则在集成了CPU和其他电路,构成了一个完整的微型计算机系统。微控制器除了CPU还包含RAM、ROM、串行接口、计时器、中断。

2、STM32诞生背景

51 是嵌入式学习中一款入门级的精典 MCU,因其结构简单,易于教学,且可以通过串口编程而不需要额外的仿真器,所以在教学时被大量采用,至今很多大学在嵌入式教学中用的还是 51。 51 诞生于 70 年代,属于传统的 8 位单片机,如今,久经岁月的洗礼,既有其辉煌又有其不足。现在的市场产品竞争越来越激烈,对成本极其敏感,相应地对 MCU的 性能要求也更苛刻: 更多功能, 更低功耗,易用界面和多任务。面对这些要求, 51 现有的 资源就显得得抓襟见肘。所以无论是高校教学还是市场需求,都急需一款新的 MCU 来为这个领域注入新的活力。

基于这样的市场需求, ARM 公司推出了其全新的基于 ARMv7 架构的 32 位 CortexM3 微控制器内核。紧随其后, ST(意法半导体)公司就推出了基于 Cortex-M3 内核的MCU—STM32。 STM32 凭借其产品线的多样化、极高的性价比、简单易用的库开发方 式,迅速在众多 Cortex-M3 MCU 中脱颖而出,成为最闪亮的一颗新星。 STM32 一上市就 迅速
占领了中低端 MCU 市场,受到了市场和工程师的无比青睐,颇有星火燎原之势。

作为一名合格的嵌入式工程师,面对新出现的技术,我们不是充耳不闻,而是要尽快吻合市场的需要,跟上技术的潮流。如今 STM32 的出现就是一种趋势,一种潮流,我们要做的就是搭上这趟快车,让自己的技术更有竞争力。

——参考野火原文《零死角玩转STM32—F429挑战者》

3、STM32分类

STM32 有很多系列,可以满足市场的各种需求,从内核上分有 Cortex-M0、 M3、 M4
和 M7 这几种在这里插入图片描述
——参考野火原文《零死角玩转STM32—F429挑战者》

4、STM32F1X系列命名规则

在这里插入图片描述

5、STM32F103C8T6最小系统

分享一个我之前学习STM32绘制的一个最小系统
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、STM32固件库

1、初始固件库

(1)51单片机的寄存器

之前写过51单片机的博客,对单片机的寄存器一些简要的说明,见下图
(图一)
回顾之前学习51单片机串口向PC机发送“0x01”的程序,如下:

#include <reg51.h>

/*向串口发送一字节数据*/
void SendOneByte(unsigned char c)
{
    SBUF = c;
    while(!TI);
    TI = 0;
}

void InitUART(void)
{
    TMOD = 0x20;//设置定时器1,工作方式2,8位自动重载
    SCON = 0x40;//设置串口工作方式1
    TH1 = 0xFD;//波特率设置为9600
    TL1 = TH1;
    PCON = 0x00;//波特率不加倍
    EA = 1;//开总中断
    ES = 1;//允许串联1中断
    TR1 = 1;//启动定时器T1
}

void main(void)
{
    InitUART();
	SendOneByte(0x01);
	while(1);
}

void UARTInterrupt(void) interrupt 4
{
    if(RI)
    {
        RI = 0;
        //add your code here!
    }
    else
        TI = 0;
}


通过这个程序我们可以理解到,CPU只有读和写这个操作,而我们要通过读和写寄存器变量的方式来实现来实现单片机控制外设的目的。
在程序中,我们通过给TMOD、SCON、TH1、TL1、PCON、EA、ES、TR1寄存器写数据来达到初始化串口的目的,而在串口中断中,我们又通过读RI寄存器来判断一帧数据发送完毕。

(2)STC8A通过库函数方式实现LED闪烁

学习过STC8单片机的应该知道,STC8是51单片机的一个升级版本,它完全兼容8051,可以使用直接操作寄存器的方式编程,也可以使用STC8A系列库函数进行开发,如下例程是使用“STC8A-SOFTWARE-LIB”实现LED闪烁的程序。

#include	"config.h"
#include	"GPIO.h"
#include	"delay.h"

void	GPIO_config(void)
{
	GPIO_InitTypeDef	GPIO_InitStructure;		//结构定义
	GPIO_InitStructure.Pin  = GPIO_Pin_0 | GPIO_Pin_1;		//指定要初始化的IO,
	GPIO_InitStructure.Mode = GPIO_PullUp;		//指定IO的输入或输出方式,GPIO_PullUp,GPIO_HighZ,GPIO_OUT_OD,GPIO_OUT_PP
	GPIO_Inilize(GPIO_P5,&GPIO_InitStructure);	//初始化
}

void main(void)
{
	GPIO_config();
	
	while(1)
	{
		P50 = ~P50;
		delay_ms(250);
		P51 = ~P51;
		delay_ms(250);
	}
}

通过这个程序,我们我们初始化STC8A的GPIO的时候,我们不需要直接写寄存器,而是先使用GPIO_InitTypeDef定义GPIO初始化结构体变量,然后调用GPIO_Inilize()函数,直接初始化GPIO。这便是一个经典的使用库函数编程。

那么,GPIO_InitTypeDef和GPIO_Inilize()函数里都有什么呢?我们看看

//GPIO_InitTypeDef
typedef struct
{
	u8	Mode;		//IO模式,  		GPIO_PullUp,GPIO_HighZ,GPIO_OUT_OD,GPIO_OUT_PP
	u8	Pin;		//要设置的端口	
} GPIO_InitTypeDef;
//========================================================================
// 函数: u8	GPIO_Inilize(u8 GPIO, GPIO_InitTypeDef *GPIOx)
// 描述: 初始化IO口.
// 参数: GPIOx: 结构参数,请参考timer.h里的定义.
// 返回: 成功返回0, 空操作返回1,错误返回2.
// 版本: V1.0, 2012-10-22
//========================================================================
u8	GPIO_Inilize(u8 GPIO, GPIO_InitTypeDef *GPIOx)
{
	if(GPIO > GPIO_P7)				return 1;	//空操作
	if(GPIOx->Mode > GPIO_OUT_PP)	return 2;	//错误
	if(GPIO == GPIO_P0)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P0M1 &= ~GPIOx->Pin,	P0M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P0M1 |=  GPIOx->Pin,	P0M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P0M1 |=  GPIOx->Pin,	P0M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P0M1 &= ~GPIOx->Pin,	P0M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P1)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P1M1 &= ~GPIOx->Pin,	P1M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P1M1 |=  GPIOx->Pin,	P1M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P1M1 |=  GPIOx->Pin,	P1M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P1M1 &= ~GPIOx->Pin,	P1M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P2)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P2M1 &= ~GPIOx->Pin,	P2M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P2M1 |=  GPIOx->Pin,	P2M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P2M1 |=  GPIOx->Pin,	P2M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P2M1 &= ~GPIOx->Pin,	P2M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P3)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P3M1 &= ~GPIOx->Pin,	P3M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P3M1 |=  GPIOx->Pin,	P3M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P3M1 |=  GPIOx->Pin,	P3M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P3M1 &= ~GPIOx->Pin,	P3M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P4)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P4M1 &= ~GPIOx->Pin,	P4M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P4M1 |=  GPIOx->Pin,	P4M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P4M1 |=  GPIOx->Pin,	P4M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P4M1 &= ~GPIOx->Pin,	P4M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P5)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P5M1 &= ~GPIOx->Pin,	P5M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P5M1 |=  GPIOx->Pin,	P5M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P5M1 |=  GPIOx->Pin,	P5M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P5M1 &= ~GPIOx->Pin,	P5M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P6)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P6M1 &= ~GPIOx->Pin,	P6M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P6M1 |=  GPIOx->Pin,	P6M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P6M1 |=  GPIOx->Pin,	P6M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P6M1 &= ~GPIOx->Pin,	P6M0 |=  GPIOx->Pin;	 //推挽输出
	}
	if(GPIO == GPIO_P7)
	{
		if(GPIOx->Mode == GPIO_PullUp)		P7M1 &= ~GPIOx->Pin,	P7M0 &= ~GPIOx->Pin;	 //上拉准双向口
		if(GPIOx->Mode == GPIO_HighZ)		P7M1 |=  GPIOx->Pin,	P7M0 &= ~GPIOx->Pin;	 //浮空输入
		if(GPIOx->Mode == GPIO_OUT_OD)		P7M1 |=  GPIOx->Pin,	P7M0 |=  GPIOx->Pin;	 //开漏输出
		if(GPIOx->Mode == GPIO_OUT_PP)		P7M1 &= ~GPIOx->Pin,	P7M0 |=  GPIOx->Pin;	 //推挽输出
	}
	return 0;	//成功
}

我们查看GPIO_InitTypeDef结构体时发现mode可以直接赋值GPIO_PullUp, GPIO_HighZ, GPIO_OUT_OD, GPIO_OUT_PP,那么这些又是什么呢?

我们查看GPIO.h文件可以得知如下:

#define	GPIO_PullUp		0	//上拉准双向口
#define	GPIO_HighZ		1	//浮空输入
#define	GPIO_OUT_OD		2	//开漏输出
#define	GPIO_OUT_PP		3	//推挽输出

我们还在GPIO.h程序中看到其他已经宏定义好的字符串

#define	GPIO_Pin_0		0x01	//IO引脚 Px.0
#define	GPIO_Pin_1		0x02	//IO引脚 Px.1
#define	GPIO_Pin_2		0x04	//IO引脚 Px.2
#define	GPIO_Pin_3		0x08	//IO引脚 Px.3
#define	GPIO_Pin_4		0x10	//IO引脚 Px.4
#define	GPIO_Pin_5		0x20	//IO引脚 Px.5
#define	GPIO_Pin_6		0x40	//IO引脚 Px.6
#define	GPIO_Pin_7		0x80	//IO引脚 Px.7
#define	GPIO_Pin_All	0xFF	//IO所有引脚
	
#define	GPIO_P0			0		
#define	GPIO_P1			1
#define	GPIO_P2			2
#define	GPIO_P3			3
#define	GPIO_P4			4
#define	GPIO_P5			5
#define	GPIO_P6			6
#define	GPIO_P7			7

(3)寄存器开发与库函数开发

我通过51单片机初始化串口和STC8A库函数方式实现LED灯闪烁,两个简单的例子,简单说明了,直接操作寄存器开发与调用库函数开发。

在51单片机开发过程中,因为51单片机相对简单,所以都是直接使用操作寄存器开发方式,这些工作很繁琐、机械。但是到了STM32开发时,外设资源非常丰富,必然寄存器和和复杂度增加了,直接配置寄存器方式就会造成开发速度慢、程序可读性差、维护复杂等缺点。而调用库函数开发正好可以弥补这些缺点。

但我们通过上面STC8A使用库函数方式使LED闪烁可以看到,只是一个简单的GPIO初始化源码代码量居然就这么大。这也是库函数开发的缺点,使用库函数开发会使代码量增大,库函数会把所有情况考虑到库函数里,势必会造成代码冗余。

所以直接操作寄存器开发不可否认还是具备程序运行占用资源少、代码简单明了、具体参数更直观的优点。

在摩尔定律预言中,每隔18个月,计算机的芯片集成度会提高一倍,功能提高一倍,而价格则下降为一半。现如今,STM32有足够的资源,权衡寄存器和库函数开发的优缺点,我们大多数的时候都会牺牲一些CPU资源来使用库函数开发。只有在一些对代码要求极为苛刻的地方,才会使用直接操作寄存器开发。调用库函数开发说得更加直接了当一点就是:我想偷点懒,然后就直接白嫖官方写好的程序,直接拿来用。而官方的程序帮我们把寄存器的工作全部都已经配置完成了,我不需要了解人家是怎么配置的,我知道的是白嫖过来后,程序下到板子里居然能跑

(4)STM32F1固件库开发参考资料

1、STM32F1X固件函数库:用于查阅STM32F1标准库,有英文的帮助文档,也要翻译好的中文pdf文档。
2、STM32F103X数据手册:介绍芯片的一些基本参数、功能一栏、电气特性、封装参数等。
3、STM32F10X参考手册:此手册包含各个模块的内部结构、所有可能的功能描述、各个工作模式的使用寄存器配置等详细信息,值得注意的是参考手册不包含有关产品的计数特征说明,这些在芯片手册中
4、Cortex-M3权威指南:本书详细概述了ARM Cortex-M3内核的架构和特性。

(5)标准库和HAL库的区别

标准库是直接把寄存器封装好的库,而HAL库可以直接使用ST的CUBEMX软件生成,相比于HAL库可移植性更强。在推行时间上,HAL库是最近才推出的库,STM32官方也主推,而标准库是STM32最早推出的库,应用非常广泛,在市面上的模块都能找得到例程,但是比较新的F7和H7等系列已经不支持了。

如果在时间紧迫的情况下,只需要掌握其中一种库就行了,没必要两种都掌握。

如果以后时间充裕的话,我写一份HAL库的博客…

(6)STM32标准库说明及新建工程模板(略)

这段内容网上有太多的资料,此处直接略过。
关于STM32标准库的说明和新建一个STM32工程模板可以多到B站上看看视频。可以到B站找一下野火的视频,不是打广告,野火也没有给我任何好处,也不是我的什么亲戚,我自己学习STM32的时候也是看野火的视频,讲得还不错。看完视频,然后再找一本参考书《STM32库开发指南(基于STM32F103)》,把视频的知识点和书中的结合,多做笔记,多动手实践。

最后,如果之前学习51单片机安装了keil c51的话,现在安装keil arm是可以安装在一起的,多去网上找找资料,或者安装两个不同的软件,分别安装在不同的目录。

2、GPIO输入输出与定时器延时函数

(1)STM32 GPIO的8种模式

通过《STM32F10X-参考手册中文版》可以得知,每个GPIO管脚都可以由软件设置成输出(推挽或开漏)、输入(带或者不带上下拉)或其他外设功能接口,多数GPIO管脚数字或模拟的外设共用,具体GPIO模式如下8种:

  • 输入浮空
  • 输入上拉
  • 输入下拉
  • 模拟输入
  • 开漏输出
  • 推挽输出
  • 推挽复用功能
  • 开漏复用功能

(2)源码详解

1)首先,我们将系统时钟设置成72MHZ

//将系统时钟配置为72MHZ
void RCC_Configuration(void)
{   
  SystemInit();
}

2)封装GPIO推挽式输出、按键输入、按键检测函数

/**
  ******************************************************************************
  * @file    gpio.c
  * @author  小途
  * @version V1.0
  * @date    2022-2-21
  * @brief   STM32F1的GPIO初始化设置
  ******************************************************************************
  * @attention
  *
  *
  ******************************************************************************
  */

#include "gpio.h"

/***************************************************
 *函数名称:void gpio_OutputConfig(uint32_t RCC_APB2Periph, uint16_t pin, GPIO_TypeDef* GPIOx)
 *函数输入:RCC_APB2Periph:		初始化GPIO总线的外设时钟
 *					pin:								引脚数
 *					GPIOx: 							where x can be (A..G) to select the GPIO peripheral.			
 *函数返回:无
 *函数说明:GPIO推挽式输出初始化
 ***************************************************/
void gpio_OutputConfig(uint32_t RCC_APB2Periph, uint16_t pin, GPIO_TypeDef* GPIOx)
{
	//定义一个GPIO_InitTypeDef类型结构体
	GPIO_InitTypeDef GPIO_InitStructure;
	
	//开启要初始化GPIO总线的外设时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph, ENABLE);
	
	//选择要控制的GPIO引脚
	GPIO_InitStructure.GPIO_Pin = pin;
	
	//设置引脚模式为通用推挽输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	
	//设置引脚速率为50MHZ
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	//初始化GPIO
	GPIO_Init(GPIOx, &GPIO_InitStructure);
}

/**
  * @brief  配置按键用到的I/O口
  * @param  无
  * @retval 无
  */
void Key_GPIO_Config(uint32_t RCC_APB2Periph, uint16_t pin, GPIO_TypeDef* GPIOx)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
	/*开启按键端口时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph, ENABLE);
	
	//选择按键的引脚
	GPIO_InitStructure.GPIO_Pin = pin;
	
	// 设置按键的引脚为浮空输入
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; 
	
	//使用结构体初始化按键
	GPIO_Init(GPIOx, &GPIO_InitStructure);
	
}

 /*
 * 函数名:Key_Scan
 * 描述  :检测是否有按键按下
 * 输入  :GPIOx:x 可以是 A,B,C,D或者 E
 *		     GPIO_Pin:待读取的端口位 	
 * 输出  :KEY_OFF(没按下按键)、KEY_ON(按下按键)
 */
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)
{			
	/*检测是否有按键按下 */
	if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON )  
	{	 
		/*等待按键释放 */
		while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);   
		return 	KEY_ON;	 
	}
	else
		return KEY_OFF;
}


//gpio.h
#ifndef __GPIO_H
#define __GPIO_H

#include "stm32f10x.h"

 /** 按键按下标置宏
	*  按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0
	*  若按键按下为低电平,把宏设置成KEY_ON=0 ,KEY_OFF=1 即可
	*/
#define KEY_ON	1
#define KEY_OFF	0

void gpio_OutputConfig(uint32_t RCC_APB2Periph, uint16_t pin, GPIO_TypeDef* GPIOx);
void Key_GPIO_Config(uint32_t RCC_APB2Periph, uint16_t pin, GPIO_TypeDef* GPIOx);
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin);

#endif

3)配置延时函数定时器
51单片机开发中,常使用软件执行空语句来达到延时一段时间的目的。这种延时会导致CPU资源被占用无法执行其他程序,导致系统效率降低。在STM32开发中,可以使用定时器来达到延时效果。

/**
  ******************************************************************************
  * @file    delay.c
  * @author  小途
  * @version V1.0
  * @date    2022-2-21
  * @brief   STM32F1的延时函数
  ******************************************************************************
  * @attention
  *
	* 使用定时器4(通用定时器),备用2个基本定时器TIM6、TIM7
  *
  ******************************************************************************
  */

#include "delay.h"

//延时函数定时器初始,初始化定时器4
void Delay_Timer_Init(void)
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
  
	TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Down;
	TIM_TimeBaseInitStruct.TIM_Period = 100-1;
	TIM_TimeBaseInitStruct.TIM_Prescaler = (84-1);
	TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStruct);
	
	while((TIM4->SR & TIM_FLAG_Update)!=SET);
	TIM4->SR = (uint16_t)~TIM_FLAG_Update;
}

void Delay_us(uint32_t us_cnt)
{
	TIM4->CNT = us_cnt-1;
	TIM4->CR1 |= TIM_CR1_CEN;    
	while((TIM4->SR & TIM_FLAG_Update)!=SET);
	TIM4->SR = (uint16_t)~TIM_FLAG_Update;
	TIM4->CR1 &= ~TIM_CR1_CEN;
}

void Delay_ms(uint32_t ms_cnt)
{
	int i;
	for(i=0; i<ms_cnt; i++)
	{
		Delay_us(1000);
	}
}
#ifndef __DELAY_H
#define __DELAY_H

#include "stm32f10x.h"


/********************基本定时器TIM参数定义,只限TIM6、7************/
#define BASIC_TIM6 // 如果使用TIM7,注释掉这个宏即可

#ifdef  BASIC_TIM6 // 使用基本定时器TIM6
#define            BASIC_TIM                   TIM6
#define            BASIC_TIM_APBxClock_FUN     RCC_APB1PeriphClockCmd
#define            BASIC_TIM_CLK               RCC_APB1Periph_TIM6
#define            BASIC_TIM_Period            (1000-1)
#define            BASIC_TIM_Prescaler         71
#define            BASIC_TIM_IRQ               TIM6_IRQn
#define            BASIC_TIM_IRQHandler        TIM6_IRQHandler

#else  // 使用基本定时器TIM7
#define            BASIC_TIM                   TIM7
#define            BASIC_TIM_APBxClock_FUN     RCC_APB1PeriphClockCmd
#define            BASIC_TIM_CLK               RCC_APB1Periph_TIM7
#define            BASIC_TIM_Period            1000-1
#define            BASIC_TIM_Prescaler         71
#define            BASIC_TIM_IRQ               TIM7_IRQn
#define            BASIC_TIM_IRQHandler        TIM7_IRQHandler

#endif
/**************************函数声明********************************/
void Delay_Timer_Init(void);
void Delay_us(uint32_t us_cnt);
void Delay_ms(uint32_t ms_cnt);

#endif

4)对GPIO输出端口进行宏定义
在编写程序过程中,还应该考虑硬件修改的情况。例如输出端口改变,这时我们更希望修改一小部分代码就可以在新的硬件上运行。我们可以把硬件的相关部分使用宏来进行封装,若更改了硬件环境,只需要修改相关硬件的宏就可以了。

#define LED5_ON 				GPIO_SetBits(GPIOC, GPIO_Pin_13)
#define LED5_OFF 				GPIO_ResetBits(GPIOC, GPIO_Pin_13)

5)主函数

int main(void)
{
	RCC_Configuration();   				
	Delay_Timer_Init();	
	gpio_OutputConfig(RCC_APB2Periph_GPIOC, GPIO_Pin_13, GPIOC);
	while(1)
	{
		LED5_ON;
		Delay_ms(500);
		LED5_OFF;
		Delay_ms(500);
	}
}

3、串口通信

(1)原理

这个段落主要讲解标准库编程源码,关于串口通信的基础,可以查看这一篇文章
【通信基础】TTL、RS232、RS485

通过查看《STM32F103数据手册中文版》STM32F103C8T6共有3个串口,各个默认串口引脚分布如下:
串口1:TX——PA9,RX——PA10
串口2:TX——PA2,RX——PA3
串口3:TX——PB10,TX——PB11

继续查看发现STM32F103C8T6光串口一就有5个功能管脚,如下图所示:
在这里插入图片描述
那么,USARTx_CK、USARTx_CTS、USARTx_RTS分别是什么呢?
首先,USARTx_CK很好理解,CK表示的是SCLK,中文意思是时钟。该管脚是一个时钟输出引脚,用于同步模式下使用。而关于CTS与RTS接口,我们是否还记得之前学习232通信的DB9接口,如下图所示:
在这里插入图片描述
我们看到DB9接口8和7管脚正好是CTS和RTS接口,这两个管脚的功能也如上图中所示,分别是清除发生和发生数据请求。
我们玩232通信都接触过DB9接口,但是我们使用最多的也就是TX、RX、GND这三个接口。但是,在STM32已经具备硬件流控的功能。这里只做了解,我平时开发的时候很少会用到。

这时我们还是编程第一步,编写usart.c和usart.h文件

(2)源码详解

/**
  ******************************************************************************
  * @file    usart.c
  * @author  小途
  * @version V1.0
  * @date    2022-2-21
  * @brief   STM32F1的串口初始化设置
  ******************************************************************************
  * @attention
  *
  *
  ******************************************************************************
  */

#include "usart.h"


 /**
  * @brief  配置嵌套向量中断控制器NVIC
  * @param  无
  * @retval 无
  */
static void NVIC_Configuration(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
  
  /* 嵌套向量中断控制器组选择 */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  
  /* 配置USART为中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
  /* 抢断优先级*/
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  /* 子优先级 */
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  /* 使能中断 */
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  /* 初始化配置NVIC */
  NVIC_Init(&NVIC_InitStructure);
}

 /**
  * @brief  USART GPIO 配置,工作参数配置
  * @param  无
  * @retval 无
  */
void USART_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;

	// 打开串口GPIO的时钟
	DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
	
	// 打开串口外设的时钟
	DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);

	// 将USART Tx的GPIO配置为推挽复用模式
	GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);

  // 将USART Rx的GPIO配置为浮空输入模式
	GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
	
	// 配置串口的工作参数
	// 配置波特率
	USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
	// 配置 针数据字长
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	// 配置停止位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	// 配置校验位
	USART_InitStructure.USART_Parity = USART_Parity_No ;
	// 配置硬件流控制
	USART_InitStructure.USART_HardwareFlowControl = 
	USART_HardwareFlowControl_None;
	// 配置工作模式,收发一起
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	// 完成串口的初始化配置
	USART_Init(DEBUG_USARTx, &USART_InitStructure);
	
	// 串口中断优先级配置
	NVIC_Configuration();
	
	// 使能串口接收中断
	USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);	
	
	// 使能串口
	USART_Cmd(DEBUG_USARTx, ENABLE);	    
}

 /**
  * @brief  USART2 GPIO 配置,工作参数配置
  * @param  无
  * @retval 无
  */
void USART2_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;

	// 打开串口GPIO的时钟
	DEBUG_USART2_GPIO_APBxClkCmd(DEBUG_USART2_GPIO_CLK, ENABLE);
	
	// 打开串口外设的时钟
	DEBUG_USART2_APBxClkCmd(DEBUG_USART2_CLK, ENABLE);

	// 将USART Tx的GPIO配置为推挽复用模式
	GPIO_InitStructure.GPIO_Pin = DEBUG_USART2_TX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(DEBUG_USART2_TX_GPIO_PORT, &GPIO_InitStructure);

  // 将USART Rx的GPIO配置为浮空输入模式
	GPIO_InitStructure.GPIO_Pin = DEBUG_USART2_RX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(DEBUG_USART2_RX_GPIO_PORT, &GPIO_InitStructure);
	
	// 配置串口的工作参数
	// 配置波特率
	USART_InitStructure.USART_BaudRate = DEBUG_USART2_BAUDRATE;
	// 配置 针数据字长
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	// 配置停止位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	// 配置校验位
	USART_InitStructure.USART_Parity = USART_Parity_No ;
	// 配置硬件流控制
	USART_InitStructure.USART_HardwareFlowControl = 
	USART_HardwareFlowControl_None;
	// 配置工作模式,收发一起
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	// 完成串口的初始化配置
	USART_Init(DEBUG_USART2, &USART_InitStructure);	
	
	// 使能串口
	USART_Cmd(DEBUG_USART2, ENABLE);	    
}

/*****************  发送一个字符 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
	/* 发送一个字节数据到USART */
	USART_SendData(pUSARTx,ch);
		
	/* 等待发送数据寄存器为空 */
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}

/*****************  发送字符串 **********************/
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
	unsigned int k=0;
  do 
  {
      Usart_SendByte( pUSARTx, *(str + k) );
      k++;
  } while(*(str + k)!='\0');
  
  /* 等待发送完成 */
  while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);
}

//重定向c库函数printf到串口,重定向后可使用printf函数,默认使用串口1
int fputc(int ch, FILE *f)
{
		/* 发送一个字节数据到串口 */
		USART_SendData(DEBUG_USART1, (uint8_t) ch);
		
		/* 等待发送完毕 */
		while (USART_GetFlagStatus(DEBUG_USART1, USART_FLAG_TXE) == RESET);		
	
		return (ch);
}

//重定向c库函数scanf到串口,重写向后可使用scanf、getchar等函数,默认使用串口1
int fgetc(FILE *f)
{
		/* 等待串口输入数据 */
		while (USART_GetFlagStatus(DEBUG_USART1, USART_FLAG_RXNE) == RESET);

		return (int)USART_ReceiveData(DEBUG_USART1);
}

#ifndef __USART_H
#define __USART_H

#include "stm32f10x.h"
#include <stdio.h>

/*
 * 串口宏定义,不同的串口挂载的总线不一样,移植时需要修改这几个宏
 */
 
 
 // 串口1-USART1
#define  DEBUG_USARTx                   USART1
#define  DEBUG_USART_CLK                RCC_APB2Periph_USART1
#define  DEBUG_USART_APBxClkCmd         RCC_APB2PeriphClockCmd
#define  DEBUG_USART_BAUDRATE           115200

// USART GPIO 引脚宏定义
#define  DEBUG_USART_GPIO_CLK           (RCC_APB2Periph_GPIOA)
#define  DEBUG_USART_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART_TX_GPIO_PIN        GPIO_Pin_9
#define  DEBUG_USART_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART_RX_GPIO_PIN        GPIO_Pin_10

#define  DEBUG_USART_IRQ                USART1_IRQn
#define  DEBUG_USART_IRQHandler         USART1_IRQHandler
 
 
 
 
 
 //------------------------------------------------------------------------

// 串口1-USART1
#define  DEBUG_USART1                    USART1
#define  DEBUG_USART1_CLK                RCC_APB2Periph_USART1
#define  DEBUG_USART1_APBxClkCmd         RCC_APB2PeriphClockCmd
#define  DEBUG_USART1_BAUDRATE           115200

// USART GPIO 引脚宏定义
#define  DEBUG_USART1_GPIO_CLK           (RCC_APB2Periph_GPIOA)
#define  DEBUG_USART1_GPIO_APBxClkCmd    RCC_APB2PeriphClockCmd
    
#define  DEBUG_USART1_TX_GPIO_PORT       GPIOA   
#define  DEBUG_USART1_TX_GPIO_PIN        GPIO_Pin_9
#define  DEBUG_USART1_RX_GPIO_PORT       GPIOA
#define  DEBUG_USART1_RX_GPIO_PIN        GPIO_Pin_10

#define  DEBUG_USART1_IRQ                USART1_IRQn
#define  DEBUG_USART1_IRQHandler         USART1_IRQHandler

//------------------------------------------------------------------------

// 串口2-USART2
#define  DEBUG_USART2                   	USART2
#define  DEBUG_USART2_CLK                	RCC_APB1Periph_USART2
#define  DEBUG_USART2_APBxClkCmd         	RCC_APB1PeriphClockCmd
#define  DEBUG_USART2_BAUDRATE           	115200

// USART GPIO 引脚宏定义
#define  DEBUG_USART2_GPIO_CLK           	(RCC_APB2Periph_GPIOA)
#define  DEBUG_USART2_GPIO_APBxClkCmd    	RCC_APB2PeriphClockCmd
   
#define  DEBUG_USART2_TX_GPIO_PORT        GPIOA   
#define  DEBUG_USART2_TX_GPIO_PIN         GPIO_Pin_2
#define  DEBUG_USART2_RX_GPIO_PORT       	GPIOA
#define  DEBUG_USART2_RX_GPIO_PIN        	GPIO_Pin_3

#define  DEBUG_USART2_IRQ                	USART2_IRQn
#define  DEBUG_USART2_IRQHandler         	USART2_IRQHandler

void NVIC_Configuration(void);
void USART_Config(void);
//void USART1_Config(void);
void USART2_Config(void);
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch);
void Usart_SendString( USART_TypeDef * pUSARTx, char *str);


#endif


并在stm32f10x_it.c里编写串口中断服务函数

#include "stm32f10x_it.h"
#include "usart.h"
#include "delay.h"

// 串口中断服务函数
void DEBUG_USART_IRQHandler(void)
{
  uint8_t ucTemp;
	if(USART_GetITStatus(DEBUG_USART1,USART_IT_RXNE)!=RESET)
	{		
		ucTemp = USART_ReceiveData(DEBUG_USART1);
    USART_SendData(DEBUG_USART1,ucTemp);
	}	 
}

最后就是主函数调用

int main(void)
{
	RCC_Configuration();   				
	Delay_Timer_Init();
	
	USART_Config();
	while(1)
	{
		Delay_ms(1000);
		printf("HelloWorld!\n");
	}
}

烧录验证效果
在这里插入图片描述

(3)串口发生一段数组例程

在开发过程中,难免会遇到向串口发生一段hex命令,此处给出STM32向串口发生一段数组的例程供参考。

/*****************  发送一个数组 **********************/
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *arr, uint8_t len)
{
	uint8_t k=0;
	for(k=0; k<len; k++)
	{
		Usart_SendByte(pUSARTx, *arr++);
	}
	/* 等待发送完成 */
  while(USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET);
}

4、STM32中断和异常(NVIC)

STM32的中断系统非常强大,与STC89C52单片机对比完全不在一个维度上。STM32每一个外设都可以产生中断。

(1)异常和中断

学过java的同学应该知道,异常是程序中的一些错误(bug),并不是所有的错误都是异常,并且有些错误可以避免,如用户输入非法数据、要打开的文件不存在等。这些异常有的是用户错误引起的,有的是程序错误引起的,也有可能是其他物理错误引起的。在java中,捕获异常使用try和catch关键字捕获异常,try/catch代码块可以放在异常可能发生的地方。语法如下:

try
{
   // 程序代码
}catch(ExceptionName e1)
{
   //Catch 块
}

没有接触过java的只需要记住,异常是程序运行中遇到的bug,这些bug会在程序运行过程中停止,导致这些不正常事件就叫做异常,而这些异常恰恰是可以避免的
在STM32中,异常也是一种中断系统,异常中断体现在STM32的内核上,而外部中断体现在外设上。当发生异常中断情况时,STM32便会捕获异常情况,运行相应的中断处理函数进行异常的处理,也可以用java的try和catch关键字捕获异常来简单理解STM32的异常处理情况。
在异常中断中,使用得比较多的便是复位中断和系统滴答定时器。
F103在内核水平上搭载了一个异常系统,支持众多的系统异常和外部中断,参考《STM32F10x-考考手册中文版》我们可以知道,系统异常有8个(如果把Reset和HardFault也算上的话就是10个)。
STM32的外部中断非常强大,每一个外设都可以配置重点,STM32F10x的外部中断就有60个。除了个别异常的优先级被定死外,其他异常的优先级都可以编辑。具体可以查看“stm32f10x.h”这个头文件查询到,在IRQn_Type这个结构体里面包含了F103系列全部的异常声明。
F103系统的异常如下图所示:
在这里插入图片描述
F103系统外部中断清单:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(2)NVIC简介

提到STM32的中断系统,先需要了解一下NVIC(Nested Vectored Interrupt Controller),它是Cortex-M3内核的一个外设,中文翻译为:嵌套中断向量控制器,控制着整个芯片中断的相关功能,而STM32是基于Cortex-M3架构进行设计的,所以说NVIC是STM32的一个子集。因为是内核的外设,所以NVIC的相关固件库函数被芯片厂商业封装在“core_cm3.h”和“misc.h”这两个文件中。
在“core_cm3.h”文件中,定义了NVIC结构体如下:

/** @addtogroup CMSIS_CM3_NVIC CMSIS CM3 NVIC
  memory mapped structure for Nested Vectored Interrupt Controller (NVIC)
  @{
 */
typedef struct
{
  __IO uint32_t ISER[8];                      /*!< Offset: 0x000  Interrupt Set Enable Register 中断使能寄存器          */
       uint32_t RESERVED0[24];                                   
  __IO uint32_t ICER[8];                      /*!< Offset: 0x080  Interrupt Clear Enable Register 中断清除寄存器        */
       uint32_t RSERVED1[24];                                    
  __IO uint32_t ISPR[8];                      /*!< Offset: 0x100  Interrupt Set Pending Register 中断使能悬起寄存器         */
       uint32_t RESERVED2[24];                                   
  __IO uint32_t ICPR[8];                      /*!< Offset: 0x180  Interrupt Clear Pending Register 中断清除悬起寄存器       */
       uint32_t RESERVED3[24];                                   
  __IO uint32_t IABR[8];                      /*!< Offset: 0x200  Interrupt Active bit Register 中断有效位寄存器          */
       uint32_t RESERVED4[56];                                   
  __IO uint8_t  IP[240];                      /*!< Offset: 0x300  Interrupt Priority Register (8Bit wide) 中断优先级寄存器 */
       uint32_t RESERVED5[644];                                  
  __O  uint32_t STIR;                         /*!< Offset: 0xE00  Software Trigger Interrupt Register 软件触发中断寄存器    */
}  NVIC_Type;                                               
/*@}*/ /* end of group CMSIS_CM3_NVIC */

在实际配置中断过程中,我们一般只用ISER、ICER和IP这三个寄存器
ISER用来使能中断,ICER用来使能中断,IP用来设置中断优先级
文件中,还提供了一些NVIC的函数,这些函数遵循CMSIS规则,只要是Cortex-M3的处理器都可以使用,具体如下:
在这里插入图片描述
但是,这些函数在实际STM32开发编程的时候使用得比较少,或者基本不用。

(3)中断优先级定义

在NVIC有一个专用的中断优先级寄存器NVIC_IPRx,用来配置外部中断的优先级,IPR宽度为8bit,原则上每个外部中断可配置的优先级为0-255,数值越小,优先级越高
但是绝大多数CM3芯片都会精简设计,以致实际上支持的优先级数减少,在F103中,只使用到4bit,如下所示:
在这里插入图片描述
用于表达优先级的这4bit又被分组成抢占优先级和子优先级,如果有多个中断同时响应,抢占优先级高的就会抢占优先级低的执行,如果抢占优先级相同,就比较子优先级。如果抢占优先级和子优先级都相同的话,就比较他们的硬件中断编号,编号越小,优先级越高。

在NVIC应用程序中断级复位控制寄存器(AIRCR)中,10:8位端PRIGROUP为优先级分组,如下图所示:
在这里插入图片描述
而F103分将优先级分组分为了5组,具体如下:
在这里插入图片描述
设置优先级分组在固件库编程时可以直接调用库函数NVIC_PriorityGroupConfig()实现。

/**
  * @brief  Configures the priority grouping: pre-emption priority and subpriority.
  * 		配置中断优先级分组:抢占优先级和子优先级
  * @param  NVIC_PriorityGroup: specifies the priority grouping bits length. 
  * 		NVIC_PriorityGroup: 指定优先级分组位长度
  *   This parameter can be one of the following values:
  *   具体形参如下:
  *     @arg NVIC_PriorityGroup_0: 0 bits for pre-emption priority(抢占优先级)
  *                                4 bits for subpriority(子优先级)
  *     @arg NVIC_PriorityGroup_1: 1 bits for pre-emption priority
  *                                3 bits for subpriority
  *     @arg NVIC_PriorityGroup_2: 2 bits for pre-emption priority
  *                                2 bits for subpriority
  *     @arg NVIC_PriorityGroup_3: 3 bits for pre-emption priority
  *                                1 bits for subpriority
  *     @arg NVIC_PriorityGroup_4: 4 bits for pre-emption priority
  *                                0 bits for subpriority
  * @retval None
  * @注意:如果优先级分组为0,则强占优先级就不存在,优先级就全部由子优先级控制
  */
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
{
  /* Check the parameters */
  assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup));
  
  /* Set the PRIGROUP[10:8] bits according to NVIC_PriorityGroup value */
  //设置优先级分组
  SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup;
}

在这里插入图片描述

(4)中断编程

在配置中断的时候,一般有3个编程要点:

  1. 使能外设某个中断,具体由每个外设的相关中断使能位控制,如串口就有发送完成中断和接收完成中断,这两个中断都由中断控制寄存器的相关中断使能位控制。
  2. 初始化NVIC_InitTypeDef结构体,配置中断优先级分组,设置抢占优先级和子优先级,使能中断请求。NVIC_InitTypeDef结构体在misc.h中定义。
typedef struct
{
  //中断源
  uint8_t NVIC_IRQChannel;                    /*!< Specifies the IRQ channel to be enabled or disabled.
                                                   This parameter can be a value of @ref IRQn_Type 
                                                   (For the complete STM32 Devices IRQ Channels list, please
                                                    refer to stm32f10x.h file) */
                                                    
  //抢占优先级
  uint8_t NVIC_IRQChannelPreemptionPriority;  /*!< Specifies the pre-emption priority for the IRQ channel
                                                   specified in NVIC_IRQChannel. This parameter can be a value
                                                   between 0 and 15 as described in the table @ref NVIC_Priority_Table */
  //子优先级
  uint8_t NVIC_IRQChannelSubPriority;         /*!< Specifies the subpriority level for the IRQ channel specified
                                                   in NVIC_IRQChannel. This parameter can be a value
                                                   between 0 and 15 as described in the table @ref NVIC_Priority_Table */
  //中断使能或者失能
  FunctionalState NVIC_IRQChannelCmd;         /*!< Specifies whether the IRQ channel defined in NVIC_IRQChannel
                                                   will be enabled or disabled. 
                                                   This parameter can be set either to ENABLE or DISABLE */   
} NVIC_InitTypeDef;
/*
NVIC_IROChannel:用来设置中断源,不同的中断中断源不一样,且不可写错,即使写错了程序也不会报错,只会导致不响应中断,程序执行到中断的时候是空的。具体的成员配置可参考 stm32f10x.h 头文件里面的 IRQn_Type结构体定义,这个结构体包含了所有的中断源。
NVIC_IRQChannelPreemptionPriority:抢占优先级,具体的值要根据优先级分组来确定,具体参考表格优先级分组真值表优先级分组真值表。
 NVIC_IRQChannelSubPriority:子优先级,具体的值要根据优先级分组来确定,具体参考表格优先级分组真值表 优先级分组真值表。
NVIC_IRQChannelCmd:中断使能(ENABLE)或者失能(DISABLE)。操作的是 NVIC_ISER和 NVIC_ICER 这两个寄存器。
*/
  1. 编写中断服务函数
    在启动文件 startup_stm32f10x_hd.s 中我们预先为每个中断都写了一个中断服务函数,只是这些中断函数都是为空,为的只是初始化中断向量表。实际的中断服务函数都需要我们重新编写,为了方便管理我们把中断服务函数统一写在 stm32f10x_it.c 这个库文件中。
    关于中断服务函数的函数名必须跟启动文件里面预先设置的一样,如果写错,系统就在中断向量表中找不到中断服务函数的入口,直接跳转到启动文件里面预先写好的空函数,并且在里面无限循环,实现不了中断。
    因为STM32的所有的外设的可以设置中断,所以单独列出来讲解,见如下链接:
    STM32F10x外部中断/事件控制器(EXTI)应用

5、ADC电压采集

STM32拥有3个ADC,精度为12位,每个ADC最多有16个外部通道,ADC的模式非常多,功能也非常强大,ADC功能理论部分可以观看野火的教程视频和书籍,写得非常不错,也很完善,网上的文档也特别多,平时学习多阅读一下参考手册,这里理论部分不再叙述,只提供参考例程。附上STM32F103ZET6的ADC IO口分配
在这里插入图片描述

adc.c

/**
  ******************************************************************************
  * @file    gpio.c
  * @author  小途
  * @version V1.0
  * @date    2022-2-21
  * @brief   STM32F1的adc初始化配置
  ******************************************************************************
  * @attention
  *
  *
  ******************************************************************************
  */

#include "adc.h"

/******************************************************
 *函数名称:void adc_gpioConfig(void)
 *函数输入:无
 *函数返回:无
 *函数说明:ADC输入采集引脚配置
 ******************************************************/
void adc_gpioConfig(void)
{
	GPIO_InitTypeDef gpio_init;
	
	//使能adc1通道1时钟
	RCC_APB2PeriphClockCmd(ADC_GPIO_CLK, ENABLE);
	
	//PA1作为模拟通道引脚
	gpio_init.GPIO_Pin = ADC_PIN;
	gpio_init.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
	GPIO_Init(ADC_PORT, &gpio_init);
}

/************************************************************
 *函数名称:void adc_Config(void)
 *函数输入:无
 *函数返回:无
 *函数说明:ADC输入采集功能配置
 */
void adc_Config(void)
{
	ADC_InitTypeDef ADC_InitStructure;
	
	//使能ADC1时钟
	RCC_APB2PeriphClockCmd(ADC_CLK, ENABLE);
	// 配置ADC时钟为PCLK2的6分频,即12MHz
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	//复位ADCx,将ADCx的全部寄存器设为缺省值
	ADC_DeInit(ADCx);
	
	// ADC 模式配置
	// 只使用一个ADC,属于独立模式
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	// 禁止扫描模式,多通道才要,单通道不需要
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;
	//转换工作单次转换模式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	// 不用外部触发转换,软件开启即可
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	// 转换结果右对齐
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	// 转换通道1个
	ADC_InitStructure.ADC_NbrOfChannel = 1;
	//初始化ADC
	ADC_Init(ADCx, &ADC_InitStructure);
	//使能指定ADC
	ADC_Cmd(ADCx, ENABLE);
	//使能复位校验
	ADC_ResetCalibration(ADCx);
	//等待复位校验结束
	while(ADC_GetResetCalibrationStatus(ADCx));
	
	ADC_StartCalibration(ADCx);//开启AD校验
	while(ADC_GetCalibrationStatus(ADCx));//等待校验结果	
}

/********************************************************
 *函数名称:void adc_init(void)
 *函数输入:无
 *函数返回:无
 *函数说明:ADC初始化
 ********************************************************/
void adc_init(void)
{
	adc_gpioConfig();	//IO初始化
	adc_Config();			//ADC功能配置
}

/*************************************************
 *函数名称:uint16_t Get_adcConvertVal(uint8_t ch)
 *函数输入:通道数
 *函数返回:单次ADC值
 *函数说明:输出ADC转换结果
 *************************************************/
uint16_t Get_adcConvertVal(uint8_t ch)
{
	//设置指定ADC的规则组,一个序列,采样时间
	ADC_RegularChannelConfig(ADCx, ch, 1, ADC_SampleTime_239Cycles5);
	
	//使能ADC软件转换启动功能
	ADC_SoftwareStartConvCmd(ADCx, ENABLE);
	
	//等待转换结果
	while(!ADC_GetFlagStatus(ADCx, ADC_FLAG_EOC));
	
	//返回一次转换结果
	return ADC_GetConversionValue(ADCx);
}

adc.h

#ifndef __ADC_H
#define __ADC_H

#include "stm32f10x.h"

/********************ADC1输入通道(引脚)配置**************************/
//ADC部分
#define    ADCx                          ADC1
#define    ADC_APBxClock_FUN             RCC_APB2PeriphClockCmd
#define    ADC_CLK                       RCC_APB2Periph_ADC1

//GPIO部分
#define    ADC_GPIO_APBxClock_FUN        RCC_APB2PeriphClockCmd
#define    ADC_GPIO_CLK                  RCC_APB2Periph_GPIOA  
#define    ADC_PORT                      GPIOA
#define    ADC_PIN                       GPIO_Pin_1

void adc_init(void);
uint16_t Get_adcConvertVal(uint8_t ch);


#endif

6、IIC通信

学习51单片机的时候,使用IIC与AT2400进行通信是使用模拟的IIC,到STM32的时候已经有了硬件的IIC。相比于51单片机,STM32的硬件IIC只要配置好该外设,就能自动的根据协议产生通讯信号,收发数据。
但是,有业界人士说,STM32硬件的IIC不是特别好,传输时候有时会出现错误,也有人说,硬件的IIC通信可以设置其通信速度,可以配置更加高速的通信。由于我个人的水平有限,也无法接触到这些。
不过,现市场上IIC通信的芯片放出外面的例程常用模拟的IIC,模拟IIC方便移植,且支持芯片范围广,遇到新的MCU,只需将原来写好的程序移植过去即可,有时候因为时钟不同,需要调整一个时许。
总之,对于初学者来说,如果在时间充裕的情况下,模拟的和硬件的IIC都需要掌握。如果在时间特别紧急的情况,掌握模拟的IIC通信也可以。
IIC的通信原理以在我写51单片机的时候叙述过,原理方面可以查看51单片机的文章,本文章不再叙述。
STC12C5A60S2使用模拟IIC通信与AT24C02芯片编程

要想完成一个IIC的通信,主要掌握它大概的一个通信逻辑顺序:
1、主机发送起始信号启动总线
2、主机发送一个字节数据指明指明从机地址和后续传送方向
3、被寻址的从机发送应答信号
4、主机发送一个字节数据
5、接收到一个发送应答信号
。。。(重复4-5)
n、当通信完成后,发送停止信号释放总线

本文章提供IIC的EEROM的例程供大家参考

(1)模拟IIC

eerom.c

/*
基于EEROM(AT2400)应用函数


小明
2023.9.5
*/



#include "eerom.h"

/*
	应用说明:
	在访问I2C设备前,请先调用 i2c_CheckDevice() 检测I2C设备是否正常,该函数会配置GPIO

*/

static void ee_i2c_CfgGpio(void);

/*
*********************************************************************************************************
*	函 数 名: ee_i2c_Delay
*	功能说明: I2C总线位延迟,最快400KHz
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
static void ee_i2c_Delay(void)
{
	uint8_t i;

	/* 
	 	下面的时间是通过逻辑分析仪测试得到的。
    工作条件:CPU主频72MHz ,MDK编译环境,1级优化
  
		循环次数为10时,SCL频率 = 205KHz 
		循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us 
	 	循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us 
	*/
	for (i = 0; i < 10; i++);
}

/*
*********************************************************************************************************
*	函 数 名: i2c_Start
*	功能说明: CPU发起I2C总线启动信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void ee_i2c_Start(void)
{
	/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
	EEPROM_I2C_SDA_1();
	EEPROM_I2C_SCL_1();
	ee_i2c_Delay();
	EEPROM_I2C_SDA_0();
	ee_i2c_Delay();
	EEPROM_I2C_SCL_0();
	ee_i2c_Delay();
}

/*
*********************************************************************************************************
*	函 数 名: i2c_Start
*	功能说明: CPU发起I2C总线停止信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void ee_i2c_Stop(void)
{
	/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
	EEPROM_I2C_SDA_0();
	EEPROM_I2C_SCL_1();
	ee_i2c_Delay();
	EEPROM_I2C_SDA_1();
}

/*
*********************************************************************************************************
*	函 数 名: i2c_SendByte
*	功能说明: CPU向I2C总线设备发送8bit数据
*	形    参:_ucByte : 等待发送的字节
*	返 回 值: 无
*********************************************************************************************************
*/
void ee_i2c_SendByte(uint8_t _ucByte)
{
	uint8_t i;

	/* 先发送字节的高位bit7 */
	for (i = 0; i < 8; i++)
	{		
		if (_ucByte & 0x80)
		{
			EEPROM_I2C_SDA_1();
		}
		else
		{
			EEPROM_I2C_SDA_0();
		}
		ee_i2c_Delay();
		EEPROM_I2C_SCL_1();
		ee_i2c_Delay();	
		EEPROM_I2C_SCL_0();
		if (i == 7)
		{
			 EEPROM_I2C_SDA_1(); // 释放总线
		}
		_ucByte <<= 1;	/* 左移一个bit */
		ee_i2c_Delay();
	}
}

/*
*********************************************************************************************************
*	函 数 名: i2c_ReadByte
*	功能说明: CPU从I2C总线设备读取8bit数据
*	形    参:无
*	返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t ee_i2c_ReadByte(void)
{
	uint8_t i;
	uint8_t value;

	/* 读到第1个bit为数据的bit7 */
	value = 0;
	for (i = 0; i < 8; i++)
	{
		value <<= 1;
		EEPROM_I2C_SCL_1();
		ee_i2c_Delay();
		if (EEPROM_I2C_SDA_READ())
		{
			value++;
		}
		EEPROM_I2C_SCL_0();
		ee_i2c_Delay();
	}
	return value;
}

/*
*********************************************************************************************************
*	函 数 名: i2c_WaitAck
*	功能说明: CPU产生一个时钟,并读取器件的ACK应答信号
*	形    参:无
*	返 回 值: 返回0表示正确应答,1表示无器件响应
*********************************************************************************************************
*/
uint8_t ee_i2c_WaitAck(void)
{
	uint8_t re;

	EEPROM_I2C_SDA_1();	/* CPU释放SDA总线 */
	ee_i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
	ee_i2c_Delay();
	if (EEPROM_I2C_SDA_READ())	/* CPU读取SDA口线状态 */
	{
		re = 1;
	}
	else
	{
		re = 0;
	}
	EEPROM_I2C_SCL_0();
	ee_i2c_Delay();
	return re;
}

/*
*********************************************************************************************************
*	函 数 名: i2c_Ack
*	功能说明: CPU产生一个ACK信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void ee_i2c_Ack(void)
{
	EEPROM_I2C_SDA_0();	/* CPU驱动SDA = 0 */
	ee_i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU产生1个时钟 */
	ee_i2c_Delay();
	EEPROM_I2C_SCL_0();
	ee_i2c_Delay();
	EEPROM_I2C_SDA_1();	/* CPU释放SDA总线 */
}

/*
*********************************************************************************************************
*	函 数 名: i2c_NAck
*	功能说明: CPU产生1个NACK信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void ee_i2c_NAck(void)
{
	EEPROM_I2C_SDA_1();	/* CPU驱动SDA = 1 */
	ee_i2c_Delay();
	EEPROM_I2C_SCL_1();	/* CPU产生1个时钟 */
	ee_i2c_Delay();
	EEPROM_I2C_SCL_0();
	ee_i2c_Delay();	
}

/*
*********************************************************************************************************
*	函 数 名: i2c_CfgGpio
*	功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
static void ee_i2c_CfgGpio(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE);	/* 打开GPIO时钟 */

	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;  	/* 开漏输出 */
	GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);

	/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
	ee_i2c_Stop();
}

/*
*********************************************************************************************************
*	函 数 名: i2c_CheckDevice
*	功能说明: 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
*	形    参:_Address:设备的I2C总线地址
*	返 回 值: 返回值 0 表示正确, 返回1表示未探测到
*********************************************************************************************************
*/
uint8_t ee_i2c_CheckDevice(uint8_t _Address)
{
	uint8_t ucAck;

	ee_i2c_CfgGpio();		/* 配置GPIO */

	
	ee_i2c_Start();		/* 发送启动信号 */

	/* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
	ee_i2c_SendByte(_Address | EEPROM_I2C_WR);
	ucAck = ee_i2c_WaitAck();	/* 检测设备的ACK应答 */

	ee_i2c_Stop();			/* 发送停止信号 */

	return ucAck;
}


//向EEROM写数据
//ADDR——EEROM的地址
//DAT——要写入数据
//返回0时,写入成功,返回其他时,写入失败
uint8_t ee_WriteBytes(uint8_t ADDR, uint8_t DAT)
{
	//1、发送启始信号
	ee_i2c_Start();
	
	//2、发送控制信号(地址+控制命令 0写/1读)
	ee_i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);
	
	//3、等待ACK
	if (ee_i2c_WaitAck() != 0)
	{
		return 1;
	}
	
	//4、发送写入字节地址
	ee_i2c_SendByte(ADDR);
	
	//5、等待ACK
	if (ee_i2c_WaitAck() != 0)
	{
		return 2;
	}
	
	//6、开始写入数据
	ee_i2c_SendByte(DAT);
	
	//7、发送ACK 
	if (ee_i2c_WaitAck() != 0)
	{
		return 3;
	}
	
	//命令执行成功,发送I2C总线停止信号
	ee_i2c_Stop();
	return 0;
}

//读EEROM内存中的数据
//ADDR——需要读取内存中的数据地址
//0 表示读取失败 其他则表示读取的数据
uint8_t ee_ReadBytes(uint8_t ADDR)
{
	uint8_t DAT;
	
	//1、发起I2C总线启动信号
	ee_i2c_Start();
	
	//2、发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
	ee_i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);	/* 此处是写指令 */
	
	//3、第3步:等待ACK
	if (ee_i2c_WaitAck() != 0)
	{
		return 0;
	}
	
	//4、发送字节地址
	ee_i2c_SendByte(ADDR);
	
	//5、等待ACK
	if (ee_i2c_WaitAck() != 0)
	{
		return 0;
	}
	
	//6、重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据
	ee_i2c_Start();
	
	//7、发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
	ee_i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_RD);	/* 此处是读指令 */
	
	//8、发送ACK
	if (ee_i2c_WaitAck() != 0)
	{
		return 0;
	}
	
	//9、读取数据
	DAT = ee_i2c_ReadByte();//读1个字节
	
	ee_i2c_NAck();//产生一个NACK信号,SDA=1
	ee_i2c_Stop();//发送IIC总线停止信号
	return DAT;
}

eerom.h

#ifndef _EEROM_H
#define _EEROM_H

#include "stm32f10x.h"

/* 
 * AT24C02 2kb = 2048bit = 2048/8 B = 256 B
 * 32 pages of 8 bytes each
 *
 * Device Address
 * 1 0 1 0 A2 A1 A0 R/W
 * 1 0 1 0 0  0  0  0 = 0XA0
 * 1 0 1 0 0  0  0  1 = 0XA1 
 */
 
 /* AT24C01/02每页有8个字节 
 * AT24C04/08A/16A每页有16个字节 
 */
 
#define EEPROM_I2C_WR	0		/* 写控制bit */
#define EEPROM_I2C_RD	1		/* 读控制bit */

#define EEPROM_DEV_ADDR			0xA0		/* 24xx02的设备地址 */
#define EEPROM_PAGE_SIZE		  8			  /* 24xx02的页面大小 */
#define EEPROM_SIZE				  256			  /* 24xx02总容量 */

/* 定义I2C总线连接的GPIO端口, 用户只需要修改下面4行代码即可任意改变SCL和SDA的引脚 */
#define EEPROM_GPIO_PORT_I2C	GPIOB			/* GPIO端口 */
#define EEPROM_RCC_I2C_PORT 	RCC_APB2Periph_GPIOB		/* GPIO端口时钟 */
#define EEPROM_I2C_SCL_PIN		GPIO_Pin_6			/* 连接到SCL时钟线的GPIO */
#define EEPROM_I2C_SDA_PIN		GPIO_Pin_7			/* 连接到SDA数据线的GPIO */

/* 定义读写SCL和SDA的宏,已增加代码的可移植性和可阅读性 */
#if 0	/* 条件编译: 1 选择GPIO的库函数实现IO读写 */
	#define EEPROM_I2C_SCL_1()  GPIO_SetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SCL_PIN)		/* SCL = 1 */
	#define EEPROM_I2C_SCL_0()  GPIO_ResetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SCL_PIN)		/* SCL = 0 */
	
	#define EEPROM_I2C_SDA_1()  GPIO_SetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN)		/* SDA = 1 */
	#define EEPROM_I2C_SDA_0()  GPIO_ResetBits(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN)		/* SDA = 0 */
	
	#define EEPROM_I2C_SDA_READ()  GPIO_ReadInputDataBit(EEPROM_GPIO_PORT_I2C, EEPROM_I2C_SDA_PIN)	/* 读SDA口线状态 */
#else	/* 这个分支选择直接寄存器操作实现IO读写 */
    /* 注意:如下写法,在IAR最高级别优化时,会被编译器错误优化 */
	#define EEPROM_I2C_SCL_1()  EEPROM_GPIO_PORT_I2C->BSRR = EEPROM_I2C_SCL_PIN				/* SCL = 1 */
	#define EEPROM_I2C_SCL_0()  EEPROM_GPIO_PORT_I2C->BRR = EEPROM_I2C_SCL_PIN				/* SCL = 0 */
	
	#define EEPROM_I2C_SDA_1()  EEPROM_GPIO_PORT_I2C->BSRR = EEPROM_I2C_SDA_PIN				/* SDA = 1 */
	#define EEPROM_I2C_SDA_0()  EEPROM_GPIO_PORT_I2C->BRR = EEPROM_I2C_SDA_PIN				/* SDA = 0 */
	
	#define EEPROM_I2C_SDA_READ()  ((EEPROM_GPIO_PORT_I2C->IDR & EEPROM_I2C_SDA_PIN) != 0)	/* 读SDA口线状态 */
#endif


uint8_t ee_i2c_CheckDevice(uint8_t _Address);
uint8_t ee_WriteBytes(uint8_t ADDR, uint8_t DAT);
uint8_t ee_ReadBytes(uint8_t ADDR);


#endif

main.c

		USART_Config();//串口初始化
		printf("这是一个EEROM的测试例程\n");
		if(ee_i2c_CheckDevice(EEPROM_DEV_ADDR) == 0)
			printf("成功检测到EEROM\n");
		else
			printf("未检测到EEROM\n");
		if(ee_WriteBytes(4,'1') == 0)
			printf("数据写入成功\n");
		else
			printf("数据写入失败\n");
		Delay_ms(500);//等待一段时间再读取数据
		if(ee_ReadBytes(4) == 0)
			printf("读取数据失败\n");
		else
		{
			printf("读取数据成功,数据是:");
			Usart_SendByte(USART1, ee_ReadBytes(4));
		}

(2)硬件IIC

bsp_i2c_ee.c

#include "bsp_i2c_ee.h"

uint16_t EEPROM_ADDRESS;

/**
  * @brief  I2C I/O配置
  * @param  无
  * @retval 无
  */
static void I2C_GPIO_Config(void)
{
  GPIO_InitTypeDef  GPIO_InitStructure; 

	/* 使能与 I2C 有关的时钟 */
	EEPROM_I2C_APBxClock_FUN ( EEPROM_I2C_CLK, ENABLE );
	EEPROM_I2C_GPIO_APBxClock_FUN ( EEPROM_I2C_GPIO_CLK, ENABLE );
	
    
  /* I2C_SCL、I2C_SDA*/
  GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	       // 开漏输出
  GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
	
  GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;	       // 开漏输出
  GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);	
	
}

/**
  * @brief  I2C 工作模式配置
  * @param  无
  * @retval 无
  */
static void I2C_Mode_Configu(void)
{
  I2C_InitTypeDef  I2C_InitStructure; 

  /* I2C 配置 */
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	
	/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	
  I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7; 
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
	 
	/* I2C的寻址模式 */
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	
	/* 通信速率 */
  I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
  
	/* I2C 初始化 */
  I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
  
	/* 使能 I2C */
  I2C_Cmd(EEPROM_I2Cx, ENABLE);   
}

/**
  * @brief  I2C 外设(EEPROM)初始化
  * @param  无
  * @retval 无
  */
void I2C_EE_Init(void)
{

  I2C_GPIO_Config(); 
 
  I2C_Mode_Configu();

/* 根据头文件i2c_ee.h中的定义来选择EEPROM的设备地址 */
#ifdef EEPROM_Block0_ADDRESS
  /* 选择 EEPROM Block0 来写入 */
  EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
#endif

#ifdef EEPROM_Block1_ADDRESS  
	/* 选择 EEPROM Block1 来写入 */
  EEPROM_ADDRESS = EEPROM_Block1_ADDRESS;
#endif

#ifdef EEPROM_Block2_ADDRESS  
	/* 选择 EEPROM Block2 来写入 */
  EEPROM_ADDRESS = EEPROM_Block2_ADDRESS;
#endif

#ifdef EEPROM_Block3_ADDRESS  
	/* 选择 EEPROM Block3 来写入 */
  EEPROM_ADDRESS = EEPROM_Block3_ADDRESS;
#endif
}

//写数据
//address-写入数据地址
//data-需要写入的数据
void I2C_WriteByte(uint16_t address, uint8_t data)
{
  // 等待I2C空闲
  while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
    ;

  // 发送START信号
  I2C_GenerateSTART(I2C1, ENABLE);

  // 等待START信号被发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    ;

  // 发送EEPROM地址
  I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

  // 等待地址发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
    ;

  // 发送数据
  I2C_SendData(I2C1, (uint8_t)(address >> 8)); //高8位地址
  //等待数据发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    ;

  I2C_SendData(I2C1, (uint8_t)(address & 0xFF)); //低8位地址
  // 等待数据发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    ;

  // 发送数据
  I2C_SendData(I2C1, data);
  //等待数据发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    ;

  // 发送STOP信号
  I2C_GenerateSTOP(I2C1, ENABLE);
}


//读数据
//address-需要读取的数据地址
uint8_t I2C_ReadByte(uint16_t address)
{
  uint8_t data;

  // 等待I2C空闲
  while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
    ;

  // 发送START信号
  I2C_GenerateSTART(I2C1, ENABLE);

  // 等待START信号被发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    ;

  // 发送EEPROM地址
  I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

  // 等待地址发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
    ;

  // 发送数据
  I2C_SendData(I2C1, (uint8_t)(address >> 8)); //高8位地址
  // 等待数据发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    ;

  I2C_SendData(I2C1, (uint8_t)(address & 0xFF)); //低8位地址
  // 等待数据发送完成
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    ;

  // 重新启动,读模式
  I2C_GenerateSTART(I2C1, ENABLE);
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
    ;

  // 发送EEPROM地址
  I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
    ;

  // 开始接收数据
  I2C_AcknowledgeConfig(I2C1, DISABLE);
  I2C_GenerateSTOP(I2C1, ENABLE);
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
    ;
  data = I2C_ReceiveData(I2C1);

  // 返回数据
  return data;
}

bsp_i2c_ee.h

#ifndef __I2C_EE_H
#define	__I2C_EE_H


#include "stm32f10x.h"

#include "stm32f10x.h"


/**************************I2C参数定义,I2C1或I2C2********************************/
#define             EEPROM_I2Cx                                I2C1
#define             EEPROM_I2C_APBxClock_FUN                   RCC_APB1PeriphClockCmd
#define             EEPROM_I2C_CLK                             RCC_APB1Periph_I2C1
#define             EEPROM_I2C_GPIO_APBxClock_FUN              RCC_APB2PeriphClockCmd
#define             EEPROM_I2C_GPIO_CLK                        RCC_APB2Periph_GPIOB     
#define             EEPROM_I2C_SCL_PORT                        GPIOB   
#define             EEPROM_I2C_SCL_PIN                         GPIO_Pin_6
#define             EEPROM_I2C_SDA_PORT                        GPIOB 
#define             EEPROM_I2C_SDA_PIN                         GPIO_Pin_7

/* STM32 I2C 快速模式 */
#define I2C_Speed              400000  //*

/* 这个地址只要与STM32外挂的I2C器件地址不一样即可 */
#define I2Cx_OWN_ADDRESS7      0X0A   

/* AT24C01/02每页有8个字节 */
#define I2C_PageSize           8

/* AT24C04/08A/16A每页有16个字节 */
//#define I2C_PageSize           16	

/* 
 * AT24C02 2kb = 2048bit = 2048/8 B = 256 B
 * 32 pages of 8 bytes each
 *
 * Device Address
 * 1 0 1 0 A2 A1 A0 R/W
 * 1 0 1 0 0  0  0  0 = 0XA0
 * 1 0 1 0 0  0  0  1 = 0XA1 
 */

/* EEPROM Addresses defines */
#define EEPROM_Block0_ADDRESS 0xA0   /* E2 = 0 */
//#define EEPROM_Block1_ADDRESS 0xA2 /* E2 = 0 */
//#define EEPROM_Block2_ADDRESS 0xA4 /* E2 = 0 */
//#define EEPROM_Block3_ADDRESS 0xA6 /* E2 = 0 */


void I2C_EE_Init(void);
void I2C_WriteByte(uint16_t address, uint8_t data);
uint8_t I2C_ReadByte(uint16_t address);

#endif /* __I2C_EE_H */

main.c

		//硬件IIC读取AT2400
		I2C_EE_Init();//硬件IIC初始化
		USART_Config();//串口初始化
		I2C_WriteByte(0x04,'2');//写入数据
		printf("读取数据成功,数据是:");
		Usart_SendByte(USART1, I2C_ReadByte(0x04));

7、CAN总线

(1)CAN总线概述

CAN总线是Controller Area Network-控制器局域网络的英文缩写,是一种异步半双工的通信,由CAN_High和CAN_Low两条信号线,以差分形式进行通信。
CAN最早是由德国BOSCH公司开发,现已是国际上最广泛的总线之一,尤其在汽车电子和嵌入式工业控制中广泛应用。
如下图是新能源汽车充电桩快充的接头,它便使用CAN总线实现充电桩与汽车充电管理系统的快速通信。
在这里插入图片描述
这张图的各个功能如下:
DC+:直流电源正
DC-:直流电源负
PE:接地(搭铁)
S+:通讯(CAN-H)
S-:通讯(CAN-L)

CC1:充电连接确认
CC2:充电连接确认
A+:12V+
A-:12V-

(2)网络连接方式

CAN总线可以挂载多个通信节点,节点之间经过总线进行传输,总线相当于是一跳高速公路,由于CAN通信不对节点进行寻址,而对数据内容进行编码,所以理论上,只要总线负载足够,CAN总线的节点不受限制,可以与无穷多个节点进行通信,我们可以通过增加中继器的方式来增加CAN总线的总线负载。

CAN总线的网络连接方式有:闭环总线通信网络连接、开环总线通信网络连接

闭环通信总线网络遵循ISO11898标准,具有高速、短距离的特点,最大允许总线长度为40米,通信速率高,可达1Mbps,总线要求各接120Ω的电阻。
在这里插入图片描述

开环总线网络遵循ISO11519-2标准,具有低速、远距离的特点,它最大的传输举例可达1km,但是它的通信速率最高只能达到125Kbps。开环总线网络结构的两根总线是独立的,不形成闭环,因此每根总线需要串联2.2KΩ的电阻。
在这里插入图片描述

(3)CAN协议中的差分信号

在这里插入图片描述

(4)CAN通信的波特率与位同步

由于CAN是异步通信,与串口类似会同时约定好一个波特率进行通信
同时,CAN还会使用“位同步”的方式增强抗干扰能力,保证通信的稳定
为了实现位同步,CAN协议把每一位数据分解为【SS段、PTS段、PB1段、PB2段】。将这四段的长度加起来即为一个CAN数据的长度,分解后最小的时间单位是Tq,而一个完整的位通常由8-25个Tq组成。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然而,波特率只是约定了每个数据位的长度,数据同步还需要涉及到相位的细节,这时就需要涉及到SS、PTS、PBS1和PBS2了。
CAN总线的数据同步有:硬同步和重新同步
硬同步是以起始信号开始同步,当数据发生偏移的时候,不能保证数据的准确性,重新同步就是为了解决硬同步存在的问题。
重新同步在硬同步的基础上进行了调整,重新获得同步,就能采集到正确的数据了。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(5)CAN的报文种类及结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(6)STM32 CAN总线通信模式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(7)STM32 CAN总线通信需要初始化的结构体

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这三个结构体的详细说明可以查看STM32库函数手册

(8)STM32 CAN总线通信编程思路

CAN总线编程调试常使用回环模式进行测试,当在回环模式测试没有问题后,便调整会正常模式进行通信测试。
编程思路如下:

  1. 初始化 CAN 通讯使用的目标引脚及端口时钟;
  2. 使能 CAN 外设的时钟;
  3. 配置 CAN 外设的工作模式、位时序以及波特率;
  4. 配置筛选器的工作方式;
  5. 编写测试程序,收发报文并校验。

(9)代码例程

例程使使用can回环模式,不断向can发送0-7的十六进制数据,因为是回环模式,发送出去的数据很快便会原路返回到can的接收端,这时我们从接收端提取出数据,然后将数据转换成我们想要的类型…
can.c

/**
  ******************************************************************************
  * @file    can.c
  * @author  fire
  * @version V1.0
  * @date    2023-12-30
  * @brief   can驱动(回环模式)
	*          实际开发过程中,在回环模式测试没有问题的情况下
	*					 便可以进行CAN的通信,调整一下模式即可
  ******************************************************************************
**/

#include "can.h"

/*
 * 函数名:CAN_GPIO_Config
 * 描述  :CAN的GPIO 配置
 * 输入  :无
 * 输出  : 无
 * 调用  :内部调用
 */
static void CAN_GPIO_Config(void)
{
 	GPIO_InitTypeDef GPIO_InitStructure;   	

  /* Enable GPIO clock */
  RCC_APB2PeriphClockCmd(CAN_TX_GPIO_CLK|CAN_RX_GPIO_CLK, ENABLE);
	
	//重映射引脚
	GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE);

	 /* Configure CAN TX pins */
  GPIO_InitStructure.GPIO_Pin = CAN_TX_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		         // 复用推挽输出
  GPIO_Init(CAN_TX_GPIO_PORT, &GPIO_InitStructure);
	
	/* Configure CAN RX  pins */
  GPIO_InitStructure.GPIO_Pin = CAN_RX_PIN ;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	             // 上拉输入
  GPIO_Init(CAN_RX_GPIO_PORT, &GPIO_InitStructure);

	
}

/*
 * 函数名:CAN_NVIC_Config
 * 描述  :CAN的NVIC 配置,第1优先级组,0,0优先级
 * 输入  :无
 * 输出  : 无
 * 调用  :内部调用
 */
static void CAN_NVIC_Config(void)
{
   	NVIC_InitTypeDef NVIC_InitStructure;
		/* Configure one bit for preemption priority */
		NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	 	/*中断设置*/
		NVIC_InitStructure.NVIC_IRQChannel = CAN_RX_IRQ;	   				//CAN1 RX0中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;		//抢占优先级0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;			   	//子优先级为0
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

/*
 * 函数名:CAN_Mode_Config
 * 描述  :CAN的模式 配置
 * 输入  :无
 * 输出  : 无
 * 调用  :内部调用
 */
static void CAN_Mode_Config(void)
{
	CAN_InitTypeDef        CAN_InitStructure;
	/************************CAN通信参数设置**********************************/
	/* Enable CAN clock */
  RCC_APB1PeriphClockCmd(CAN_CLK, ENABLE);

	/*CAN寄存器初始化*/
	CAN_DeInit(CANx);
	CAN_StructInit(&CAN_InitStructure);

	/*CAN单元初始化*/
	CAN_InitStructure.CAN_TTCM=DISABLE;			   //MCR-TTCM  关闭时间触发通信模式使能
	CAN_InitStructure.CAN_ABOM=ENABLE;			   //MCR-ABOM  自动离线管理 
	CAN_InitStructure.CAN_AWUM=ENABLE;			   //MCR-AWUM  使用自动唤醒模式
	CAN_InitStructure.CAN_NART=DISABLE;			   //MCR-NART  禁止报文自动重传	  DISABLE-自动重传
	CAN_InitStructure.CAN_RFLM=DISABLE;			   //MCR-RFLM  接收FIFO 锁定模式  DISABLE-溢出时新报文会覆盖原有报文  
	CAN_InitStructure.CAN_TXFP=DISABLE;			   //MCR-TXFP  发送FIFO优先级 DISABLE-优先级取决于报文标示符 
	CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;  //回环工作模式
	CAN_InitStructure.CAN_SJW=CAN_SJW_1tq;		   //BTR-SJW 重新同步跳跃宽度 1个时间单元
	 
	/* ss=1 bs1=5 bs2=3 位时间宽度为(1+5+3) 波特率即为时钟周期tq*(1+3+5)  */
	CAN_InitStructure.CAN_BS1=CAN_BS1_5tq;		   //BTR-TS1 时间段1 占用了5个时间单元
	CAN_InitStructure.CAN_BS2=CAN_BS2_3tq;		   //BTR-TS1 时间段2 占用了3个时间单元	
	
	/* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB1 = 36 MHz) */
	CAN_InitStructure.CAN_Prescaler =4;		   BTR-BRP 波特率分频器  定义了时间单元的时间长度 36/(1+5+3)/4=1 Mbps
	CAN_Init(CANx, &CAN_InitStructure);
}

/*
 * 函数名:CAN_Filter_Config
 * 描述  :CAN的过滤器 配置
 * 输入  :无
 * 输出  : 无
 * 调用  :内部调用
 */
static void CAN_Filter_Config(void)
{
	CAN_FilterInitTypeDef  CAN_FilterInitStructure;

	/*CAN筛选器初始化*/
	CAN_FilterInitStructure.CAN_FilterNumber=0;						//筛选器组0
	CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;	//工作在掩码模式
	CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;	//筛选器位宽为单个32位。
	/* 使能筛选器,按照标志的内容进行比对筛选,扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */

	CAN_FilterInitStructure.CAN_FilterIdHigh= ((((u32)0x1314<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xFFFF0000)>>16;		//要筛选的ID高位 
	CAN_FilterInitStructure.CAN_FilterIdLow= (((u32)0x1314<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xFFFF; //要筛选的ID低位 
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0xFFFF;			//筛选器高16位每位必须匹配
	CAN_FilterInitStructure.CAN_FilterMaskIdLow= 0xFFFF;			//筛选器低16位每位必须匹配
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ;				//筛选器被关联到FIFO0
	CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;			//使能筛选器
	CAN_FilterInit(&CAN_FilterInitStructure);
	/*CAN通信中断使能*/
	CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE);
}

/*
 * 函数名:CAN_Config
 * 描述  :完整配置CAN的功能
 * 输入  :无
 * 输出  : 无
 * 调用  :外部调用
 */
void CAN_Config(void)
{
  CAN_GPIO_Config();
  CAN_NVIC_Config();
  CAN_Mode_Config();
  CAN_Filter_Config();
}


can.h

#ifndef __CAN_H
#define	__CAN_H

#include "stm32f10x.h"


#define CANx                       	CAN1
#define CAN_CLK                    	RCC_APB1Periph_CAN1
#define CAN_RX_IRQ									USB_LP_CAN1_RX0_IRQn				//USB低优先级或者CAN接收中断0
#define CAN_RX_IRQHandler						USB_LP_CAN1_RX0_IRQHandler

#define CAN_RX_PIN                 	GPIO_Pin_8
#define CAN_TX_PIN                 	GPIO_Pin_9
#define CAN_TX_GPIO_PORT          	GPIOB
#define CAN_RX_GPIO_PORT          	GPIOB
#define CAN_TX_GPIO_CLK           	(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB)
#define CAN_RX_GPIO_CLK           	RCC_APB2Periph_GPIOB

extern void CAN_Config(void);

#endif


stm32f10x_it.c

extern volatile uint8_t can_flag;
extern CanRxMsg RxMessage;//接收缓冲区
/*
 * 函数名:USB_LP_CAN1_RX0_IRQHandler
 * 描述  :USB中断和CAN接收中断服务程序,USB跟CAN公用I/O,这里只用到CAN的中断。
 * 输入  :无
 * 输出  : 无	 
 * 调用  :无
 */
void CAN_RX_IRQHandler(void)
{
	CAN_Receive(CANx, CAN_FIFO0, &RxMessage);//从邮箱中提取报文
	
	if((RxMessage.ExtId==0x1314) && (RxMessage.IDE==CAN_ID_EXT) && (RxMessage.DLC==8))
		can_flag = 1;//接收成功
	else
		can_flag = 0;//接收失败
}

main.c

volatile uint8_t can_flag = 0;
volatile CanRxMsg RxMessage;//接收缓冲区


void RCC_Configuration(void)
{   
  //Install system time  is 72MHZ
	SystemInit();
}

int main(void)
{
	RCC_Configuration();//系统时钟初始化   				
	Delay_Timer_Init();//延时函数定时器初始化
	
	CAN_Config();						//can总线初始化
	USART_Config();					//串口初始化
	
	CanTxMsg TxMessage;			//发送缓冲区
	u8 rxTempData;
	
	TxMessage.ExtId=0x1314;				//使用的扩展ID
  TxMessage.IDE=CAN_ID_EXT;			//扩展模式
  TxMessage.RTR=CAN_RTR_DATA;		//发送的是数据
  TxMessage.DLC=8;							//数据长度为8字节
	//设置要发送的数据
	for(int i=0; i<8; i++)
	{
		TxMessage.Data[i] = i;
	}
	
  while(1)
	{
		CAN_Transmit(CANx, &TxMessage);//发送数据
		Delay_ms(1000);
		
		if(can_flag == 1)
		{
			can_flag = 0;
			for(int i=0; i<8; i++)
			{
				rxTempData = RxMessage.Data[i];
				Usart_SendByte(USART1, rxTempData);
			}
		}
	}
}

8、SPI通信

(1)SPI通信概述

参考之前写的博客:
SPI通信概述理论部分
STC12C5A60S2软件模式SPI读取DS1302时钟实时显示在1602

(2)STM32 SPI通信简介

使用STM32硬件SPI通信的编程要点与CAN、I2C大同小异,要点如下:

1、初始化通信使用的引脚和时钟
2、使能SPI外设时钟
3、配置SPI收发模式、地址、速率等参数,并使能SPI外设
4、编写基本SPI按字节收发的函数

本例程使用SPI通信进行FLASH读写操作,编程要点如下:

1、编写对FLASH的擦除和读写操作函数
2、编写测试程序,对读写程序进行校验

SPI初始化结构体如下:

typedef struct
{
  uint16_t SPI_Direction;           /*!< Specifies the SPI unidirectional or bidirectional data mode.
                                         This parameter can be a value of @ref SPI_data_direction */

  uint16_t SPI_Mode;                /*!< Specifies the SPI operating mode.
                                         This parameter can be a value of @ref SPI_mode */

  uint16_t SPI_DataSize;            /*!< Specifies the SPI data size.
                                         This parameter can be a value of @ref SPI_data_size */

  uint16_t SPI_CPOL;                /*!< Specifies the serial clock steady state.
                                         This parameter can be a value of @ref SPI_Clock_Polarity */

  uint16_t SPI_CPHA;                /*!< Specifies the clock active edge for the bit capture.
                                         This parameter can be a value of @ref SPI_Clock_Phase */

  uint16_t SPI_NSS;                 /*!< Specifies whether the NSS signal is managed by
                                         hardware (NSS pin) or by software using the SSI bit.
                                         This parameter can be a value of @ref SPI_Slave_Select_management */
 
  uint16_t SPI_BaudRatePrescaler;   /*!< Specifies the Baud Rate prescaler value which will be
                                         used to configure the transmit and receive SCK clock.
                                         This parameter can be a value of @ref SPI_BaudRate_Prescaler.
                                         @note The communication clock is derived from the master
                                               clock. The slave clock does not need to be set. */

  uint16_t SPI_FirstBit;            /*!< Specifies whether data transfers start from MSB or LSB bit.
                                         This parameter can be a value of @ref SPI_MSB_LSB_transmission */

  uint16_t SPI_CRCPolynomial;       /*!< Specifies the polynomial used for the CRC calculation. */
}SPI_InitTypeDef;

1、SPI_Direction
本成员可以SPI通信方式:
双线全双工(SPI_Direction_2Lines_FullDuplex)
双线只接收(SPI_Direction_2Lines_RxOnly)
单线只接收 (SPI_Direction_1Line_Rx)
单线只发送模式(SPI_Direction_1Line_Tx)

2、SPI_Mode
本成员可以设置SPI工作模式:
主机模式 (SPI_Mode_Master)
从机模式 (SPI_Mode_Slave )
备注:这两个模式最大的区别为SPI的时钟线的时许,如果是主机模式,SPI的时许由主机产生;如果为从机模式,SPI的时许由外来设备提供。

3、SPI_DataSize
本成员可设置SPI通信的数据帧大小,可以设置为8位和16位的

4、SPI_CPOL 和 SPI_CPHA
这两个成员分别设置时钟极性和时钟相位

5、SPI_NSS
本成员配置NSS引脚使用模式,可以设置为
硬件模式(SPI_NSS_Hard)
软件模式(SPI_NSS_Soft)
备注:在硬件模式中SPI片选信号由硬件自动产生,而软件需要我们亲自把相应的GPIO端口拉高或置低产生非片选和片选信号,实际中软件模式应用得比较多。

6、SPI_BaudRatePrescaler
本成员设置波特率分频因子,分频后时钟为SPI的SCK信号线的时钟频率,这个成员的参数可以设置为2、4、6、8、16、32、64、128、256分频。

SPI_CRCPolynomial
这是SPI的CRC校验中的多项式,若使用CRC校验时,就使用这个成员的参数来计算CRC校验值。

(3)使用STM32对W25Q64读写操作(理论部分)

在这里插入图片描述
在这里插入图片描述
CS:SPI片选输入
DO:数据输出,直接连接SPI的SDI
WP:写入使能接口(使能和使能写入功能)
GND:设备STM32-GND
DI:数据输入,直接连接SPI的SDO
CLK:时钟输入接口,直接连接SPI的时钟接口
HOLD:控制引脚(启动和暂停芯片工作状态)
VCC:自己接STM32-VCC(2.7-3.6V)
在这里插入图片描述
在这里插入图片描述
编程的时许图可以查看相关芯片手册…

(3)使用STM32对W25Q64读写操作(参考例程)

spi_flash.h

#ifndef __SPI_FLASH_H
#define	__SPI_FLASH_H

#include "stm32f10x.h"
#include "usart.h"

/*命令定义-开头*******************************/
#define W25X_WriteEnable		      0x06	//写入启动
#define W25X_WriteDisable		      0x04 	//写入失能
#define W25X_ReadStatusReg		    0x05 	//读状态寄存器
#define W25X_WriteStatusReg		    0x01 	//写状态寄存器
#define W25X_ReadData			        0x03 	//读数据
#define W25X_FastReadData		      0x0B 	//快速读数据
#define W25X_FastReadDual		      0x3B 	//快速读取数据双输出
#define W25X_PageProgram		      0x02 	//页面程序
#define W25X_BlockErase			      0xD8 	//块擦除
#define W25X_SectorErase		      0x20	//扇区删除
#define W25X_ChipErase			      0xC7 	//芯片删除
#define W25X_PowerDown			      0xB9 	//关机
#define W25X_ReleasePowerDown	    0xAB	//掉电模式
#define W25X_DeviceID			        0xAB 	//获取设备ID
#define W25X_ManufactDeviceID   	0x90 	//获取制造商设备ID
#define W25X_JedecDeviceID		    0x9F

/* WIP(busy)标志,FLASH内部正在写入 */
#define WIP_Flag                  0x01
#define Dummy_Byte                0xFF
/*命令定义-结尾*******************************/

/*SPI接口定义-开头****************************/
#define      FLASH_SPIx                        	SPI1
#define      FLASH_SPI_APBxClock_FUN          	RCC_APB2PeriphClockCmd
#define      FLASH_SPI_CLK                     	RCC_APB2Periph_SPI1

//CS(NSS)引脚 片选选普通GPIO即可
#define      FLASH_SPI_CS_APBxClock_FUN       	RCC_APB2PeriphClockCmd
#define      FLASH_SPI_CS_CLK                  	RCC_APB2Periph_GPIOA    
#define      FLASH_SPI_CS_PORT                 	GPIOA
#define      FLASH_SPI_CS_PIN                  	GPIO_Pin_4

//SCK引脚
#define      FLASH_SPI_SCK_APBxClock_FUN      	RCC_APB2PeriphClockCmd
#define      FLASH_SPI_SCK_CLK                 	RCC_APB2Periph_GPIOA   
#define      FLASH_SPI_SCK_PORT                	GPIOA   
#define      FLASH_SPI_SCK_PIN                 	GPIO_Pin_5
//MISO引脚
#define      FLASH_SPI_MISO_APBxClock_FUN     	RCC_APB2PeriphClockCmd
#define      FLASH_SPI_MISO_CLK                	RCC_APB2Periph_GPIOA    
#define      FLASH_SPI_MISO_PORT               	GPIOA 
#define      FLASH_SPI_MISO_PIN                	GPIO_Pin_6
//MOSI引脚
#define      FLASH_SPI_MOSI_APBxClock_FUN     	RCC_APB2PeriphClockCmd
#define      FLASH_SPI_MOSI_CLK                	RCC_APB2Periph_GPIOA    
#define      FLASH_SPI_MOSI_PORT               	GPIOA 
#define      FLASH_SPI_MOSI_PIN                	GPIO_Pin_7

#define  		SPI_FLASH_CS_LOW()     							GPIO_ResetBits( FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN )
#define  		SPI_FLASH_CS_HIGH()    							GPIO_SetBits( FLASH_SPI_CS_PORT, FLASH_SPI_CS_PIN )
/*SPI接口定义-结尾****************************/

extern void SPI_FLASH_Init(void);
extern uint8_t SPI_SendData( uint8_t data);
extern u32 SPI_FLASH_ReadDeviceID(void);
extern void SPI_FLASH_SectorErase(u32 SectorAddr);
extern void SPI_FLASH_BulkErase(void);
extern void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite);
extern void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead);

#endif


spi_flash.c

#include "./flash/spi_flash.h"

/**
  * @brief  SPI_FLASH初始化
  * @param  无
  * @retval 无
  */
void SPI_FLASH_Init(void)
{
	SPI_InitTypeDef  SPI_InitStructure;		//定义SPI结构体
  GPIO_InitTypeDef GPIO_InitStructure;	//定义GPIO结构体
	
	/* 使能SPI时钟 */
	FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
	
	/* 使能SPI引脚相关的时钟 */
 	FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );
	
	/* 配置SPI的 CS引脚,普通IO即可 */
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
	
  /* 配置SPI的 SCK引脚*/
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);

  /* 配置SPI的 MISO引脚*/
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
  GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);

  /* 配置SPI的 MOSI引脚*/
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
  GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);

  /* 停止信号 FLASH: CS引脚高电平*/
  SPI_FLASH_CS_HIGH();

  /* SPI 模式配置 */
  // FLASH芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;		//双线全双工模式
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;													//主机模式
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;											//8位SPI通信数据帧大小
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;														//时钟极性CPOL设置为高电平
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;													//时钟相位在SCK的偶数边沿采集
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;															//硬件模式,SPI片选信号由SPI硬件自动产生
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;		//波特率4分频
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;										//通信过程中,高数据在前
  SPI_InitStructure.SPI_CRCPolynomial = 7;															//CRC校验
  SPI_Init(FLASH_SPIx , &SPI_InitStructure);														//初始化SPI

  /* 使能 SPI  */
  SPI_Cmd(FLASH_SPIx , ENABLE);
}

//SPI发送一个帧数据,同时接收一个帧数据
uint8_t SPI_SendData( uint8_t data)
{	
	uint16_t timeout=0x2710;   //10,000
	
	/*
		寄存器的状态读取可以随时就行,这个不受SPI是否在传输数据的影响,
		检查发送缓冲空标志位,
		如果发送缓冲为RESET,继续运行程序;
		如果为set,结束程序
	*/
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_TXE)==RESET)
		if((timeout--)==0) return printf("等待超时,发送失败!\n");

	//通过外设向SPI发送一个数据
	SPI_I2S_SendData(FLASH_SPIx, data);
	
	timeout=0x2710;          //10,000次循环无果后为失败
	while(SPI_I2S_GetFlagStatus(FLASH_SPIx, SPI_I2S_FLAG_RXNE)==RESET)
		if((timeout--)==0) return printf("等待超时,接收失败!\n");
		
	//返回通过SPI接收到的数据
	return SPI_I2S_ReceiveData(FLASH_SPIx);
}

//下面是STM32与FLASH通信功能的函数*
//---------------------------------------------------------------

 /**
  * @brief  读取FLASH Device ID
  * @param 	无
  * @retval FLASH Device ID
  */
u32 SPI_FLASH_ReadDeviceID(void)
{
  u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;

  /* 开始通讯:CS低电平 */
  SPI_FLASH_CS_LOW();

  /* 发送JEDEC指令,读取ID */
  SPI_SendData(W25X_JedecDeviceID);

  /* 读取一个字节数据 */
  Temp0 = SPI_SendData(Dummy_Byte);

  /* 读取一个字节数据 */
  Temp1 = SPI_SendData(Dummy_Byte);

  /* 读取一个字节数据 */
  Temp2 = SPI_SendData(Dummy_Byte);

 /* 停止通讯:CS高电平 */
  SPI_FLASH_CS_HIGH();

  /*把数据组合起来,作为函数的返回值*/
	Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;

  return Temp;
}

 /**
  * @brief  向FLASH发送 写使能 命令
  * @param  none
  * @retval none
  */
void SPI_FLASH_WriteEnable(void)
{
  /* 通讯开始:CS低 */
  SPI_FLASH_CS_LOW();

  /* 发送写使能命令*/
  SPI_SendData(W25X_WriteEnable);

  /*通讯结束:CS高 */
  SPI_FLASH_CS_HIGH();
}

/* WIP(busy)标志,FLASH内部正在写入 */
#define WIP_Flag                  0x01
 /**
  * @brief  等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
  * @param  none
  * @retval none
  */
void SPI_FLASH_WaitForWriteEnd(void)
{
  u8 FLASH_Status = 0;

  /* 选择 FLASH: CS 低 */
  SPI_FLASH_CS_LOW();

  /* 发送 读状态寄存器 命令 */
  SPI_SendData(W25X_ReadStatusReg);

  /* 若FLASH忙碌,则等待 */
  do
  {
		/* 读取FLASH芯片的状态寄存器 */
    FLASH_Status = SPI_SendData(Dummy_Byte);	 
  }
  while ((FLASH_Status & WIP_Flag) == SET);  /* 正在写入标志 */

  /* 停止信号  FLASH: CS 高 */
  SPI_FLASH_CS_HIGH();
}

 /**
  * @brief  擦除FLASH扇区
  * @param  SectorAddr:要擦除的扇区地址
  * @retval 无
  */
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
  /* 发送FLASH写使能命令 */
  SPI_FLASH_WriteEnable();
  SPI_FLASH_WaitForWriteEnd();
  /* 擦除扇区 */
  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();
  /* 发送扇区擦除指令*/
  SPI_SendData(W25X_SectorErase);
  /*发送擦除扇区地址的高位*/
  SPI_SendData((SectorAddr & 0xFF0000) >> 16);
  /* 发送擦除扇区地址的中位 */
  SPI_SendData((SectorAddr & 0xFF00) >> 8);
  /* 发送擦除扇区地址的低位 */
  SPI_SendData(SectorAddr & 0xFF);
  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();
  /* 等待擦除完毕*/
  SPI_FLASH_WaitForWriteEnd();
}

 /**
  * @brief  擦除FLASH扇区,整片擦除
  * @param  无
  * @retval 无
  */
void SPI_FLASH_BulkErase(void)
{
  /* 发送FLASH写使能命令 */
  SPI_FLASH_WriteEnable();

  /* 整块 Erase */
  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();
  /* 发送整块擦除指令*/
  SPI_SendData(W25X_ChipErase);
  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();

  /* 等待擦除完毕*/
  SPI_FLASH_WaitForWriteEnd();
}

 /**
  * @brief  对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
  * @param	pBuffer,要写入数据的指针
  * @param 	WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
  * @retval 无
  */
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  /* 发送FLASH写使能命令 */
  SPI_FLASH_WriteEnable();

  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();
  /* 写页写指令*/
  SPI_SendData(W25X_PageProgram);
  /*发送写地址的高位*/
  SPI_SendData((WriteAddr & 0xFF0000) >> 16);
  /*发送写地址的中位*/
  SPI_SendData((WriteAddr & 0xFF00) >> 8);
  /*发送写地址的低位*/
  SPI_SendData(WriteAddr & 0xFF);

  if(NumByteToWrite > 256)
  {
     NumByteToWrite = 256;
     printf("SPI_FLASH_PageWrite too large!");
  }

  /* 写入数据*/
  while (NumByteToWrite--)
  {
    /* 发送当前要写入的字节数据 */
    SPI_SendData(*pBuffer);
    /* 指向下一字节数据 */
    pBuffer++;
  }

  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();

  /* 等待写入完毕*/
  SPI_FLASH_WaitForWriteEnd();
}

#define SPI_FLASH_PageSize      256
 /**
  * @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
  * @param	pBuffer,要写入数据的指针
  * @param  WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度
  * @retval 无
  */
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
	
	/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
  Addr = WriteAddr % SPI_FLASH_PageSize;
	
	/*差count个数据值,刚好可以对齐到页地址*/
  count = SPI_FLASH_PageSize - Addr;
	/*计算出要写多少整数页*/
  NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
	/*mod运算求余,计算出剩余不满一页的字节数*/
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
	
	/* Addr=0,则WriteAddr 刚好按页对齐 aligned  */
  if (Addr == 0)
  {
		/* NumByteToWrite < SPI_FLASH_PageSize */
    if (NumOfPage == 0) 
    {
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    }
    else /* NumByteToWrite > SPI_FLASH_PageSize */
    { 
			/*先把整数页都写了*/
      while (NumOfPage--)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;
        pBuffer += SPI_FLASH_PageSize;
      }
			/*若有多余的不满一页的数据,把它写完*/
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
  }
	/* 若地址与 SPI_FLASH_PageSize 不对齐  */
  else 
  {
		/* NumByteToWrite < SPI_FLASH_PageSize */
    if (NumOfPage == 0)
    {
			/*当前页剩余的count个位置比NumOfSingle小,一页写不完*/
      if (NumOfSingle > count) 
      {
        temp = NumOfSingle - count;
				/*先写满当前页*/
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
				
        WriteAddr +=  count;
        pBuffer += count;
				/*再写剩余的数据*/
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
      }
      else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
      }
    }
    else /* NumByteToWrite > SPI_FLASH_PageSize */
    {
			/*地址不对齐多出的count分开处理,不加入这个运算*/
      NumByteToWrite -= count;
      NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
      NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
			
			/* 先写完count个数据,为的是让下一次要写的地址对齐 */
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
			
			/* 接下来就重复地址对齐的情况 */
      WriteAddr +=  count;
      pBuffer += count;
			/*把整数页都写了*/
      while (NumOfPage--)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;
        pBuffer += SPI_FLASH_PageSize;
      }
			/*若有多余的不满一页的数据,把它写完*/
      if (NumOfSingle != 0)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
      }
    }
  }
}

 /**
  * @brief  读取FLASH数据
  * @param 	pBuffer,存储读出数据的指针
  * @param   ReadAddr,读取地址
  * @param   NumByteToRead,读取数据长度
  * @retval 无
  */
void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();

  /* 发送 读 指令 */
  SPI_SendData(W25X_ReadData);

  /* 发送 读 地址高位 */
  SPI_SendData((ReadAddr & 0xFF0000) >> 16);
  /* 发送 读 地址中位 */
  SPI_SendData((ReadAddr& 0xFF00) >> 8);
  /* 发送 读 地址低位 */
  SPI_SendData(ReadAddr & 0xFF);
	
	/* 读取数据 */
  while (NumByteToRead--) /* while there is data to be read */
  {
    /* 读取一个字节*/
    *pBuffer = SPI_SendData(Dummy_Byte);
    /* 指向下一个字节缓冲区 */
    pBuffer++;
  }

  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();
}

main.c

#include "./flash/spi_flash.h"

volatile uint32_t time = 0;
volatile uint8_t can_flag = 0;
volatile CanRxMsg RxMessage;//接收缓冲区


void RCC_Configuration(void)
{   
  //Install system time  is 72MHZ
	SystemInit();
}

int main(void)
{
	RCC_Configuration();	//系统时钟初始化
	Delay_Timer_Init();		//延时函数定时器初始化
	USART_Config();				//串口初始化
	
	//向串口发送一段字符串,正面串口初始化成功
	printf("这是一个8Mbyte串行flash(W25Q64)实验 \r\n");
	
	//初始化W25Q64
	SPI_FLASH_Init();
	
	//获取 Flash Device ID
	//通过这段程序,可以证明SPI通信成功
	uint32_t DeviceID = 0;
	DeviceID = SPI_FLASH_ReadDeviceID();
	printf("FlashID is 0x%X \r\n",DeviceID);
	
	//擦除将要写入的扇区(写数据之前要先擦除数据)
	SPI_FLASH_SectorErase(0x00000);
	
	//写入数据
	uint8_t Tx_Buffer[] = "Hello World!\r\n";
	SPI_FLASH_BufferWrite(Tx_Buffer, 0x00000, sizeof(Tx_Buffer));
	
	//读取数据
	uint8_t Rx_Buffer[sizeof(Tx_Buffer)];
	SPI_FLASH_BufferRead(Rx_Buffer, 0x00000, sizeof(Tx_Buffer));
	printf("\r\n 读出的数据为:%s \r\n", Rx_Buffer);
	
  while(1)
	{
		
	}
}

参考文献

【1】STM32F10x-参考手册中文版
【2】STM32F101xx和STM32F103xx固件函数库
【3】stm32f103数据手册中文版
【4】Cortex-M3权威指南Cn
【5】零死角玩转STM32—F103指南者
【6】STM32库开发实战指南——基于野火霸道开发板

  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小明n.n

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值