5。STM32裸机开发(5)

嵌入式软件开发学习过程记录,本部分结合本人的学习经验撰写,系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维,为RTOS做铺垫(本部分基于库函数版实现),如有不足之处,敬请批评指正。 

(5)中主要讲解外部数据的获取,首先介绍模数、数模转换、随后介绍除串口外的常用四种通信协议:IIC、SPI、485、CAN

一 ADC模数转换实验

        ADC按照其转换原理主要分为逐次逼近型、双积分型、电压频率转换型三种。STM32F1 的 ADC 就是逐次逼近型的模拟数字转换器。STM32F103 系列一般都有 3 个 ADC,这些 ADC 可以独立使用,也可以使用双重/三重模式(提高采样率)。

选择输入通道-设置转换顺序-选择触发源(外部触发事件:禁止触发检测、上升沿检测、下降沿检测以及上升沿和下降沿均检测)-设置采样周期-数据寄存器存储数据

1.STM32F1 的 ADC 是 12 位逐次逼近型的模拟数字转换器。

12位表示ADC输出的数字信号由12位数字值组成

2.具有多达 18 个复用通道,可测量来自 16 个外部源、2 个内部信号源。

3.通道的 A/D 转换可以单次、连续、扫描或间断模式执行。

4.ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。

5.ADC 具有模拟看门狗特性,允许应用程序检测输入电压是否超出用户定义的阀值上限或者下限。

 18个通道对应的引脚

ADC中的中断

当发生如下事件且使能相应中断标志位时,ADC 能产生中断。
1.转换结束(规则转换)与注入转换结束
        数据转换结束后,如果使能中断转换结束标志位,转换一结束就会产生转换结束中断。
2.模拟看门狗事件
        当被 ADC 转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断,前提是我们开启了模拟看门狗中断,其中低阈值和高阈值由 ADC_LTR 和 ADC_HTR 设置。
3.DMA 请求
        规则和注入通道转换结束后,除了产生中断外,还可以产生 DMA 请求,把转换好的数据直接存储在内存里面。要注意的是只有 ADC1 和 ADC3 可以产生DMA 请求。

        DMA(Direct Memory Access)是单片机中的一种数据传输方式,它可以在不占用处理器资源的情况下,通过直接将数据从外设传输到存储器或者从存储器传输到外设。

        在传统的 I/O 操作中,CPU 需要向外设发送读写指令,并不断地查询外设是否已经完成读写操作。这种方式需要占用 CPU 大量的时间和资源,容易造成 CPU 的负载过高,影响系统的稳定性和可靠性。而采用 DMA 传输方式,可以使得 CPU 参与的仅仅是开始和结束的操作,即启动 DMA 传输和等待 DMA 传输完成,从而节省了大量的 CPU 资源。

        单片机 DMA 的实现方式有多种,其中最常见的是将该功能集成在芯片内部,由硬件直接控制。在 STM32 系列单片机中,DMA 控制器包括多个通道,每个通道可以连接不同的外设和存储器地址,通过配置相应的寄存器,可以实现数据在外设和存储器之间的直接传输。这样,就能够提高系统的并行性和整体性能,同时降低 CPU 的使用率,提高系统的可靠性。

        需要注意的是,使用单片机 DMA 传输时需要对通道进行合理的分配和调度,避免出现数据竞争和冲突等问题,同时需要根据具体的硬件和软件要求进行相应的配置,并且确保 DMA 传输的正确性和可靠性。

我们知道 STM32F1 ADC 转换模式有单次转换与连续转换区分。
        在单次转换模式下,ADC 执行一次转换。可以通过 ADC_CR2 寄存器的 SWSTART 位(只适用于规则通道)启动,也可以通过外部触发启动(适用于规则通道和注入通道),这时 CONT 位为 0。以规则通道为例,一旦所选择的通道转换完成,转换结果将被存在 ADC_DR 寄存器中,EOC(转换结束)标志将被置位,如果设置了 EOCIE,则会产生中断。然后 ADC 将停止,直到下次启动。
        在连续转换模式下,ADC 结束一个转换后立即启动一个新的转换。CONT 位为 1 时,可通过外部触发或将 ADC_CR2 寄存器中的 SWSTRT 位置 1 来启动此模式(仅适用于规则通道)。需要注意的是:此模式无法连续转换注入通道。连续模式下唯一的例外情况是,注入通道配置为在规则通道之后自动转换(使用 JAUTO 位)。
STM32F1 ADC 配置步骤
(ADC 相关库函数在 stm32f10x_adc.c 和 stm32f10x_adc.h 文件中)
(1)使能端口时钟和 ADC 时钟,设置引脚模式为模拟输入
        由于 ADCx_IN0-ADCx_IN15 属于 外部通道,每个通道都会对应芯片的一个引脚,比如 ADC1_IN1 对应 STM32F103ZET6 的 PA1 引脚,所以首先要使能 GPIOA 端口时钟和 ADC1 时钟,如下:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,E
NABLE);
        然后将 PA1 引脚配置为模拟输入模式

 GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AN; //模拟输入模式

(2)设置 ADC 的分频因子
        开启 ADC1 时钟之后,我们就可以通过 RCC_CFGR 设置 ADC 的分频因子。分频因子要确保 ADC 的时钟( ADCCLK)不要超过 14Mhz。 这个我们设置分频因子为 6, 因此 ADC 时钟为 72/6=12MHz,库函数的实现方法如下:
RCC_ADCCLKConfig(RCC_PCLK2_Div6);

 (3)初始化 ADC 参数,包括 ADC 工作模式、规则序列等

        我们知道要使用 ADC,需要配置 ADC 的转换模式、触发方式、数据对齐方式、规则序列等参数,这些参数是通过库函数 ADC_Init 函数实现。函数原型如下:函数中第一个参数是用来选择 ADC,例如 ADC1;第二个参数是一个结构体指针变量,结构体类型是 ADC_InitTypeDef,其内包含了 ADC 初始化的成员变量。

void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);

typedef struct
{
        uint32_t ADC_Mode; // ADC 工作模式选择
        FunctionalState ADC_ScanConvMode; /* ADC 扫描(多通道)或者单次 (单通道)模式选择 */
        FunctionalState ADC_ContinuousConvMode; // ADC 单次转换或者连续转换选择
        uint32_t ADC_ExternalTrigConv; // ADC 转换触发信号选择
        uint32_t ADC_DataAlign; // ADC 数据寄存器对齐格式
        uint8_t ADC_NbrOfChannel; // ADC 采集通道数
} ADC_InitTypeDef;
参数如下:
ADC_Mode:ADC 模式选择,有独立模式、双重模式,在双重模式下还有很多细分模式可选,具体由 ADC_CR1:DUALMOD 位配置。
ADC_ScanConvMode:ADC 扫描模式选择。可选参数为 ENABLE 或 DISABLE,用来设置是否打开 ADC 扫描模式。如果是单通道 AD 转换,选择 DISABLE;如果是多通道 AD 转换,选择 ENABLE。
ADC_ContinuousConvMode:ADC 连续转换模式选择。可选参数为 ENABLE 或DISABLE,用来设置是连续转换还是单次转换模式。如果为 ENABLE,则选择连续转换模式;如果为 DISABLE,则选择单次转换模式,转换一次后停止,需要手动控制才能重新启动转换。
ADC_ExternalTrigConv:ADC 外部触发选择。ADC 外部触发条件有很多,在前面介绍框图时已列出,根据需要选择对应的触发条件,通常我们使用软件自动触发,所以此成员可以不用配置。
ADC_DataAlign: ADC 数据对齐方式 。可选参数为右对齐ADC_DataAlign_Right 和左对齐 ADC_DataAlign_Left。
ADC_NbrOfChannel:AD 转换通道数目,根据实际设置。具体的通道数和通道的转换顺序是配置规则序列或注入序列寄存器。
(4)使能 ADC 并校准
        前面几个步骤已经将 ADC 配置好,但还不能正常使用,只有开启 ADC 并且复位校准了才能让它正常工作
