网站策划建设方案书/百度一下百度搜索百度
嵌入式软件开发学习过程记录,本部分结合本人的学习经验撰写,系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维,为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中的中断
DMA(Direct Memory Access)是单片机中的一种数据传输方式,它可以在不占用处理器资源的情况下,通过直接将数据从外设传输到存储器或者从存储器传输到外设。
在传统的 I/O 操作中,CPU 需要向外设发送读写指令,并不断地查询外设是否已经完成读写操作。这种方式需要占用 CPU 大量的时间和资源,容易造成 CPU 的负载过高,影响系统的稳定性和可靠性。而采用 DMA 传输方式,可以使得 CPU 参与的仅仅是开始和结束的操作,即启动 DMA 传输和等待 DMA 传输完成,从而节省了大量的 CPU 资源。
单片机 DMA 的实现方式有多种,其中最常见的是将该功能集成在芯片内部,由硬件直接控制。在 STM32 系列单片机中,DMA 控制器包括多个通道,每个通道可以连接不同的外设和存储器地址,通过配置相应的寄存器,可以实现数据在外设和存储器之间的直接传输。这样,就能够提高系统的并行性和整体性能,同时降低 CPU 的使用率,提高系统的可靠性。
需要注意的是,使用单片机 DMA 传输时需要对通道进行合理的分配和调度,避免出现数据竞争和冲突等问题,同时需要根据具体的硬件和软件要求进行相应的配置,并且确保 DMA 传输的正确性和可靠性。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_ADC1,ENABLE);
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AN; //模拟输入模式
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
(3)初始化 ADC 参数,包括 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 转换通道数目,根据实际设置。具体的通道数和通道的转换顺序是配置规则序列或注入序列寄存器。
开启 ADC 的库函数void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);开启 ADC1ADC_Cmd(ADC1, ENABLE);//开启 AD 转换器执行复位校准的方法是:ADC_ResetCalibration(ADC1);执行 ADC 校准的方法是:ADC_StartCalibration(ADC1); //开始指定 ADC1 的校准状态每次进行校准之后要等待校准结束。 这里是通过获取校准状态来判断是否校准是否结束。比如while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel,uint8_t Rank, uint8_t ADC_SampleTime);
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5 );
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的 ADC1 的软件转换启动功能
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);
ADC_GetConversionValue(ADC1);
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);
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;//ADCGPIO_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 数模转换实验
STM32F1 DAC 模块是 12 位电压输出数模转换器,它可以配置为 8 位或 12 位模式,
也可以与 DMA 控制器配合使用。
DAC 工作在 12 位模式下,数据可以采 用左对齐或右对齐。
DAC 工作在 8 位模式下,数据只有右对齐方式。
DAC 有两个输出通道,每个通道各有一个转换器。
在 DAC 双通道模式下,每个通道可以单独进行转换;
当两个通道组合在一起同步执行更新操作时,也可以同时进行转换。
DAC 可通过一个输入参考电压引脚 VREF+(与 ADC 共享)来提高转换后的数据精度。
例如要让 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);//初始化
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。
初始化 DAC 后,我们就需要开启它,使能 DAC 输出通道的库函数为:
void DAC_Cmd(uint32_t DAC_Channel, FunctionalState NewState);
DAC_Cmd(DAC_Channel_1, ENABLE); //使能 DAC 通道 1
DAC_SetChannel1Data(DAC_Align_12b_R, 0); //12 位右对齐数据格式设置 DAC 值
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_1GPIO_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=0DAC_InitStructure.DAC_WaveGeneration=DAC_WaveGeneration_None;//不使用波形发生DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude=DAC_LFSRUnmask_Bit0;//屏蔽、幅值设置DAC_InitStructure.DAC_OutputBuffer=DAC_OutputBuffer_Disable ; //DAC1输出缓存关闭 BOFF1=1DAC_Init(DAC_Channel_1,&DAC_InitStructure); //初始化DAC通道1DAC_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的协议层有如下特点
(4)总线的寻址方式