开启 ADC 的库函数
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);
开启 ADC1
ADC_Cmd(ADC1, ENABLE);//开启 AD 转换器
执行复位校准的方法是:
ADC_ResetCalibration(ADC1);
执行 ADC 校准的方法是:
ADC_StartCalibration(ADC1); //开始指定 ADC1 的校准状态
每次进行校准之后要等待校准结束。 这里是通过获取校准状态来判断是否校准是否结束。
比如
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
(5)读取 ADC 转换值 (在实际使用时,如果要使用32的原装AD转换而不用外置芯片的话,AD转换为温度是有计算公式的,这里只获取AD值,注意哦)
        通过上面几步配置,ADC 就算准备好了,接下来我们要做的就是设置规则序列里面的通道采样顺序以及通道的采样周期,然后启动 ADC 转换。在转换结束后,读取转换结果值就可以了。设置规则序列通道以及采样周期的库函数是:
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel,uint8_t Rank, uint8_t ADC_SampleTime);
        参数 1 用来选择 ADC,参数 2 用来选择规则序列里面的通道,参数 3 用来设置转换通道的数量,参数 4 用来设置采样周期。例如ADC1_IN1 单次转换,采样周期为 239.5,代码如下:
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5 );
设置好规则序列通道及采样周期,接下来就要开启转换
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
例如要开启 ADC1 转换,调用函数为:
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的 ADC1 的软件转换启动功能
开启转换之后,就可以获取 ADC 转换结果数据,调用的库函数是:
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);
例如要获取 ADC1 转换结果,调用函数是:

 ADC_GetConversionValue(ADC1);

        同时在 AD 转换中,我们还要根据状态寄存器的标志位来获取 AD 转换的各个状态信息。获取 AD 转换的状态信息的库函数是:
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
例如我们要判断 ADC1 的转换是否结束,方法是:
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
#include "adc.h"
#include "SysTick.h"

//ADC1初始化函数
void ADCx_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure; //定义结构体变量	
	ADC_InitTypeDef       ADC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,ENABLE);//使能时钟
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
	
    //配置GPIO引脚
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1;//ADC
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;	//模拟输入
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
    //初始化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;//右对齐	
	ADC_InitStructure.ADC_NbrOfChannel = 1;//1个转换在规则序列中 也就是只转换规则序列1 
	ADC_Init(ADC1, &ADC_InitStructure);//ADC初始化
	
	ADC_Cmd(ADC1, ENABLE);//使能ADC或开启AD转换器
	
	ADC_ResetCalibration(ADC1);//重置指定的ADC的校准寄存器
	while(ADC_GetResetCalibrationStatus(ADC1));//获取ADC重置校准寄存器的状态
	
	ADC_StartCalibration(ADC1);//开始指定ADC的校准状态
	while(ADC_GetCalibrationStatus(ADC1));//获取指定ADC的校准程序

	ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能或者失能指定的ADC的软件转换启动功能
}


//编写读取ADC转换值函数
u16 Get_ADC_Value(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	//设置指定ADC的规则组通道,采样顺序,采样周期
	ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5);	//ADC1,ADC通道,239.5个周期,提高采样时间可以提高精确度			    
	
	for(t=0;t<times;t++)
	{
		ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能指定的ADC1的软件转换启动功能	
		while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
		temp_val+=ADC_GetConversionValue(ADC1);
		delay_ms(5);
	}
	return temp_val/times;
} 

二 DAC 数模转换实验

        DAC(Digital to analog converter)即数字模拟转换器,它可以将数字信号转换为模拟信号。它的功能与 ADC 相反。在常见的数字信号系统中,大部分传感器信号被转化成电压信号,而 ADC 把电压模拟信号转换成易于计算机存储、处理的数字编码,由计算机处理完成后,再由 DAC 输出电压模拟信号,该电压模拟信号常常用来驱动某些执行器件,使人类易于感知。如音频信号的采集及还原就是这样一个过程。

STM32F1 DAC 模块是 12 位电压输出数模转换器,它可以配置为 8 位或 12 位模式,

也可以与 DMA 控制器配合使用。

DAC 工作在 12 位模式下,数据可以采 用左对齐或右对齐。

DAC 工作在 8 位模式下,数据只有右对齐方式。

DAC 有两个输出通道,每个通道各有一个转换器。

在 DAC 双通道模式下,每个通道可以单独进行转换;

当两个通道组合在一起同步执行更新操作时,也可以同时进行转换。

DAC 可通过一个输入参考电压引脚 VREF+(与 ADC 共享)来提高转换后的数据精度。

STM32F1 DAC 配置步骤
(DAC 相关库函数在 stm32f10x_dac.c 和 stm32f10x_dac.h 文件中)
(1)使能端口及 DAC 时钟,设置引脚为模拟输入
        DAC 的两个通道对应的是 PA4、PA5 引脚,这个在芯片数据手册内可以查找到。因此使用 DAC 某个通道输出的时候需要使能 GPIOA 端口和 DAC 时钟(DAC 模块时钟是由 APB1 提供),并且还要将对应通道的引脚配置为模拟输入模式。

例如要让 DAC1_OUT 输出,其对应的是 PA4 引脚,所以使能时钟和配置 PA4 引脚为模拟输入模式,代码如下:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);// 使能 GPIOA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);//使能 DAC 时钟
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化
(2)初始化 DAC,设置 DAC 工作模式
        要使用 DAC,必须对其相关参数进行设置,包括 DAC 通道 1 使能、 DAC 通道 1 输出缓存关闭、不使用触发、不使用波形发生器等设置,该部分设置通过DAC 初始化函数 DAC_Init 完成的:
void DAC_Init(uint32_t DAC_Channel, DAC_InitTypeDef* DAC_InitStruct);

        函 数 中 第 一 个 参 数 是 用 来 确 定 哪 个 DAC 通 道 , 例 如 DAC 通 道 1 (DAC_Channel_1);第二个参数是一个结构体指针变量,结构体类型是 DAC_InitTypeDef,其内包含了 DAC 初始化的成员变量。下面我们简单介绍下它的成员:

typedef struct
{
        uint32_t DAC_Trigger; //DAC 触发选择
        uint32_t DAC_WaveGeneration; //DAC 波形发生
        uint32_t DAC_LFSRUnmask_TriangleAmplitude; //屏蔽/幅值选择器
        uint32_t DAC_OutputBuffer; //DAC 输出缓存
}DAC_InitTypeDef;
参数如下:
DAC_Trigger:设置是否使用触发功能。前面介绍框图时已经说了 DAC 具有多个触发源,有定时器触发,外部中断线 9 触发,软件触发和不使用触发。其配置参数可在stm32f10x_dac.h 找到
DAC_WaveGeneration:设置是否使用波形发生。在前面框图介绍也讲过,其配置参数可在 stm32f10x_dac.h 找到
DAC_LFSRUnmask_TriangleAmplitude:设置屏蔽/幅值选择器。这个变量只在使用波形发 生器的时候才有用, 通常我们设置为 0 即可,值 为 DAC_LFSRUnmask_Bit0。其他配置参数同样可在 stm32f10x_dac.h 找到。
DAC_OutputBuffer:设置输出缓存控制位。通常我们不使用输出缓存功能, 所以配置参数为 DAC_OutputBuffer_Disable。如果使用的话可以配置为使能 DAC_OutputBuffer_Enable。
(3)使能 DAC 的输出通道

 初始化 DAC 后,我们就需要开启它,使能 DAC 输出通道的库函数为:

void DAC_Cmd(uint32_t DAC_Channel, FunctionalState NewState);

例如:使能 DAC 通道 1 输出
DAC_Cmd(DAC_Channel_1, ENABLE); //使能 DAC 通道 1
(4)设置 DAC 的输出值
        通过前面 4 个步骤的设置, DAC 就可以开始工作了,如果我们使用 12 位右对齐数据格式,我们通过设置 DHR12R1,就可以在 DAC 输出引脚(PA4)得到不同的电压值了。设置DHR12R1 的库函数中,
        第一个参数是设置数据对其方式,可以为 12 位右对齐 DAC_Align_12b_R, 12 位左对齐 DAC_Align_12b_L 以及 8 位右对齐 DAC_Align_8b_R 方式。
        第二个参数就是 DAC 的输入值,初始化时我们一般设置输入值为 0。
DAC_SetChannel1Data(DAC_Align_12b_R, 0); //12 位右对齐数据格式设置 DAC 值
库函数中,还提供一个读取 DAC 对应通道最后一次转换的数值,函数中参数 DAC_Channel 用于选择读取的 DAC 通道。可以为 DAC_Channel_1 和DAC_Channel_2。
uint16_t DAC_GetDataOutputValue(uint32_t DAC_Channel);
#include "dac.h"
#include "usart.h"

//初始化DCA函数
void DAC1_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure; //初始化结构体变量
	DAC_InitTypeDef DAC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能GPIOA时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);//使能DAC时钟
	
    //设置GPIO引脚为模拟输入模式
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;//DAC_1
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//模拟量输入
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
    //初始化DAC,设置DAC的工作模式
	DAC_InitStructure.DAC_Trigger=DAC_Trigger_None;	//不使用触发功能 TEN1=0
	DAC_InitStructure.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用波形发生
	DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0;//屏蔽、幅值设置
	DAC_InitStructure.DAC_OutputBuffer=DAC_OutputBuffer_Disable ;	//DAC1输出缓存关闭 BOFF1=1
	DAC_Init(DAC_Channel_1,&DAC_InitStructure);	 //初始化DAC通道1
	
	DAC_SetChannel1Data(DAC_Align_12b_R, 0);  //12位右对齐数据格式设置DAC值
	
	DAC_Cmd(DAC_Channel_1, ENABLE);  //使能DAC通道1
	
}

三 IIC实验(软件模拟IIC,因为STM32自带的稳定性不好)

        I2C 总线只有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强等特点,因此被广泛的使用在各大集成芯片内。

包括:主机、从机、主模式、从模式、仲裁、同步、发送器、接收器。

IIC的物理层有如下特点

(1)它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在 一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2)一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
(5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
(6)具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
(7)连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。

IIC的协议层有如下特点

(1)数据有效性规定
        I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
(2)起始和停止信号
        SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;
        SCL线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。
        起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态。
(3)应答响应
        每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。
        每当发送器件传输完一个字节的数据后,后面必须紧跟一个校验位,这个校验位是接收端通过控制 SDA(数据线)来实现的,以提醒发送端数据我这边已经接收完成,数据传送可以继续进行。这个校验位其实就是数据或地址传输过程中的响应。
        响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号即特定的低电平脉冲,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号即特定的高电平脉冲,发送方接收到该信号后会产生一个停止
信号,结束信号传输。应答响应时序图如下:

 (4)总线的寻址方式

        I2C 总线寻址按照从机地址位数可分为两种,一种是 7 位,另一种是 10 位。 采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)的位定义如下:
        D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。以7位寻址为例介绍
        当主机发送了一个地址后,总线上的每个器件都将头 7 位与它自己的地址 比较,如果一样,器件会判定它被主机寻址,其他地址不同的器件将被忽略后面的数据信号。至于是从机接收器还是从机发送器,都由 R/W 位决定的

 (5)数据传输

        I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/W),用“0”表示主机发送(写)数据(W),“1”表示主机接收数据 (R)。每次数据传送总是由主机产生的终止信号结束。但是,若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机进行寻址。
在总线的一次数据传送过程中,可以有以下几种组合方式:
a、主机向从机发送数据,数据传送方向在整个传送过程中不变

        注意:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。A 表示应答,A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。

b、主机在第一个字节后,立即从从机读数据
c、在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好相反

注意:此处使用IO口模拟IIC输出,而不是使用STM32的硬件IIC

#include "iic.h"
#include "SysTick.h"

/*******************************************************************************
* 函 数 名         : IIC_Init
* 函数功能		   : IIC初始化
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;//初始化结构体变量
	RCC_APB2PeriphClockCmd(IIC_SCL_PORT_RCC|IIC_SDA_PORT_RCC,ENABLE);//使能时钟
	
    //对GPIO引脚的配置
	GPIO_InitStructure.GPIO_Pin=IIC_SCL_PIN;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//配置推挽输出
	GPIO_Init(IIC_SCL_PORT,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;
	GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
	
	IIC_SCL=1;
	IIC_SDA=1;	
}

/*******************************************************************************
* 函 数 名         : SDA_OUT
* 函数功能		   : SDA输出配置	   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void SDA_OUT(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}

/*******************************************************************************
* 函 数 名         : SDA_IN
* 函数功能		   : SDA输入配置	   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void SDA_IN(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	
	GPIO_InitStructure.GPIO_Pin=IIC_SDA_PIN;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_Init(IIC_SDA_PORT,&GPIO_InitStructure);
}

/*******************************************************************************
* 函 数 名         : IIC_Start
* 函数功能		   : 产生IIC起始信号   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Start(void)
{
	SDA_OUT();     //sda线输出
	IIC_SDA=1;	  	  
	IIC_SCL=1;
	delay_us(5);
 	IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
	delay_us(6);
	IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
}	

/*******************************************************************************
* 函 数 名         : IIC_Stop
* 函数功能		   : 产生IIC停止信号   
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Stop(void)
{
	SDA_OUT();//sda线输出
	IIC_SCL=0;
	IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
 	IIC_SCL=1; 
	delay_us(6); 
	IIC_SDA=1;//发送I2C总线结束信号
	delay_us(6);							   	
}

/*******************************************************************************
* 函 数 名         : IIC_Wait_Ack
* 函数功能		   : 等待应答信号到来   
* 输    入         : 无
* 输    出         : 1,接收应答失败
        			 0,接收应答成功
*******************************************************************************/
u8 IIC_Wait_Ack(void)
{
	u8 tempTime=0;
	SDA_IN();      //SDA设置为输入  
	IIC_SDA=1;
	delay_us(1);	   
	IIC_SCL=1;
	delay_us(1);	 
	while(READ_SDA)
	{
		tempTime++;
		if(tempTime>250)
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;//时钟输出0 	   
	return 0;  
} 

/*******************************************************************************
* 函 数 名         : IIC_Ack
* 函数功能		   : 产生ACK应答  
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void IIC_Ack(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(5);
	IIC_SCL=0;
}

/*******************************************************************************
* 函 数 名         : IIC_NAck
* 函数功能		   : 产生NACK非应答  
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/		    
void IIC_NAck(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(5);
	IIC_SCL=0;
}	

/*******************************************************************************
* 函 数 名         : IIC_Send_Byte
* 函数功能		   : IIC发送一个字节 
* 输    入         : txd:发送一个字节
* 输    出         : 无
*******************************************************************************/		  
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
	SDA_OUT(); 	    
    IIC_SCL=0;//拉低时钟开始数据传输
    for(t=0;t<8;t++)
    {              
        if((txd&0x80)>0) //0x80  1000 0000
			IIC_SDA=1;
		else
			IIC_SDA=0;
        txd<<=1; 	  
		delay_us(2);   //对TEA5767这三个延时都是必须的
		IIC_SCL=1;
		delay_us(2); 
		IIC_SCL=0;	
		delay_us(2);
    }	 
} 

/*******************************************************************************
* 函 数 名         : IIC_Read_Byte
* 函数功能		   : IIC读一个字节 
* 输    入         : ack=1时,发送ACK,ack=0,发送nACK 
* 输    出         : 应答或非应答
*******************************************************************************/  
u8 IIC_Read_Byte(u8 ack)
{
	u8 i,receive=0;
	SDA_IN();//SDA设置为输入
    for(i=0;i<8;i++ )
	{
        IIC_SCL=0; 
        delay_us(2);
		IIC_SCL=1;
        receive<<=1;
        if(READ_SDA)receive++;   
		delay_us(1); 
    }					 
    if (!ack)
        IIC_NAck();//发送nACK
    else
        IIC_Ack(); //发送ACK   
    return receive;
}

具体使用:

编写功能函数——写入一个数据——写入长度为Len的数据——写入指定个数的数据

#include "24cxx.h"
#include "SysTick.h"