(5)数据传输
在总线的一次数据传送过程中,可以有以下几种组合方式: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 highIIC_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 0000IIC_SDA=1;elseIIC_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();//发送nACKelseIIC_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~255pBuffer :数据数组首地址NumToRead:要读出数据的个数
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{while(NumToRead){*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++); NumToRead--;}
} /*******************************************************************************
* 函 数 名 : AT24CXX_Write
* 函数功能 : 在AT24CXX里面的指定地址开始写入指定个数的数据
* 输 入 : WriteAddr :开始写入的地址 对24c02为0~255pBuffer :数据数组首地址NumToRead:要读出数据的个数
* 输 出 : 无
*******************************************************************************/
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{while(NumToWrite--){AT24CXX_WriteOneByte(WriteAddr,*pBuffer);WriteAddr++;pBuffer++;}
}
四 SPI实验
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 线被拉高作为结束信号。
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);
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 即可。
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
SPI_Cmd(SPI2, ENABLE); //使能 SPI 外设
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);
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工作模式:设置为主SPISPI_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; //定义波特率预分频的值:波特率预分频值为256SPI_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工作模式:设置为主SPISPI_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; //定义波特率预分频的值:波特率预分频值为256SPI_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实验(半双工)
#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 //串口输出PA2GPIO_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 //串口输入PA3GPIO_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-485GPIO_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); //初始化串口2USART_Cmd(USART2, ENABLE); //使能串口 2USART_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;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority =2; //子优先级2NVIC_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->DRRS485_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的帧结构

以数据帧为例
(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)
上图中, r0 和 r1 为保留位。保留位必须全部以显性电平发送,但接收方可以接收显性、隐性及其任意组合的电平。DLC 为数据长度码,数据长度码与数据的字节数的对应关系如图所示:
数据的字节数必须为 0~8 字节。但接收方对 DLC = 9~15 的情况并不视为错误。
CRC 顺序是根据多项式生成的 CRC 值, CRC 的计算范围包括帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算 CRC 值并进行比较,不一致时会通报错误。
发送单元在 ACK 段发送 2 个位的隐性位。接收到正确消息的单元在 ACK 槽(ACK Slot)发送显性位, 通知发送单元正常接收结束。这称作“发送 ACK” 或者“返回 ACK”。发送 ACK 的是在既不处于总线关闭态也不处于休眠态的所有接收单元中,接收到正常消息的单元(发送单元不发送 ACK)。所谓正常消息是指不含填充错误、格式错误、CRC 错误的消息。

CAN配置步骤
(CAN 相关库函数在 stm32f10x_can.c 和 stm32f10x_can.h 文件中)
要使用 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);
uint8_t CAN_Init(CAN_TypeDef* CANx, CAN_InitTypeDef* CAN_InitStruct);
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 的优先级来发送。
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)。
CAN 的中断类型很多,可以在 stm32f10x_can.h 文件查找,void CAN_ITConfig(CAN_TypeDef* CANx, uint32_t CAN_IT, FunctionalState NewState);
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; // 标准标识符为 0TxMessage.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);
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 的,可以用它简化软件处理。
#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;
#endifRCC_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口速度为50MHzGPIO_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_4tqCAN_InitStructure.CAN_BS1=tbs1; //Tbs1范围CAN_BS1_1tq ~CAN_BS1_16tqCAN_InitStructure.CAN_BS2=tbs2;//Tbs2范围CAN_BS2_1tq ~ CAN_BS2_8tqCAN_InitStructure.CAN_Prescaler=brp; //分频系数(Fdiv)为brp+1 CAN_Init(CAN1, &CAN_InitStructure); // 初始化CAN1//配置过滤器CAN_FilterInitStructure.CAN_FilterNumber=0; //过滤器0CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //32位 CAN_FilterInitStructure.CAN_FilterIdHigh=0x0000;32位IDCAN_FilterInitStructure.CAN_FilterIdLow=0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdHigh=0x0000;//32位MASKCAN_FilterInitStructure.CAN_FilterMaskIdLow=0x0000;CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0;//过滤器0关联到FIFO0CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //激活过滤器0CAN_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; // 主优先级为1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 次优先级为0NVIC_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; // 标准标识符为0TxMessage.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;
}