/*******************************************************************************
* 函 数 名         : AT24CXX_Init
* 函数功能		   : AT24CXX初始化
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
void AT24CXX_Init(void)
{
	IIC_Init();//IIC初始化
}

/*******************************************************************************
* 函 数 名         : AT24CXX_ReadOneByte
* 函数功能		   : 在AT24CXX指定地址读出一个数据
* 输    入         : ReadAddr:开始读数的地址 
* 输    出         : 读到的数据
*******************************************************************************/
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{				  
	u8 temp=0;		  	    																 
    IIC_Start();  
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0XA0);	   //发送写命令
		IIC_Wait_Ack();
		IIC_Send_Byte(ReadAddr>>8);//发送高地址	    
	}
	else 
	{
		IIC_Send_Byte(0XA0+((ReadAddr/256)<<1));   //发送器件地址0XA0,写数据
	} 	   
	IIC_Wait_Ack(); 
    IIC_Send_Byte(ReadAddr%256);   //发送低地址
	IIC_Wait_Ack();	    
	IIC_Start();  	 	   
	IIC_Send_Byte(0XA1);           //进入接收模式			   
	IIC_Wait_Ack();	 
    temp=IIC_Read_Byte(0);		   
    IIC_Stop();//产生一个停止条件	    
	return temp;
}

/*******************************************************************************
* 函 数 名         : AT24CXX_WriteOneByte
* 函数功能		   : 在AT24CXX指定地址写入一个数据
* 输    入         : WriteAddr  :写入数据的目的地址 
					 DataToWrite:要写入的数据
* 输    出         : 无
*******************************************************************************/
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{				   	  	    																 
    IIC_Start();  
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0XA0);	    //发送写命令
		IIC_Wait_Ack();
		IIC_Send_Byte(WriteAddr>>8);//发送高地址	  
	}
	else 
	{
		IIC_Send_Byte(0XA0+((WriteAddr/256)<<1));   //发送器件地址0XA0,写数据
	} 	 
	IIC_Wait_Ack();	   
    IIC_Send_Byte(WriteAddr%256);   //发送低地址
	IIC_Wait_Ack(); 	 										  		   
	IIC_Send_Byte(DataToWrite);     //发送字节							   
	IIC_Wait_Ack();  		    	   
    IIC_Stop();//产生一个停止条件 
	delay_ms(10);	 
}

/*******************************************************************************
* 函 数 名         : AT24CXX_WriteLenByte
* 函数功能		   : 在AT24CXX里面的指定地址开始写入长度为Len的数据
					 用于写入16bit或者32bit的数据
* 输    入         : WriteAddr  :写入数据的目的地址 
					 DataToWrite:要写入的数据
					 Len        :要写入数据的长度2,4
* 输    出         : 无
*******************************************************************************/
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{  	
	u8 t;
	for(t=0;t<Len;t++)
	{
		AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
	}												    
}

/*******************************************************************************
* 函 数 名         : AT24CXX_ReadLenByte
* 函数功能		   : 在AT24CXX里面的指定地址开始读出长度为Len的数据
					 用于读出16bit或者32bit的数据
* 输    入         : ReadAddr   :开始读出的地址 
					 Len        :要读出数据的长度2,4
* 输    出         : 读取的数据
*******************************************************************************/
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{  	
	u8 t;
	u32 temp=0;
	for(t=0;t<Len;t++)
	{
		temp<<=8;
		temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1); 	 				   
	}
	return temp;												    
}

/*******************************************************************************
* 函 数 名         : AT24CXX_Check
* 函数功能		   : 检查AT24CXX是否正常
* 输    入         : 无
* 输    出         : 1:检测失败,0:检测成功
*******************************************************************************/
u8 AT24CXX_Check(void)
{
	u8 temp;
	temp=AT24CXX_ReadOneByte(255);//避免每次开机都写AT24CXX			   
	if(temp==0x36)return 0;		   
	else//排除第一次初始化的情况
	{
		AT24CXX_WriteOneByte(255,0X36);
	    temp=AT24CXX_ReadOneByte(255);	  
		if(temp==0X36)return 0;
	}
	return 1;											  
}

/*******************************************************************************
* 函 数 名         : AT24CXX_Read
* 函数功能		   : 在AT24CXX里面的指定地址开始读出指定个数的数据
* 输    入         : ReadAddr :开始读出的地址 对24c02为0~255
					 pBuffer  :数据数组首地址
					 NumToRead:要读出数据的个数
* 输    出         : 无
*******************************************************************************/
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
	while(NumToRead)
	{
		*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);	
		NumToRead--;
	}
} 

/*******************************************************************************
* 函 数 名         : AT24CXX_Write
* 函数功能		   : 在AT24CXX里面的指定地址开始写入指定个数的数据
* 输    入         : WriteAddr :开始写入的地址 对24c02为0~255
					 pBuffer  :数据数组首地址
					 NumToRead:要读出数据的个数
* 输    出         : 无
*******************************************************************************/
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
	while(NumToWrite--)
	{
		AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
		WriteAddr++;
		pBuffer++;
	}
}

四 SPI实验

        SPI 接口主要应用在 EEPROM、FLASH、实时时钟、AD 转换器,还有数字信号处理器和数字信号解码器之间。SPI 是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为 PCB 的布局上节省空间,提供方便,正是出于这种简单易用的特性,如今越来越多的芯片集成了这种通信协议,比如 STM32 系列芯片。

        SPI 接口一般使用 4 条线通信,事实上只需 3 条线也可以进行 SPI 通信(单向传输时),其中 3 条为 SPI 总线(MISO、MOSI、SCLK),一条为 SPI 片选信号线(CS)。它们的作用如下:

MISO:主设备输入/从设备输出引脚。主机从这条信号线读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到主机。
MOSI:主设备输出/从设备输入引脚。主机的数据从这条信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机到从机。
SCLK:时钟信号线,用于通信数据同步。它由主机产生,决定了通信的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为 fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。
CS:从设备选择信号线,常称为片选信号线,也称为 NSS 或 CS,以下用 CS 表示。 当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用这 3 条总线;而每个从设备都有独立的这一条 CS 信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条选信号线。I2C 协议中通过设备地址来寻址、选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 CS 信号线来寻址,当主机要选择从设备时,把该从设备的 CS 信号线设置为低电平,该从设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通信。所以 SPI 通信以 NSS 线置低电平为开始信号,以 CS 线被拉高作为结束信号。
与 I2C 总线相比它的缺点是:没有指定的流控制,没有应答机制确认是否接收到数据。
SPI 配置步骤(与IIC不同,这里是直接调用SPI)
(SPI 相关库函数在 stm32f10x_spi.c 和 stm32f10x_spi.h 文件中)
        要使用 SPI 就必须使能它的时钟,STM32103中SPI1 是挂接在 APB2 总线上,而 SPI2 和 SPI3 挂接在 APB1 总线上。而且 SPI 总线接口对应不同的 STM32 引脚,所以还需使能对应引脚的端口时钟,同时配置为复用功能。
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
SPI2 对应的 IO 口前面也介绍了,这里只需使用 SPI 的 3 条总线,片选线使用普通 IO 即可。配置引脚复用映射代码如下
/* SPI 的 IO 口设置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
(2)初始化 SPI,包括数据帧长度、传输模式、MSB 和 LSB 顺序
        使能了 SPI 时钟后,我们就可以对 SPI 相关参数进行配置,包括数据帧长度、传输模式、MSB 和 LSB 顺序等,这些其实都是通过 SPI_CR1 寄存器来设置,库函数中用来初始化 SPI 的函数是:函数中第一个参数是用来确定哪个 SPI,例如 SPI2;第二个参数是一个结构体指针变量,结构体类型是 SPI_InitTypeDef
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
typedef struct
{
        uint16_t SPI_Direction; //设置 SPI 的单双向模式
        uint16_t SPI_Mode; //设置 SPI 的主/从机端模式
        uint16_t SPI_DataSize; //设置 SPI 的数据帧长度,可选 8/16 位
        uint16_t SPI_CPOL; //设置时钟极性 CPOL,可选高/低电平
        uint16_t SPI_CPHA; //设置时钟相位,可选奇/偶数边沿采样
        uint16_t SPI_NSS; //设置 NSS 引脚由 SPI 硬件控制还是软件控制
        uint16_t SPI_BaudRatePrescaler;//设置时钟分频因子
        uint16_t SPI_FirstBit; //设置 MSB/LSB 顺序
        uint16_t SPI_CRCPolynomial; //设置 CRC 校验的表达式
}SPI_InitTypeDef;
参数如下:
SPI_Direction:用于设置 SPI 的通信方向,可设置为双线全双工 (SPI_Direction_2Lines_FullDuplex) , 双线只接收
(SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只发送模式(SPI_Direction_1Line_Tx)。本实验设置的是双线全双工,即 SPI_Direction_2Lines_FullDuplex。
SPI_Mode:用于设置 SPI 工作在主机模式(SPI_Mode_Master)或从机模式 (SPI_Mode_Slave ),这两个模式的最大区别为 SPI 的 SCK 信号线的时序, SCK的时序是由通讯中的主机产生的。若被配置为从机模式, STM32 的 SPI 外设将接受外来的 SCK 信号。本实验设置的是主机模式,即 SPI_Mode_Master。
SPI_DataSize : 用于设置 SPI 通信的数据帧长度,可以选择 8 位 (SPI_DataSize_8b)或者 16 位(SPI_DataSize_16b)。本实验设置的是 8 位,即SPI_DataSize_8b。
SPI_CPOL:用于设置时钟极性,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。本实验我们设置串行同步时钟的空闲状态为高电平所以选择SPI_CPOL_High。
SPI_CPHA:用于设置时钟相位,也就是选择在串行同步时钟的第几个跳变沿(上升或下降)数据被采样,可以为 SPI_CPHA_1Edge(在 SCK 的奇数边沿采集数据) 或 SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据) 。
SPI_NSS : 用于设置 NSS 引脚的使用模式,可以选择为硬件模式 (SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ),在硬件模式中的 NSS 信号由 SPI 硬件自动产生,而软件模式则需要我们使用相应的 GPIO 端口来控制。本实验我们使用软件模式,即 SPI_NSS_Soft。
SPI_BaudRatePrescaler:用于设置波特率分频因子,分频后的时钟即为 SPI 的 SCK 信号线的时钟频率。可设置为 fpclk 的 2、 4、 6、 8、 16、 32、 64、 128 、 256 分 频 。
SPI_FirstBit:用于设置数据传输顺序是 MSB 位在前还是 LSB 位在前
SPI_CRCPolynomial:用于设置 CRC 校验多项式,提高通信可靠性,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值,我们这里大于 1 即可。
(3)使能(开启)SPI
        要让 SPI 正常工作,还需要使能 SPI,通过 SPI_CR1 的第 6 位来设置,库函数中使能 SPI 函数是:
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
比如要使用的是 SPI2,所以调用函数为:
SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设
(4)SPI 数据传输(发送与接收)
        通过上面几个步骤的配置,SPI 已经可以开始通信了,在通信的过程中肯定会有数据的发送和接收,固件库也提供了 SPI 的发送和接收函数。
        SPI 发送数据函数原型为:这个函数很好理解,往 SPIx 数据寄存器写入数据 Data,从而实现发送。
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
        SPI 接收数据函数原型为:此函数非常简单,从 SPIx 数据寄存器中读取接收到的数据。
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
(5)查看 SPI 传输状态
        在 SPI 传输过程中,我们经常要判断数据是否传输完成,发送区是否为空等状态,这是通过函数 SPI_I2S_GetFlagStatus 实现的,此函数原型中,第二个参数是用来选择 SPI 传输过程中判断的标志,对应的标志可在 stm32f10x_spi.h 文件中查找到,使用较多的是发送完成标志
(SPI_I2S_FLAG_TXE)和接收完成标志(SPI_I2S_FLAG_RXNE)。
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
判断发送是否完成的方法是:
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE);
#include "spi.h"

//以下是SPI模块的初始化代码,配置成主机模式 						  
//SPI口初始化
//这里针是对SPI1的初始化
void SPI1_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure; //初始化结构体
	SPI_InitTypeDef  SPI_InitStructure;
	
	/* SPI的IO口和SPI外设打开时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	/* SPI的IO口设置,配置复用功能 */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//设置SPI工作模式:设置为主SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//设置SPI的数据大小:SPI发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		//串行同步时钟的空闲状态为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;	//串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;		//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;		//定义波特率预分频的值:波特率预分频值为256
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
	SPI_Init(SPI1, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
	
	SPI_Cmd(SPI1, ENABLE); //使能SPI外设
	
	SPI1_ReadWriteByte(0xff);//启动传输	
}

//SPI1速度设置函数
//SPI速度=fAPB2/分频系数
//@ref SPI_BaudRate_Prescaler:SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256  
//fAPB2时钟一般为84Mhz:

void SPI1_SetSpeed(u8 SPI_BaudRatePrescaler)
{
	SPI1->CR1&=0XFFC7;//位3-5清零,用来设置波特率
	SPI1->CR1|=SPI_BaudRatePrescaler;	//设置SPI1速度 
	SPI_Cmd(SPI1,ENABLE); //使能SPI1
} 

//SPI1 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节

u8 SPI1_ReadWriteByte(u8 TxData)
{		 			 
 
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);//等待发送区空  
	
	SPI_I2S_SendData(SPI1, TxData); //通过外设SPIx发送一个byte  数据
		
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); //等待接收完一个byte  
 
	return SPI_I2S_ReceiveData(SPI1); //返回通过SPIx最近接收的数据	
 		    
}



//SPI口初始化
//这里针是对SPI2的初始化
void SPI2_Init(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	SPI_InitTypeDef  SPI_InitStructure;
	
	/* SPI的IO口和SPI外设打开时钟 */
    RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
	
	/* SPI的IO口设置 */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //PB13/14/15复用推挽输出 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//设置SPI工作模式:设置为主SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//设置SPI的数据大小:SPI发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;		//串行同步时钟的空闲状态为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;	//串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;		//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;		//定义波特率预分频的值:波特率预分频值为256
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC值计算的多项式
	SPI_Init(SPI2, &SPI_InitStructure);  //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
	
	SPI_Cmd(SPI2, ENABLE); //使能SPI外设
	
	SPI2_ReadWriteByte(0xff);//启动传输	
}

//SPI2速度设置函数
//SPI速度=fAPB1/分频系数
//@ref SPI_BaudRate_Prescaler:SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256  
//fAPB1时钟一般为36Mhz:
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
	SPI2->CR1&=0XFFC7;//位3-5清零,用来设置波特率
	SPI2->CR1|=SPI_BaudRatePrescaler;	//设置SPI速度 
	SPI_Cmd(SPI2,ENABLE); //使能SPI2
} 

//SPI2 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI2_ReadWriteByte(u8 TxData)
{		 			 
 
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);//等待发送区空  
	
	SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个byte  数据
		
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET); //等待接收完一个byte  
 
	return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据	
 		    
}

五 RS485实验(半双工)

        RS485 是半双工通信(2 线制),可以一点对多点进行组网,而且 RS485 是用缆线两端的电压差值来表示传递信号,这与 RS232 电气特性大不一样。RS485 仅仅规定了接受端和发送端的电气特性,并没有规定或推荐任何数据协议(常用MODBUS RTU协议),因此RS485 的协议层可以和 RS232 一样。
RS485 推荐使用在点对点,线型,总线型网络中,不能使用在星型和环型网络。常用芯片为SP3485
代码与之前串口初始化代码基本相同,有以下两点不同之处
1)Pin_3引脚设置了模拟输入和推挽输出两种模式
2)通过控制PGout(3)电平的高低设置485的接收、发送模式
#include "rs485.h"
#include "SysTick.h"

/*******************************************************************************
* 函 数 名         : RS485_Init
* 函数功能		   : USART2初始化函数
* 输    入         : bound:波特率
* 输    出         : 无
*******************************************************************************/  
void RS485_Init(u32 bound)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG|RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA\G时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//使能USART2时钟
	
	/*  配置GPIO的模式和IO口 */
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_2;	//TX-485	//串口输出PA2
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;		  //复用推挽输出
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;	
	GPIO_Init(GPIOA,&GPIO_InitStructure);		/* 初始化串口输入IO */
	
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;	//RX-485	   //串口输入PA3
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;	    //模拟输入
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_3;	//CS-485
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;	   //推挽输出
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOG,&GPIO_InitStructure);
	
	//USART2 初始化设置
	USART_InitStructure.USART_BaudRate = bound;//波特率设置
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
	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(USART2, &USART_InitStructure); //初始化串口2
	
	USART_Cmd(USART2, ENABLE);  //使能串口 2
	
	USART_ClearFlag(USART2, USART_FLAG_TC);
		
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);//开启接受中断

	//Usart2 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority =2;		//子优先级2
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);	//根据指定的参数初始化VIC寄存器、
	
	RS485_TX_EN=0;				//默认为接收模式	
}

/*******************************************************************************
* 函 数 名         : USART2_IRQHandler
* 函数功能		   : USART2中断函数
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/ 
void USART2_IRQHandler(void)
{
	u8 res;	    
	if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)//接收到数据
	{	 	
		res =USART_ReceiveData(USART2);//;读取接收到的数据USART2->DR
		RS485_TX_EN=1;
		delay_ms(1);
		USART_SendData(USART2,res);
		while(USART_GetFlagStatus(USART2,USART_FLAG_TC)!=1);
		delay_ms(2);
		RS485_TX_EN=0;
	}  
	USART_ClearFlag(USART2,USART_FLAG_TC);	
} 	

六 CAN通信实验

        这部分由于笔者会从事于汽车电控的相关知识,因此会进行学习,一般来说CAN会用在汽车内的通信总线,具备多主的特点。而在其他领域 MODBUS RTU/TCP协议则会更加通用

        CAN 通信只具有两根信号线,分别是 CAN_H 和 CAN_L,CAN 控制器根据这两根线上的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。发送方通过使总线电平发生变化,将消息发送给接收方。
        CAN协议的特点:
(1)多主控制。 在总线空闲时,所有单元都可以发送消息(多主控制), 而两个以上的单元同时开始发送消息时, 根据标识符( Identifier 以下称为 ID)决定优先级。 ID 并不是表示发送的目的地址,而是表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时,对各消息 ID 的每个位进行逐个仲裁比较。仲裁获胜(被判定为优先级最高)的单元可继续发送消息,仲裁失利
的单元则立刻停止发送而进行接收工作。
(2) 系统的柔软性。与总线相连的单元没有类似于“地址”的信息。因此在总线上增加单元时,连接在总线上的其它单元的软硬件及应用层都不需要改变。
(3) 通信速度较快,通信距离远。 最高 1Mbps(距离小于 40M),最远可达 10KM(速率低于 5Kbps)。
(4)具有错误检测、错误通知和错误恢复功能。所有单元都可以检测错误 (错误检测功能),检测出错误的单元会立即同时通知其他所有单元(错误通知功能), 正在发送消息的单元一旦检测出错误,会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止(错误恢复功能)。
(5)故障封闭功能。CAN 可以判断出错误的类型是总线上暂时的数据错误(如外部噪声等)还是持续的数据错误(如单元内部故障、驱动器故障、断线等)。 由此功能,当总线上发生持续数据错误时,可将引起此故障的单元从总线上隔离出去
(6)连接节点多。 CAN 总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度,可连接的单元数增加;提高通信速度,则可连接的单元数减少。
        STM32F1 芯片自带 bxCAN 控制器 (Basic Extended CAN),即基本扩展 CAN, 可与 CAN 网络进行交互,它支持 2.0A 和 B 版本的 CAN 协议。旨在以最少的CPU 负载高效管理大量的传入消息。

CAN的帧结构

        CAN 通信主要是通过 5 种类型的帧进行,分别是数据帧、遥控帧、错误帧、过载帧和帧间隔。 其中数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有 11 个位的标识符(ID),扩展格式有 29 个位的 ID。各种帧的用途如图所示:

以数据帧为例

(1)帧起始。表示数据帧开始的段。
(2)仲裁段。表示该帧优先级的段。
(3)控制段。表示数据的字节数及保留位的段。
(4)数据段。数据的内容,一帧可发送 0~8 个字节的数据。
(5)CRC 段。检查帧的传输错误的段。
(6)ACK 段。表示确认正常接收的段。
(7)帧结束。表示数据帧结束的段。

 图中 D 表示显性电平, R 表示隐形电平(下同)。

①.帧起始是由 1 个位的显性电平表示,标准帧和扩展帧相同。

②.仲裁段,表示数据优先级的段,标准帧和扩展帧格式在本段有所区别, 如图所示

        从图中可以看出,标准格式的 ID 有 11 个位。从 ID28 到 ID18 被依次发送。禁止高 7 位都为隐性(禁止设定: ID=1111111XXXX)。扩展格式的 ID 有 29 个位。基本 ID 从 ID28 到 ID18,扩展 ID 由 ID17 到 ID0 表示。基本 ID 和 标 准 格 式 的 ID 相 同 。 禁 止 高 7 位 都 为 隐 性 ( 禁 止 设 定 : 基 本 ID=1111111XXXX)

        其中 RTR 位用于标识是否是远程帧( 0,数据帧; 1,远程帧), IDE 位为标识符选择位( 0,使用标准标识符; 1,使用扩展标识符), SRR 位为代替远程请求位,为隐性位,它代替了标准帧中的 RTR 位。
③.控制段由 6 个位构成,表示数据段的字节数。标准帧和扩展帧的控制段稍有不同,如图所示:

上图中, r0 和 r1 为保留位。保留位必须全部以显性电平发送,但接收方可以接收显性、隐性及其任意组合的电平。DLC 为数据长度码,数据长度码与数据的字节数的对应关系如图所示:

 数据的字节数必须为 0~8 字节。但接收方对 DLC = 9~15 的情况并不视为错误。

④.数据段,该段可包含 0~8 个字节的数据。从 MSB(最高位)开始输出, 标准帧和扩展帧此段相同。如图所示:

⑤.CRC 段,该段用于检查帧传输错误。由 15 个位的 CRC 顺序和 1 个位的 CRC 界定符(用于分隔的位)构成,标准帧和扩展帧此段也是相同的。如图所示:

 

CRC 顺序是根据多项式生成的 CRC 值, CRC 的计算范围包括帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算 CRC 值并进行比较,不一致时会通报错误。
⑥.ACK 段,此段用来确认是否正常接收。由 ACK 槽(ACK Slot)和 ACK 界定符 2 个位构成。标准帧和扩展帧此段也是相同的。如图所示:

        发送单元在 ACK 段发送 2 个位的隐性位。接收到正确消息的单元在 ACK 槽(ACK Slot)发送显性位, 通知发送单元正常接收结束。这称作“发送 ACK” 或者“返回 ACK”。发送 ACK 的是在既不处于总线关闭态也不处于休眠态的所有接收单元中,接收到正常消息的单元(发送单元不发送 ACK)。所谓正常消息是指不含填充错误、格式错误、CRC 错误的消息。

⑦.帧结束,它表示该帧的结束的段,由 7 个位的隐性位构成。标准帧和扩展帧此帧是相同的。如图所示:

CAN配置步骤 

(CAN 相关库函数在 stm32f10x_can.c 和 stm32f10x_can.h 文件中)

(1)使能 CAN 时钟,将对应引脚复用映射为 CAN 功能

        要使用 CAN,首先就是使能它的时钟,我们知道 CAN1 和 CAN2 是挂接在 APB1 总线上的,其发送和接收引脚对应不同的 STM32F1 IO(具体 IO 可以通过数据手册查找,也可以在我们原理图上查找),因此使能 CAN 时钟后,还需要使能对应端口的时钟,并且将其引脚配置为复用功能。因为我们使用的 STM32F103ZET6 芯片只有一个 CAN,即 CAN1,其对应的 IO 是 PA11(CAN1_RX)和 PA12(CAN1_TX)。 所以配置代码如下:

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); //打开CAN1时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //PA端口时钟打开
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;        //PA11       
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;      //上拉输入模式
    GPIO_Init(GPIOA, &GPIO_InitStructure);    

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;        //PA12       
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;      //复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;     //IO口速度为50MHz
    GPIO_Init(GPIOA, &GPIO_InitStructure);

(2)设置 CAN 工作模式、波特率等(结构体)
        使能了 CAN 时钟后,接下来就可以通过 CAN_MCR 寄存器配置其工作模式、波特率大小等参数。库函数中提供了 CAN_Init()函数用来完成这一步骤,函数原型是:
uint8_t CAN_Init(CAN_TypeDef* CANx, CAN_InitTypeDef* CAN_InitStruct);
        函数中第一个参数是用来设置哪个 CAN,例如 CAN1;第二个参数是一个结构体指针变量,结构体类型是 CAN_InitTypeDef,其内包含了 CAN 工作模式及波特率初始化的成员变量。
typedef struct
{
        uint16_t CAN_Prescaler;
        uint8_t CAN_Mode;
        uint8_t CAN_SJW;
        uint8_t CAN_BS1;
        uint8_t CAN_BS2;
        FunctionalState CAN_TTCM; 
        FunctionalState CAN_ABOM; 
        FunctionalState CAN_AWUM; 
        FunctionalState CAN_NART; 
        FunctionalState CAN_RFLM; 
        FunctionalState CAN_TXFP; 
} CAN_InitTypeDef;
参数如下:
CAN_Prescaler:用于设置 CAN 外设的时钟分频,它可控制时间片 tq 的时 间长度,这里设置的值最终会加 1 后再写入 BRP 寄存器位。
CAN_Mode : 用于设置 CAN 的工作模式, 可设置为正常模式 (CAN_Mode_Normal) 、 回 环模式 (CAN_Mode_LoopBack) 、 静默模式 (CAN_Mode_Silent)以及回环静默模式(CAN_Mode_Silent_LoopBack)。本实验使用到的只有正常模式和回环模式。
CAN_SJW:用于置 SJW 的极限长度,即 CAN 重新同步时单次可增加或缩短的最大长度,它可以被配置为 1-4tq(CAN_SJW_1/2/3/4tq)。
CAN_BS1:用于设置 CAN 位时序中的 BS1 段的长度,它可以被配置为 1-16 个 tq 长度(CAN_BS1_1/2/3…16tq)。
CAN_BS2:用于设置 CAN 位时序中的 BS2 段的长度,它可以被配置为 1-8 个 tq 长度(CAN_BS2_1/2/3…8tq)。
CAN_TTCM:用于设置是否使用时间触发功能,ENABLE 为使能,DISABLE 为失能。时间触发功能在某些 CAN 标准中会使用到。
CAN_ABOM:用于设置是否使用自动离线管理(ENABLE/DISABLE),使用自动离线管理可以在节点出错离线后适时自动恢复,不需要软件干预。
CAN_AWUM:用于设置是否使用自动唤醒功能(ENABLE/DISABLE),使能自动唤醒功能后它会在监测到总线活动后自动唤醒。
CAN_NART:用于设置是否使用自动重传功能(ENABLE/DISABLE),使用自动重传功能时,会一直发送报文直到成功为止。
CAN_RFLM:用于设置是否使用锁定接收 FIFO(ENABLE/DISABLE),锁定接收 FIFO 后,若 FIFO 溢出时会丢弃新数据,否则在 FIFO 溢出时以新数据覆盖旧数据。
CAN_TXFP:用于设置发送报文的优先级判定方法(ENABLE/DISABLE),使能时,以报文存入发送邮箱的先后顺序来发送,否则按照报文 ID 的优先级来发送。
​​​
(3)设置 CAN 筛选器
        设置好 CAN 的工作模式及波特率后,我们还需要通过 CAN_FMR 寄存器设置CAN 筛选器,库函数中提供了 CAN_FilterInit()函数来完成这一步骤,函数原型是:
void CAN_FilterInit(CAN_FilterInitTypeDef* CAN_FilterInitStruct);
typedef struct
{
        uint16_t CAN_FilterIdHigh;
        uint16_t CAN_FilterIdLow;
        uint16_t CAN_FilterMaskIdHigh;
        uint16_t CAN_FilterMaskIdLow;
        uint16_t CAN_FilterFIFOAssignment; //
        uint8_t CAN_FilterNumber;
        uint8_t CAN_FilterMode;
        uint8_t CAN_FilterScale;
        FunctionalState CAN_FilterActivation;
} CAN_FilterInitTypeDef;
参数如下:
CAN_FilterIdHigh:用于存储要筛选的 ID,若筛选器工作在 32 位模式, 它存储的是所筛选 ID 的高 16 位;若筛选器工作在 16 位模式,它存储的就是一个完整的要筛选的 ID。
CAN_FilterIdLow:同上一个成员一样,它也是用于存储要筛选的 ID,若筛选器工作在 32 位模式,它存储的是所筛选 ID 的低 16 位;若筛选器工作在 16 位模式,它存储的就是一个完整的要筛选的 ID。
CAN_FilterMaskIdHigh : 用 于 存 储 要 筛 选 的 ID 或 掩 码 。CAN_FilterMaskIdHigh 存储的内容分两种情况,当筛选器工作在标识符列表模式时,它的功能与 CAN_FilterIdHigh 相同,都是存储要筛选的 ID;而当筛选器工作在掩码模式时,它存储的是 CAN_FilterIdHigh 成员对应的掩码,与 CAN_FilterIdLow 组成一组筛选器。
​​​​​​​CAN_FilterMaskIdLow:同上一个成员一样,它也是用于存储要筛选的 ID或掩码,只不过这里对应存储 CAN_FilterIdLow 的成员。
CAN_FilterFIFOAssignment:用于设置当报文通过筛选器的匹配后,该报文会被存储到哪一 个接收 FIFO,它的可选值为 FIFO0 或 FIFO1(CAN_Filter_FIFO0/1)。
CAN_FilterNumber:用于设置筛选器的编号,即使用的是哪个筛选器。CAN 一共有 28 个筛选器,所以它的可输入参数范围为 0-27。
CAN_FilterMode:用于设置筛选器的工作模式,可以设置为列表模式 (CAN_FilterMode_IdList)及掩码模式(CAN_FilterMode_IdMask)。
CAN_FilterScale : 用于设置筛选器的位宽,可以设置为 32 位长 (CAN_FilterScale_32bit)及 16 位长(CAN_FilterScale_16bit)。
CAN_FilterActivation:用于设置是否激活这个筛选器(ENABLE/DISABLE)。
(4)选择 CAN 中断类型,开启中断
        配置好上述 3 个步骤后,CAN 就可以开始工作了,如果要开启 CAN 的接收中断,我们还需要选择它的中断类型并使能。配置 CAN 中断类型及使能的库函数是: 第一个参数用来选择 CAN,第二个参数用来选择 CAN 中断类型,最后一个参数用来使能或者失能 CAN 中断。
CAN 的中断类型很多,可以在 stm32f10x_can.h 文件查找,
void CAN_ITConfig(CAN_TypeDef* CANx, uint32_t CAN_IT, FunctionalState NewState);
(5)CAN 发送和接收消息
        初始化 CAN 相关参数以及筛选器之后,接下来就是发送和接收消息了。库函数中提供了发送和接受消息的函数。
发送消息的函数是这个函数非常好理解,第一个参数用来选择 CAN,比如 CAN1,第二个参数是一个结构体指针变量,结构体类型是 CanTxMsg,其内包含了往邮箱发送的报文信息。
uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);
​​​​​​​
typedef struct
{
        uint32_t StdId;
        uint32_t ExtId;
        uint8_t IDE;
        uint8_t RTR;
        uint8_t DLC;
        uint8_t Data[8];
} CanTxMsg;
参数如下:
StdId:用于存储报文的 11 位标准标识符,范围是 0-0x7FF。
ExtId:用于存储报文的 29 位扩展标识符,范围是 0-0x1FFFFFFF。ExtId 与 StdId 这两个成员哪一个有效要根据下面的 IDE 位配置。
IDE:用于存储扩展标志 IDE 位的值,其值可配置为 CAN_ID_STD 和CAN_ID_EXT。如果为 CAN_ID_STD 时表示本报文是标准帧,使用 StdId 成员存储报文 ID。如果为 CAN_ID_EXT 时表示本报文是扩展帧,使用 ExtId 成员存储报文 ID。
RTR:用于存储报文类型标志 RTR 位的值,当它的值为宏 CAN_RTR_Data 时表示本报文是数据帧;当它的值为宏 CAN_RTR_Remote 时表示本报文是遥控帧,由于遥控帧没有数据段,所以当报文是遥控帧时,下面的 Data[8]成员的内容是无效的。
DLC:用于存储数据帧数据段的长度,其值范围是 0-8,当报文是遥控帧时DLC 值为 0。
Data[8]:用于存储数据帧中数据段的数据。
例如:
CanTxMsg TxMessage;
TxMessage.StdId=0x12; // 标准标识符为 0
TxMessage.ExtId=0x12; // 设置扩展标示符(29 位)
TxMessage.IDE=0; // 使用扩展标识符
TxMessage.RTR=0; // 消息类型为数据帧,一帧 8 位
TxMessage.DLC=8; // 发送两帧信息
for(i=0;i<8;i++)
        TxMessage.Data[8]=msg[i]; // 第一帧信息
CAN_Transmit(CAN1, &TxMessage);
接收消息的函数是:前两个参数很好理解,选择 CAN 和 FIFO。第三个参数是一个结构体指针变量,结构体类型是 CanRxMsg,与 CanTxMsg 比较类似,其内包含了从邮箱接收的报文信息。
void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);
typedef struct
{
        uint32_t StdId;
        uint32_t ExtId;
        uint8_t IDE;
        uint8_t RTR;
        uint8_t DLC;
        uint8_t Data[8];
        uint8_t FMI;
} CanRxMsg;
前面几个成员和 CanTxMsg 结构体内是一样的,在 CanRxMsg 中多了一个成员 FMI,它用于存储筛选器的编号,表示本报文是经过哪个筛选器存储进接收 FIFO 的,可以用它简化软件处理。
(6)CAN 状态获取
        当使用 CAN 进行数据传输时,我们会通过获取 CAN 状态标志来确认是否传输完成,比如说在接收报文时,通过检测标志位获知接收 FIFO 的状态,若收到报文,可调用库函数CAN_Receive 把接收 FIFO 中的内容读取到预先定义的接收类型结构体中,然后再访问该结构体即可利用报文了。
        库函数中提供了很多获取 CAN 状态标志的函数,如 CAN_TransmitStatus() 函数, CAN_MessagePending()函数, CAN_GetFlagStatus()函数等等,大家可以根据需要来调用
#include "can.h"
#include "usart.h"

//CAN初始化
//tsjw:重新同步跳跃时间单元.范围:CAN_SJW_1tq~ CAN_SJW_4tq
//tbs2:时间段2的时间单元.   范围:CAN_BS2_1tq~CAN_BS2_8tq;
//tbs1:时间段1的时间单元.   范围:CAN_BS1_1tq ~CAN_BS1_16tq
//brp :波特率分频器.范围:1~1024;  tq=(brp)*tpclk1
//波特率=Fpclk1/((tbs1+tbs2+1)*brp);
//mode:CAN_Mode_Normal,普通模式;CAN_Mode_LoopBack,回环模式;
//Fpclk1的时钟在初始化的时候设置为36M,如果设置CAN_Mode_Init(CAN_SJW_1tq,CAN_BS2_8tq,CAN_BS1_9tq,4,CAN_Mode_LoopBack);
//则波特率为:36M/((8+9+1)*4)=500Kbps
//返回值:0,初始化OK;
//    其他,初始化失败;
void CAN_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	CAN_InitTypeDef        CAN_InitStructure;
	CAN_FilterInitTypeDef  CAN_FilterInitStructure;
	
#if CAN_RX0_INT_ENABLE 
	NVIC_InitTypeDef  		NVIC_InitStructure;
#endif
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); //打开CAN1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   //PA端口时钟打开
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;		//PA11	   
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; 	 //上拉输入模式
	GPIO_Init(GPIOA, &GPIO_InitStructure);	

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;		//PA12	   
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 	 //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	 //IO口速度为50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//CAN单元设置
   	CAN_InitStructure.CAN_TTCM=DISABLE;	//非时间触发通信模式   
  	CAN_InitStructure.CAN_ABOM=DISABLE;	//软件自动离线管理	  
  	CAN_InitStructure.CAN_AWUM=DISABLE;//睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位)
  	CAN_InitStructure.CAN_NART=ENABLE;	//使用报文自动传送 
  	CAN_InitStructure.CAN_RFLM=DISABLE;	//报文不锁定,新的覆盖旧的  
  	CAN_InitStructure.CAN_TXFP=DISABLE;	//优先级由报文标识符决定 
  	CAN_InitStructure.CAN_Mode= mode;	 //模式设置 
  	CAN_InitStructure.CAN_SJW=tsjw;	//重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1tq~CAN_SJW_4tq
  	CAN_InitStructure.CAN_BS1=tbs1; //Tbs1范围CAN_BS1_1tq ~CAN_BS1_16tq
  	CAN_InitStructure.CAN_BS2=tbs2;//Tbs2范围CAN_BS2_1tq ~	CAN_BS2_8tq
  	CAN_InitStructure.CAN_Prescaler=brp;  //分频系数(Fdiv)为brp+1	
  	CAN_Init(CAN1, &CAN_InitStructure);   // 初始化CAN1
	
	//配置过滤器
 	CAN_FilterInitStructure.CAN_FilterNumber=0;	  //过滤器0
  	CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask; 
  	CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //32位 
  	CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000;32位ID
  	CAN_FilterInitStructure.CAN_FilterIdLow=0x0000;
  	CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000;//32位MASK
  	CAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;
   	CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;//过滤器0关联到FIFO0
  	CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器0
  	CAN_FilterInit(&CAN_FilterInitStructure);//滤波器初始化
	
#if CAN_RX0_INT_ENABLE 
	CAN_ITConfig(CAN1,CAN_IT_FMP0,ENABLE);				//FIFO0消息挂号中断允许.		    

	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;     // 主优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;            // 次优先级为0
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
#endif
}

#if CAN_RX0_INT_ENABLE	//使能RX0中断
//中断服务函数			    
void USB_LP_CAN1_RX0_IRQHandler(void)
{
  	CanRxMsg RxMessage;
	int i=0;
    CAN_Receive(CAN1, 0, &RxMessage);
	for(i=0;i<8;i++)
	printf("rxbuf[%d]:%d\r\n",i,RxMessage.Data[i]);
}
#endif

//can发送一组数据(固定格式:ID为0X12,标准帧,数据帧)	
//len:数据长度(最大为8)				     
//msg:数据指针,最大为8个字节.
//返回值:0,成功;
//		 其他,失败;
u8 CAN_Send_Msg(u8* msg,u8 len)
{	
	u8 mbox;
	u16 i=0;
	CanTxMsg TxMessage;
	TxMessage.StdId=0x12;	 // 标准标识符为0
	TxMessage.ExtId=0x12;	 // 设置扩展标示符(29位)
	TxMessage.IDE=0;		  // 使用扩展标识符
	TxMessage.RTR=0;		  // 消息类型为数据帧,一帧8位
	TxMessage.DLC=len;							 // 发送两帧信息
	for(i=0;i<len;i++)
		TxMessage.Data[i]=msg[i];				 // 第一帧信息          
	mbox= CAN_Transmit(CAN1, &TxMessage);   
	i=0;
	while((CAN_TransmitStatus(CAN1, mbox)==CAN_TxStatus_Failed)&&(i<0XFFF))i++;	//等待发送结束
	if(i>=0XFFF)return 1;
	return 0;		
}

//can口接收数据查询
//buf:数据缓存区;	 
//返回值:0,无数据被收到;
//		 其他,接收的数据长度;
u8 CAN_Receive_Msg(u8 *buf)
{		   		   
 	u32 i;
	CanRxMsg RxMessage;
    if( CAN_MessagePending(CAN1,CAN_FIFO0)==0)return 0;		//没有接收到数据,直接退出 
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);//读取数据	
    for(i=0;i<RxMessage.DLC;i++)
    buf[i]=RxMessage.Data[i];  
	return RxMessage.DLC;	
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值