我是靠谱客的博主 美满黑裤,最近开发中收集的这篇文章主要介绍STM32理论 —— ADC、存储、定时器、时钟、中断1. ADC2. 存储3. 定时器4. 时钟5. 中断,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 1. ADC
    • 1.1 ADC相关寄存器
      • 1.1.1 ADC 控制寄存器1(ADC_CR1)
      • 1.1.2 ADC 控制寄存器2(ADC_CR2)
      • 1.1.3 ADC 采样时间寄存器(ADC_SMPR1 和 ADC_SMPR2)
      • 1.1.4 序列寄存器(ADC_SQR1~3)
      • 1.1.5 ADC 规则数据寄存器(ADC_DR)
      • 1.1.6 ADC注入通道数据偏移寄存器(ADC_JOFR)
      • 1.1.7 ADC 状态寄存器(ADC_SR)
    • 1.2 ADC初始化一般步骤
    • 1.3 核心代码
      • 1.3.1 关于ADC电压转换与线性修正算法
    • 1.4 ADC功能框图
      • 1.4.1 电压输入范围
      • 1.4.2 模拟信号输入通道
      • 1.4.3 信号转换顺序
        • 1.4.3.1 规则通道转换顺序
        • 1.4.3.2 注入通道转换顺序
      • 1.4.4 触发源
        • 1.4.5 转换时间
      • 1.4.6 数据寄存器
      • 1.4.7 中断
    • 1.5 ADC芯片ADC0809应用
      • 1.5.1 STM32与芯片连接电路
      • 1.5.2 工作过程
      • 1.5.3 时钟信号
      • 1.5.3 核心代码
    • 1.6 ADC芯片AD5592应用
    • 1.7光强采集芯片BH1750FVI应用
      • 1.7.1 核心代码
      • 1.7.2 BH1750采集速度与MCU采集速度区别
    • 1.8 ADC芯片 - AD7780应用
      • 1.8.1 AD7780 小电阻与大电阻测量方法
    • 1.9 解决ADC 分辨率不够导致测试进度不够的解决方案
  • 2. 存储
    • 2.1 EEPROM芯片 - AT24Cxx
      • 2.1.1 写操作
      • 2.1.2 读操作
      • 2.1.3 核心代码
    • 2.2 延长EEPROM寿命
    • 2.3 关于存储器读写地址
  • 3. 中断
    • 3.1 STM32 的中断
    • 3.1 NVIC 嵌套向量中断控制器
      • 3.1.1 中断优先级分组(中断管理方法)
      • 3.1.2 NVIC 参数结构体
    • 3.2 外部中断/事件控制器 EXTI
    • 3.3 中断服务函数
    • 3.4 STM32 中断优先级寄存器配置及其参考代码
    • 3.5 其他:

1. ADC


ADC(Analog-to-digital converters,模数转换器),STM32上的ADC可独立使用也可双ADC搭配使用以提高采样率,STM32F1系列的ADC是12 位逐次逼近型的,也即是12位精度,它有 18 个通道,可测量 16 个外部和 2 个内部信号源。每个通道的 A/D 转换可以单次、连续、扫描或间断模式执行。ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。
ADC的时钟频率由PCLK2分频产生,在 ADC时钟ADCCLK=14M、采样周期为 1.5 个 ADC 时钟下,STM32 的 ADC 最大的转换速率为 1Mhz,即转换时间为 1us,若ADCCLK超过14M,将导致转换结果的准确度下降。

  • STM32 将 ADC 的转换分为 2 个通道组:规则通道组注入通道组,其中注入通道组相当于中断,在执行规则通道上的转换时,注入通道的转换可打断规则通道的转换, 在注入通道被转换完成之后,规则通道才得以继续转换。STM32F1系列 其 ADC 的规则通道组最多包含 16 个转换,而注入通道组最多包含 4 个通道。它们的转换顺序见1.4.3节

1.1 ADC相关寄存器

1.1.1 ADC 控制寄存器1(ADC_CR1)

Control register 1.在这里插入图片描述

  • SCAN(扫描):用于设置扫描模式,由软件设置和清除,如果设置为 1,则使用扫描模式,如果为 0,则关闭扫描模式。在扫描模式下,由 ADC_SQRxADC_JSQRx 寄存器选中的通道被转换。如果设置了 EOCIEJEOCIE,只在最后一个通道转换完毕后才会产生 EOCJEOC 中断
  • AWDEN:用于使能温度传感器和 Vrefint。
  • DUALMOD(双模式选择):用于设置ADC的操作模式。
    在这里插入图片描述

1.1.2 ADC 控制寄存器2(ADC_CR2)

Control register 2.
在这里插入图片描述

  • ADON(AD开):用于开关 AD 转换器。
  • CONT(连续转换):用于设置是否进行连续转换,1为连续转换,0为单次转换。
  • CAL 和 RSTCAL(计算与重计算):用于AD校准。
  • ALIGN(对齐):用于设置数据对齐。 0为右对齐,1为左对齐。
  • EXTSEL:用于选择启动规则转换组转换的外部事件。其中SWSTART表示软件触发。
    在这里插入图片描述

1.1.3 ADC 采样时间寄存器(ADC_SMPR1 和 ADC_SMPR2)

用于设置通道 0~17 的采样时间,每个通道占用 3 个位。
在这里插入图片描述
在这里插入图片描述
对于采样时间,时间越长,准确度越高,但同时也降低了 ADC 的转换速率。ADC 的转换时间计算公式为:Tcovn=采样时间+12.5 个周期,其中:Tcovn 为总转换时间,采样时间是根据每个通道的 SMP 位的设置来决定。

如:当 ADCCLK=14Mhz 的时候,并设置 采样时间为1.5 个周期,根据上述公式有:Tcovn=1.5+12.5=14 个周期=1us.

1.1.4 序列寄存器(ADC_SQR1~3)

该寄存器总共有 3 个,但功能都一样,这里仅介绍ADC_SQR1.
在这里插入图片描述

  • L[3:0](规则通道序列长度):用于存储规则序列的长度,这里只用了 1 个,所以设置这几个位的值为 0.
  • ** SQ13~16**:存储了规则序列中第 13~16 个通道的编号(0 ~17)。

如:选择的是通道1,就需要在寄存器ADC_SQR3中的最低5为(即SQ1)中设置。

1.1.5 ADC 规则数据寄存器(ADC_DR)

规则序列中的 AD 转化结果都将被存在这个寄存器中,而注入通道的转换结果被保存在 ADC_JDRx 中。

注:该寄存器的数据可以通过 ADC_CR2 的 ALIGN 位设置左对齐还是右对齐。在读取数据的时候要注意。

在这里插入图片描述

1.1.6 ADC注入通道数据偏移寄存器(ADC_JOFR)

该寄存器共有4个,而注入通道本身就只有4个,所以注入通道转换的数据都有固定的存放位置,不会跟规则寄存器那样产生数据覆盖的问题。 ADC_JDRx 是 32 位的,低 16 位有效,高 16 位保留,数据同样分为左对齐和右对齐,具体是以哪一种方式存放,由ADC_CR2 的 11 位 ALIGN 设置。
在这里插入图片描述

1.1.7 ADC 状态寄存器(ADC_SR)

该寄存器保存了 ADC 转换时的各种状态。
在这里插入图片描述

  • ** EOC(转换结束)**:通过判断该位来决定是否此次规则通道的 AD 转换已经完成,如果完成我们就从 ADC_DR 中读取转换结果,否则等待转换完成。

1.2 ADC初始化一般步骤

使用到的库函数引自stm32f10x_adc.cstm32f10x_adc.h 文件中。

以STM32F103ZET中 ADC1 的通道 1 进行 AD 转换为例:

  1. 使用 ADC1 的通道 1 进行 AD 转换: 已知ADC 通道 1 在 PA1 上,所以先要使能 GPIOA 的时钟和 和 ADC1时钟,然后设置 PA1 为模拟输入。使能 GPIOA 和 ADC 时钟用 RCC_APB2PeriphClockCmd 函数,设置 PA1 的输入方式,使用GPIO_Init 函数即可。
  2. 复位 ADC1,同时设置 ADC1 分频因子:开启 ADC1 时钟之后,再复位 ADC1,将 ADC1 的全部寄存器重设为缺省值之后就可通过 RCC_CFGR 设置 ADC1 的分频因子。分频因子要确保 ADC1 的时钟(ADCCLK)不要超过 14Mhz。 这里设置分频因子为 6,时钟为 72/6=12MHz.
//设置分频因子为 6
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
//复位 ADC1
ADC_DeInit(ADC1);
  1. 初始化 ADC1 参数,设置 ADC1 的工作模式以及规则序列的相关信息:ADC相关参数配置,在库函数的ADC_Init中完成。
//ADC_Init的定义
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct)//ADC_Init的第二个入口参数通过结构体变量配置
typedef struct
{
	 uint32_t ADC_Mode;  // ADC工作模式
	 FunctionalState ADC_ScanConvMode; //是否开启扫描
	 FunctionalState ADC_ContinuousConvMode; //是否开启连续转换
	 uint32_t ADC_ExternalTrigConv; //设置启动规则转换组转换的外部事件
	 uint32_t ADC_DataAlign; //设置 ADC 数据对齐方式
	 uint8_t ADC_NbrOfChannel; //设置规则序列的长度
}ADC_InitTypeDef;
  1. 使能 ADC 并校准:使能 AD 转换器,执行复位校准和 AD 校准。
//使能指定的 ADC
ADC_Cmd(ADC1, ENABLE); 
//复位校准
ADC_ResetCalibration(ADC1);
//ADC校准
ADC_StartCalibration(ADC1);
//等待复位校准结束与等待AD校准结束
while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
while(ADC_GetCalibrationStatus(ADC1)); //等待校 AD 准结束
  1. 读取 ADC 值:设置规则序列 1 里面的通道、采样顺序以及通道的采样周期,然后启动 ADC 转换,在转换结束后,读取 ADC 转换结果值。
//设置规则序列通道以及采样周期的函数定义
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel,uint8_t Rank, uint8_t ADC_SampleTime)//如:设置规则序列中的第 1 个转换,同时采样周期为 239.5
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 );
//使能指定的 ADC1 的软件转换启动功能
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
//获取转换 ADC 转换数据
ADC_GetConversionValue(ADC1);
//等待转换结束
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));

1.3 核心代码

void Adc_Init(void)
{ 
	//定义结构体变量
	ADC_InitTypeDef ADC_InitStructure; 
	GPIO_InitTypeDef GPIO_InitStructure;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1 , ENABLE ); //使能 ADC1 通道时钟
	RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置 ADC 分频因子 6 
	//72M/6=12,ADC 最大时间不能超过 14M
	//PA1 作为模拟通道输入引脚 
	GPIO_InitStructure.GPIO_Pin =GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
	GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.1
	
	ADC_DeInit(ADC1); //复位 ADC1,将外设 ADC1 的全部寄存器重设为缺省值
	
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC 独立模式
	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 数据右对齐
	ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC 通道数
	ADC_Init(ADC1, &ADC_InitStructure); //根据指定的参数初始化外设 ADCx 
	
	ADC_Cmd(ADC1, ENABLE); //使能指定的 ADC1
	ADC_ResetCalibration(ADC1); //开启复位校准 
	while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束
	ADC_StartCalibration(ADC1); //开启 AD 校准
	while(ADC_GetCalibrationStatus(ADC1)); //等待校准结束
}
//获得 ADC 值
//ch:通道值 0~3
u16 Get_Adc(u8 ch) 
{
	 //设置指定 ADC 的规则组通道,设置它们的转化顺序和采样时间
	ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //通道 1
	//规则采样顺序值为 1,采样时间为 239.5 周期 
	ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的 ADC1 的软件转换功能
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
	return ADC_GetConversionValue(ADC1); //返回最近一次 ADC1 规则组的转换结果
}
// 用于多次获取 ADC 值,取平均,用来提高准确度
u16 Get_Adc_Average(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	for(t=0;t<times;t++)
	{
		temp_val+=Get_Adc(ch);
		delay_ms(5);
	}
	return temp_val/times;
}
int main(void)
{
	u16 adcx;
	float temp;
	delay_init(); //延时函数初始化 
	NVIC_Configuration(); //设置 NVIC 中断分组 2
	uart_init(9600); //串口初始化波特率为 9600
	Adc_Init(); //ADC 初始化
	//显示提示信息
	while(1)
	{
		adcx=Get_Adc_Average(ADC_CH1,10);
		LCD_ShowxNum(156,130,adcx,4,16,0);//显示 ADC 的值
		temp=(float)adcx*(3.3/4096); // 计算电压值的算法
		adcx=temp;
		LCD_ShowxNum(156,150,adcx,1,16,0);//显示电压值
		temp-=adcx;
		temp*=1000;
		LCD_ShowxNum(172,150,temp,3,16,0X80);
		delay_ms(250);
	}
}

1.3.1 关于ADC电压转换与线性修正算法

  • ADC转换算法:从ADC端口采集而来的模拟值通过ADC转换后得到的数据是一个12位的二进制数,电压转换的工作就是把这个二进制数代表的模拟量用数字表示出来。

如:测量的电压范围是0~3.3V,设ADC采集转换后得到的二进制数是ADC_Value,因为12位ADC在转换时将电压的范围大小(即3.3)分为4096(即2^12)份,所以转换后的二进制数ADC_Value代表的实际电压Voltage算法是:
ADC_Value/Voltage=4096/3300

Voltage=(3300*ADC_Value)/4096

  • 电压转换的线性修正:原理是直线的斜截式方程y=kx+b,考虑硬件电路元器件的参数差异,需加入随时可改的线性修正值,以保证采集值ADC_Value与实际电压Voltage的转换保持在线性关系;

调试时一般只需读取两个不同的电压值,然后计算其直线关系,再设定斜率k和截点b;读取的电压值数据越多,计算的修正值越准确;

  • 参考代码:
u16 ADC_CAL_voltage(void)
{
		u16 adcx;
		float temp;
		float ad_k,ad_b;
		ad_k=IIC_Read_float(ad_k_Add);//从EEPROM中读取线性修正值
		ad_b=IIC_Read_float(ad_b_Add);
		adcx=Get_Adc_Average(ADC_Channel_6,20);//获取并计算10次ADC采样数据平均值
		temp=(float)ad_k*adcx*(3.3/4096)*1000*8.1/3+ad_b;//求出mV,增加线性校准系数,其中1000为放大倍数(从V转换到mV),8.1/3为分压电阻修正值
		adcx=temp;//得到电压值
//		temp-=adcx;
//		temp*=1000;
		delay_Nms(250);
//	  sprintfU3("The ADCVol is %dmVrn@_@",adcx);
	  sprintfU3("%dmVrn@_@",adcx);
	  return adcx;
}
  • 关于分压电阻修正值:如下图,设实际电压值为X,经分压后读取的电压值为Y,则从Y转换到X的算法是:
    X/Y=8.1/3

    X=Y*(8.1/3)
    在这里插入图片描述

1.4 ADC功能框图

在这里插入图片描述

1.4.1 电压输入范围

ADC所能测量的电压范围就是VREF- ≤ VIN ≤ VREF+,若把 VSSA 和 VREF-接地,把 VREF+和 VDDA 接 3V3,得到ADC 的输入电压范围为: 0~3.3V.

1.4.2 模拟信号输入通道

模拟信号通过通道输入到单片机中,单片机经过转换后,将模拟信号输出为数字信号。STM32F1系列中的ADC有着18个通道,其中外部的16个通道已经在框图中标出。
这16个通道对应着不同的GPIO端口,此外ADC1/2/3 还有内部通道: ADC1 的通道 16 连接到了芯片内部的温度传感器, Vrefint 连接到了通道 17。 ADC2 的模拟通道 16 和 17 连接到了内部的 VSS。
ADC的全部通道如示:
在这里插入图片描述

1.4.3 信号转换顺序

1.4.3.1 规则通道转换顺序

规则通道中的转换顺序由三个序列寄存器控制,它们都是32位寄存器。SQR寄存器控制着转换通道的数目和转换顺序。在这里插入图片描述

1.4.3.2 注入通道转换顺序

注入通道的转换也是通过 注入序列寄存器(JSQR寄存器来) 来控制,控制关系如下:
在这里插入图片描述

注:只有当JL=4的时候,注入通道的转换顺序才会按照JSQ1、JSQ2、JSQ3、JSQ4的顺序执行。当JL<4时,注入通道的转换顺序恰恰相反,也就是执行顺序为:JSQ4、JSQ3、JSQ2、JSQ1.

1.4.4 触发源

像通信协议一样,都要规定一个起始信号才能传输信息,ADC也需要一个触发信号来实行模/数转换。

  1. 直接通过配置 控制寄存器2(CR2) 的ADON位(使能AD转换器),写1时开始转换,写0时停止转换。
  2. 通过内部定时器或者外部IO触发AD转换,即可以利用内部时钟让ADC进行周期性的转换,也可以利用外部IO使ADC在需要时转换,具体的触发由控制寄存器CR2决定。

1.4.5 转换时间

DC的每一次信号转换都要一定的时间,转换时间由输入时钟采样周期共同决定。

  • 输入时钟:ADC在STM32中是挂载在APB2总线上的,所以ADC的时钟是由PCLK2(72MHz)经过分频得到的,分频因子由 RCC 时钟配置寄存器RCC_CFGR 的位 [15:14] ADCPRE[1:0]设置,可以是 2/4/6/8 分频,一般配置分频因子为8,即8分频得到ADC的输入时钟频率为9MHz。
  • 采样周期:采样周期建立在输入时钟上,采样周期也即是使用多少个ADC时钟周期来对电压进行采样,采样周期数可通过 ADC采样时间寄存器 ADC_SMPR1 和 ADC_SMPR2 中的 SMP[2:0]位设置。每个通道可以配置不同的采样周期,但最小的采样周期是1.5个周期,即如果想最快时间采样就设置采样周期为1.5.
  • 转换时间转换时间=采样时间+12.5个周期,12.5个周期是固定的,一般设置 PCLK2=72M,经过 ADC 预分频器能分频到最大的时钟只能是 12M,采样周期设置为 1.5 个周期,算出最短的转换时间为 1.17us。

1.4.6 数据寄存器

转换完成后的数据存放在数据寄存器中,且规则通道转换数据注入通道转换数据是分开存放的。

  • 规则数据寄存器:负责存放规则通道转换的数据,在32位寄存器ADC_DR中存放。当使用ADC独立模式(也就是只使用一个ADC,但可以使用多个通道)时,数据存放在低16位中,当使用ADC多模式时高16位存放ADC2的数据。需要注意的是ADC转换的精度是12位,而寄存器中有16个位来存放数据,所以要规定数据存放是左对齐还是右对齐。

当使用多个通道转换数据时,会产生多个转换数据,然而数据寄存器只有一个,多个数据存放在一个寄存器中会覆盖数据导致ADC转换错误,所以我们经常在一个通道转换完成之后就立刻将数据取出来,方便下一个数据存放。一般开启DMA模式将转换的数据,传输在一个数组中,程序对数组读操作就可以得到转换的结果。
DMA介绍

  • 注入数据寄存器:详见1.1.7节

1.4.7 中断

在这里插入图片描述
从框图中可知数据转换完成后可以产生三种中断,这些中断都在*ADC状态寄存器(ADC_SR)*配置:

  • 规则通道转换完成中断(EOCIE):规则通道数据转换完成之后,产生一个中断,可在中断函数中读取规则数据寄存器的值。也是单通道时读取数据的一种方法。
  • 注入通道转换完成中断(JEOCIE):注入通道数据转换完成之后,产生一个中断,并且也可在中断中读取注入数据寄存器的值,达到读取数据的作用。
  • 模拟看门狗事件(AWDIE):当输入的模拟量(电压)不在阈值范围内就会产生看门狗事件,就是用来监视输入的模拟量是否正常。

1.5 ADC芯片ADC0809应用

STM32驱动ADC0809详解
ADC0809 datasheet

1.5.1 STM32与芯片连接电路

在这里插入图片描述

ADC0809引脚STM32引脚GPIO方向
STARTPA2输出
EOCPA3输入
OEPA4输出
CLOCKPA7输出
ALEPA6输出
ADD APA5输出
ADD BPB10输出
ADD CPB11输出
ADC0809_D0(最低位)PA11输入
ADC0809_D1PA12输入
ADC0809_D2PC10输入
ADC0809_D3PC11输入
ADC0809_D4PC12输入
ADC0809_D5PD2输入
ADC0809_D6PB13输入
ADC0809_D7(最高位)PB12输入
  • 时序图:
    在这里插入图片描述

1.5.2 工作过程

  1. 控制与ADD A~ADD C相连的引脚,选择一个模拟输入端;
    在这里插入图片描述

  2. CLOCK端输入一个时钟信号,这里通过STM32的PWM实现此时钟脉冲,脉冲频率100 KHz;

  3. 将ALE由低电平置为高电平,从而将ADD A-ADD C送进的通道锁存,经译码后被选中的通道的模拟量送给内部转换单元;

  4. 给START一个正脉冲。当上升沿时,所有内部寄存器清零。下降沿时,开始进行A/D转换;在转换期间,START保持低电平;

  5. 读取EOC引脚的状态,A/D转换期间,EOC输入低电平;A/D转换结束,EOC引脚输入高电平;

  6. 当A/D转换结束后,将OE设置为1,这时D0-D7的数据便可以读取了。

1.5.3 时钟信号

根据datasheet,芯片时钟信号允许的范围为:
在这里插入图片描述
这个脉冲信号可以采用定时器中断的方式来产生脉冲信号或使用PWM的方式来产生脉冲信号,下面采用PWM的方式,在STM32的引脚中选择了一个带有PWM功能的引脚PA7:TIM3_CH2.
在这里插入图片描述

1.5.3 核心代码

  • AD转换:

因为ADC0809为8位的AD芯片,所以将8位数据中的每一位数据缓存至一个数组中,然后对这个数组中的值求和即为此次AD的采样值。

这里参考电压Vref(+)=+5V ,Vref(-)=0 ,所以8位数的最大值0xFF对应5V,0x00对应0,所以AD采样值和电压值的换算公式为:adc = (float)sum*5/256; .

float get_adc0809()
{
	 int i=0;
	 u8 sum=0;
	 float adc=0; 
	 int AD_DATA[8] = {0};    
	    
	 ADC0809_ALE=0;   
	 ADC0809_START=0; 
	 delay_us(10); 
	 ADC0809_ALE=1;       
	 ADC0809_START=1; 
	 delay_us(10); 
	 ADC0809_ALE=0; 
	 ADC0809_START=0;            //启动AD转换
	    
	 while(0==ADC0809_EOC);      //等待转换结束 
	    
	 ADC0809_OE=1;  
	
	 AD_DATA[0]=ADC0809_D0*1  ;
	 AD_DATA[1]=ADC0809_D1*2  ;
	 AD_DATA[2]=ADC0809_D2*4  ;
	 AD_DATA[3]=ADC0809_D3*8  ;
	 AD_DATA[4]=ADC0809_D4*16 ;
	 AD_DATA[5]=ADC0809_D5*32 ;
	 AD_DATA[6]=ADC0809_D6*64 ;
	 AD_DATA[7]=ADC0809_D7*128 ;
	    
	 ADC0809_OE=0; 
	 
	 for(i=0; i<8; i++)
	 {
		  sum += AD_DATA[i];
	 }
	    
	 adc = (float)sum*5/256;
	 printf("sum=%d  ad=%0.2f Vrn",sum,adc);
	    
	 return adc;
}

  • PWM信号初始化:
//arr为重载值
//psc为预分频系数
void Clock_PWM_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef       TIM_OCInitStructure;
    GPIO_InitTypeDef  GPIO_InitStructure;
    
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); 
    RCC_APB1PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure); 

    TIM_DeInit(TIM3);

    /* Time Base configuration */
    TIM_TimeBaseStructure.TIM_Period            = arr;
    TIM_TimeBaseStructure.TIM_Prescaler         = psc;
    TIM_TimeBaseStructure.TIM_CounterMode       = TIM_CounterMode_Up;
    TIM_TimeBaseStructure.TIM_ClockDivision     = 0;
    TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;

    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    TIM_OCInitStructure.TIM_OCMode             = TIM_OCMode_PWM2;
    TIM_OCInitStructure.TIM_OutputState        = TIM_OutputState_Enable;  
    TIM_OCInitStructure.TIM_Pulse                   = 0; 
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC2Init(TIM3, &TIM_OCInitStructure);    //TIM3_CH2

    TIM_CtrlPWMOutputs(TIM3, ENABLE);

    TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
 
    TIM_ARRPreloadConfig(TIM3, ENABLE);
    
    TIM_Cmd(TIM3, ENABLE);
    
    TIM_SetCompare2(TIM3,arr/2);
}

main函数中调用如下:

Clock_PWM_Init(720-1,0);    //PWM频率=72000/720 = 100Khz

  • 代码运行:
    在这里插入图片描述

1.6 ADC芯片AD5592应用

//代码索引:
void AD5592_Init(void);
void AD5592_IO_Config(void);
void AD5592_Write(uint16_t data_temp);
uint16_t AD5592_Read(u8 ch);
// SPI引脚定义
#define AD5592_CS       PAout(5)   
#define AD5592_CLK      PAout(7)
#define AD5592_DIN      PAout(8)

#define AD5592_DOUT     PAin(11)
/***************************************************************************
** 函数名称   :   AD5592_Init
** 功能描述   :  	AD5592芯片初始化
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210408
** 说    明   :		无
***************************************************************************/
void AD5592_Init(void)
{
   AD5592_Write(0X7DAC);  //芯片复位
	 delay_Xus1(250);       // 延时250us
   AD5592_IO_Config();    //SPI引脚配置
   AD5592_Write(0X20FF);  //配置所有端口为ADC输入
   AD5592_Write(0X5AFF);  //开启基准电压源V_REF
	 delay_Xus1(200);
}
/***************************************************************************
** 函数名称   :   AD5592_IO_Config
** 功能描述   :  	AD5592 SPI通信引脚IO配置
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210408
** 说    明   :		注意芯片的输出SDO对应MCU的输入,芯片的输入SDI对应MCU的输出
***************************************************************************/
void AD5592_IO_Config(void)
{
		GPIO_InitTypeDef GPIO_InitStructure;
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
		
		// 输入配置
		// SDO引脚
		GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;   
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;                            
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
		GPIO_Init(GPIOA,&GPIO_InitStructure); 
	  
		// 输出配置
		// SYNC引脚
		GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;   
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;                            
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
		GPIO_Init(GPIOA,&GPIO_InitStructure);
		// SCLK引脚
	  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;   
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;                            
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
		GPIO_Init(GPIOA,&GPIO_InitStructure);
		// SDI引脚
	  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;   
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;                            
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	
		GPIO_Init(GPIOA,&GPIO_InitStructure);
}
/***************************************************************************
** 函数名称   :   AD5592_Write
** 功能描述   :  	AD5592 写数据
** 输入变量   :   data_temp :要向AD5592寄存器写入的数据
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  20210408
** 说    明   :		无
***************************************************************************/
void AD5592_Write(uint16_t data_temp)
{
//	u8 t = 0;
//  AD5592_CS = 1;
//	delay_Xus1(10);
//	AD5592_CS = 0;
//	delay_Xus1(5);	
//	AD5592_CLK = 0;
//	for(t=0;t<16;t++)
//  {              	        
//	  if(((data_temp&0x8000)>>15)==1){AD5592_DIN = 1;}  	  
//	  else {AD5592_DIN = 0;}	  	   
//	  AD5592_CLK = 0;
//	  delay_Xus1(5); 
//	  AD5592_CLK = 1;
//	  delay_Xus1(5);
//		data_temp<<=1;      
//  }
//	AD5592_CLK = 0;
//	delay_Xus1(5); 
//	AD5592_CLK = 1;
//	delay_Xus1(5);
   
  u8 t = 0;
  AD5592_CS = 1;
	delay_Xus1(20);
	AD5592_CS = 0;
	delay_Xus1(20);	

	for(t=0;t<16;t++)
  {     
		 AD5592_CLK = 1;
		 delay_Xus1(20);	
		 if(((data_temp&0x8000)>>15)==1){AD5592_DIN = 1;}  	  
		 else {AD5592_DIN = 0;}	  	 
		 delay_Xus1(20); 
		 AD5592_CLK = 0;
		 delay_Xus1(20); 
		 data_temp<<=1;      
  }
	AD5592_CLK = 0;
	delay_Xus1(20); 
	AD5592_CLK = 1;
	delay_Xus1(20);
  AD5592_CS = 1;
	delay_Xus1(20);
}
/***************************************************************************
** 函数名称   :   AD5592_Read
** 功能描述   :  	AD5592 读数据
** 输入变量   :   ch :选择要读取的芯片端口(ADC)
** 返 回 值   :  	返回读取的寄存器值
** 最后修改人 :   xxx
** 最后更新日期:  20210408
** 说    明   :		注意返回值只是寄存器的值,而不是目标电压值
***************************************************************************/
uint16_t AD5592_Read(u8 ch)
{
	uint16_t value_temp = 0;
	uint16_t data_temp = 0;
	u8 t=0;
	if(ch==0){AD5592_Write(0x1001);}  //选择AD0转换
	if(ch==1){AD5592_Write(0x1002);}  //选择AD1转换
	if(ch==2){AD5592_Write(0x1004);}  //选择AD2转换
	if(ch==3){AD5592_Write(0x1008);}  //选择AD3转换
	if(ch==4){AD5592_Write(0x1010);}  //选择AD4转换
	if(ch==5){AD5592_Write(0x1020);}  //选择AD5转换
	if(ch==6){AD5592_Write(0x1040);}  //选择AD6转换
	if(ch==7){AD5592_Write(0x1080);}  //选择AD7转换
	
	//等待第二个始终周期
	AD5592_CS = 1;
	delay_Xus1(20);
	AD5592_CS = 0;
	delay_Xus1(20);	
	for(t=0;t<16;t++)
  {              	        
	  AD5592_CLK = 1;
	  delay_Xus1(20); 
	  AD5592_CLK = 0;
	  delay_Xus1(20);
  }
	AD5592_CLK = 1;
  delay_Xus1(20); 
  AD5592_CS = 1;
  delay_Xus1(20);
  AD5592_CS = 0;
  delay_Xus1(20);
	
	for(t=0;t<16;t++)
	{				
		AD5592_CLK = 0;
		delay_Xus1(20); 
		AD5592_CLK = 1;
		delay_Xus1(20);
		if(AD5592_DOUT==1)value_temp = value_temp|0x01;
		value_temp = value_temp<<1;
	}
   AD5592_CS = 1;
   delay_Xus1(20);
   
   //转换结果取低12位
  value_temp = value_temp&0xfff;		 //转换结果取低12位
  return value_temp;    //注意此结果只是寄存器的值,还要根据公式计算出实际的电压值;
}

1.7光强采集芯片BH1750FVI应用

在这里插入图片描述
上图是BH1750芯片的通信序列图,其可分为以下几个步骤:

  1. 发送上电命令:上电命令是0x01
  2. 发送测量命令:以发送的测量命令是“连续高分辨率测量(0x10)”为例,先是“起始信号(ST)”,接着是“器件地址+读写位”,然后是应答位,紧接着就是测量的命令“00010000”,然后应答,最后是“结束信号(SP)”。(相比于OPT3001的写入过程,BH1750少了一个发送寄存器地址的步骤,因为它只有一个寄存器,所以就没必要了)
  3. 等待测量结束:测量的时间手册上有,高分辨率连续测量需要等待的时间最长,手册上面写的是平均120ms,最大值180ms,所以为了保证每次读取到的数据都是最新测量的,程序上面可以延时200ms以上。
  4. 读取数据:先是“起始信号(ST)”,接着是“器件地址+读写位”,然后是应答位,紧接着接收1个字节的数据(单片机在这个时候要把SDA引脚从输出改成输入),然后给BH1750发送应答,继续接收1个字节数据,然后不应答(因为接收的数据只有2个字节,收完就可以结束通讯),最后是“结束信号(SP)”。
  5. 计算结果光照强度 =(寄存器值[15:0] * 分辨率) / 1.2 (单位:勒克斯lx) . 因为从BH1750寄存器读出来的是2个字节的数据,先接收的是高8位[15:8],后接收的是低8位[7:0],所以需要先把这2个字节合成一个数,然后乘上分辨率,再除以1.2即可得到光照值。

例如:读出来的第1个字节是0x12(0001 0010),第2个字节是0x53(0101 0011),那么合并之后就是0x1253(0001
0010 0101 0011),换算成十进制也就是4691,乘上分辨率(1),再除以1.2,最后等于3909.17 lx。

BH1750所有指令如下图:
在这里插入图片描述


1.7.1 核心代码

// 代码索引:
void BH1750_StartSwitch(void);
double BH1750_ReadBasicData(void);
uint16_t BH1750_ReadData(void);
uint16_t BH1750_ReadData(void);
void BH1750_Power_On(void);
void BH1750_Clr_Data(void);
void BH1750_Mode_set(void);
void Strobe_led_state(void);
void Test_LED_Start(void);
void Test_LED(void);
void Read_LED(void);
void ReadLEDlightValue(void);
void Read_BH1750_Data(void);
void Clear_BH1750_Data(void);

参数定义:
#include "public.h"
extern u8 hsg_flag;
u32 SetLux;
uint32_t ALS_Value[5000];
u16 ALS_num;
u8  ALS_Read_Time;
u16	ALS_Delay_Time;
u8 GetLightVal;
double ALS_k=1.0;
double ALS_b=0.0;
double ALS_Cool_k=1.0;
double ALS_Cool_b=0.0;
double ALS_Warm_k=1.0;
double ALS_Warm_b=0.0;
extern u8 IIC_Channel;

extern uint16_t ALS_READ_TIME;
extern uint16_t ALS_DELAY_TIME;
extern uint32_t  SetLux;

void BH1750_Power_On(void);
void BH1750_Clr_Data(void);
void BH1750_Mode_set(void);
void BH1750_StartSwitch(void);
uint16_t BH1750_ReadData(void);
uint32_t  LuxValue;
/***************************************************************************
** 函数名称   :   BH1750_StartSwitch
** 功能描述   :  	BH1750开始工作
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		此函数可以在采光开始时使用,若使用过程中出现采光不及时问题,可改为程序初始化时使用。
***************************************************************************/
void BH1750_StartSwitch(void) 
{
	BH1750_Power_On();
	BH1750_Mode_set();
}
/***************************************************************************
** 函数名称   :   BH1750_ReadBasicData
** 功能描述   :  	BH1750读取基本数据
** 输入变量   :   无
** 返 回 值   :  	读到的数据
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		此函数未使用
***************************************************************************/
double BH1750_ReadBasicData(void)
{
	u8 flag = 0;
	u8 t=0;
	u8 Data_H=0,Data_L=0;
	float  DATA=0.0;
	u8 a,b,c,d,e,f,g,h;
	u8 a1,b1,c1,d1,e1,f1,g1,h1;
	IIC_Channel = 2;
	do
	{
		IIC_Start();
		IIC_WRITE_BYTE(0x47);	 //写从属地址 
		if(IIC_Recelve_Ack()==0)
		{
			Data_H = IIC_Read_Byte(1);
			Data_L = IIC_Read_Byte(0);
			flag = 0;
		}
		else {flag = 1;t++;}		
	}
  while((flag==1)&&(t<250));        //  1111 1111 & 1000 0000 = 1000 0000  
  IIC_Stop();	

  
//	sprintfU3("DATA_H: %drn",Data_H);
//	sprintfU3("DATA_L: %drn",Data_L);
	
	a = (Data_H&0X80)>>7;
	b = (Data_H&0X40)>>6;
	c = (Data_H&0X20)>>5;
	d = (Data_H&0X10)>>4;
	e = (Data_H&0X08)>>3;
	f = (Data_H&0X04)>>2;
	g = (Data_H&0X02)>>1;
	h = Data_H&0X01;
	//
	a1 = (Data_L&0X80)>>7;
	b1 = (Data_L&0X40)>>6;
	c1 = (Data_L&0X20)>>5;
	d1 = (Data_L&0X10)>>4;
	e1 = (Data_L&0X08)>>3;
	f1 = (Data_L&0X04)>>2;
	g1 = (Data_L&0X02)>>1;
	h1 = Data_L&0X01;
	
	DATA = pow(2*a,15.0)+pow(2*b,14.0)+pow(2*c,13.0)+pow(2*d,12.0)+pow(2*e,11.0)+pow(2*f,10.0)+pow(2*g,9.0)+pow(2*h,8.0)+
	       pow(2*a1,7.0)+pow(2*b1,6.0)+pow(2*c1,5.0)+pow(2*d1,4.0)+pow(2*e1,3.0)+pow(2*f1,2.0)+pow(2*g1,1.0)+h1;
	
	DATA = DATA/1.2;

	return (DATA);
}
/***************************************************************************
** 函数名称   :   BH1750_ReadData
** 功能描述   :  	BH1750读取数据
** 输入变量   :   无
** 返 回 值   :  	读到的数据
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
uint16_t BH1750_ReadData(void)
{
	u8 t=0;
	u8 flag = 0;
	u8 Data_H=0,Data_L=0;
	float  DATA=0.0;
//	u8 a,b,c,d,e,f,g,h;
//	u8 a1,b1,c1,d1,e1,f1,g1,h1;
	IIC_Channel = 2;
	do
	{
		IIC_Start();
		IIC_WRITE_BYTE(0x47);	 //写从属地址 
		if(IIC_Recelve_Ack()==0)
		{
			Data_H = IIC_Read_Byte(1);
			Data_L = IIC_Read_Byte(0);
			flag = 0;
		}
		else {flag = 1;	t++;}	
	}
  while((flag==1)&&t<250);        //  1111 1111 & 1000 0000 = 1000 0000  
  IIC_Stop();	

  
//	sprintfU3("DATA_H: %drn",Data_H);
//	sprintfU3("DATA_L: %drn",Data_L);
	
//	a = (Data_H&0X80)>>7;
//	b = (Data_H&0X40)>>6;
//	c = (Data_H&0X20)>>5;
//	d = (Data_H&0X10)>>4;
//	e = (Data_H&0X08)>>3;
//	f = (Data_H&0X04)>>2;
//	g = (Data_H&0X02)>>1;
//	h = Data_H&0X01;
//	//
//	a1 = (Data_L&0X80)>>7;
//	b1 = (Data_L&0X40)>>6;
//	c1 = (Data_L&0X20)>>5;
//	d1 = (Data_L&0X10)>>4;
//	e1 = (Data_L&0X08)>>3;
//	f1 = (Data_L&0X04)>>2;
//	g1 = (Data_L&0X02)>>1;
//	h1 = Data_L&0X01;
	
	//DATA = pow(2*a,15.0)+pow(2*b,14.0)+pow(2*c,13.0)+pow(2*d,12.0)+pow(2*e,11.0)+pow(2*f,10.0)+pow(2*g,9.0)+pow(2*h,8.0)+
	 //      pow(2*a1,7.0)+pow(2*b1,6.0)+pow(2*c1,5.0)+pow(2*d1,4.0)+pow(2*e1,3.0)+pow(2*f1,2.0)+pow(2*g1,1.0)+h1;
	
	//DATA = DATA/1.2*CoefficientLight;
	DATA = Data_H<<8|Data_L;
	DATA=DATA;
	return ((uint16_t)DATA);
}
/***************************************************************************
** 函数名称   :   BH1750_Power_On
** 功能描述   :  	给BH1750芯片通电
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void BH1750_Power_On(void)
{
  u8 flag = 0;
	u8 t=0;
	IIC_Channel = 2;
	do
	{
    IIC_Start();
    IIC_WRITE_BYTE(0x46);	 //写从属地址 
    if(IIC_Recelve_Ack()==0)
	{
		IIC_WRITE_BYTE(0x01);	  //上电指令
		if(IIC_Recelve_Ack()==0)
		{
			flag = 0;
		}
		else {flag = 1;t++;}
    }
    else {flag = 1;t++;}		
  }
	 while((flag==1)&&t<250); 
	IIC_Stop();
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
}
/***************************************************************************
** 函数名称   :   BH1750_Clr_Data
** 功能描述   :  	清除BH1750芯片中的数据
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		此函数暂未使用
***************************************************************************/
void BH1750_Clr_Data(void)
{
  u8 flag = 0;
	u8 t=0;
	IIC_Channel = 2;
	do
	{
    IIC_Start();
    IIC_WRITE_BYTE(0x46);	 //写从属地址 
    if(IIC_Recelve_Ack()==0)
		{
      IIC_WRITE_BYTE(0x07);	  //数据寄存器重置命令
			if(IIC_Recelve_Ack()==0)
			{
         flag = 0;
      }
			else {flag = 1;t++;}
    }
    else {flag = 1;t++;}		
  }
	while((flag==1)&&t<250); 
	IIC_Stop();
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
}
/***************************************************************************
** 函数名称   :   BH1750_Mode_set
** 功能描述   :  	BH1750芯片参数设置
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void BH1750_Mode_set(void)
{
  u8 flag = 0;
	u8 t=0;
	IIC_Channel = 2;
	do
	{
    IIC_Start();
    IIC_WRITE_BYTE(0x46);	 //写从属地址 
    if(IIC_Recelve_Ack()==0)
	{
		IIC_WRITE_BYTE(0x13);	  //连续低分辨率模式
		if(IIC_Recelve_Ack()==0)
		{
			flag = 0;
		}
		else {flag = 1;t++;}
    }
    else {flag = 1;t++;}	
  }
	while((flag==1)&&t<250); 
	IIC_Stop();
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
	delay_Nus(250);
}
/***************************************************************************
** 函数名称   :   Strobe_led_state
** 功能描述   :  	读取光强状态并返回信息
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void Strobe_led_state(void)
{
    uint8_t i=0;     
	  uint32_t luxVal=0;
	  uint32_t luxresult=0;
	  IIC_Channel=2;
	  SetLux = IIC_Read_4Byte(Lux_Add,1);
	  ALS_Read_Time = IIC_Read_2Byte(Lux_Time_Add,1);
	  ALS_Delay_Time = IIC_Read_2Byte(Lux_Delay_Add,1);
		BH1750_StartSwitch(); 
		delay_ms(100);
		delay_ms(ALS_Delay_Time);
	if(h_flag==H_B)
	{
		printfU4("S Led Statern");
	}
     for(i=0;i<ALS_Read_Time;i++)
	  {    
			luxVal=BH1750_ReadData();
			luxVal=ALS_k*luxVal+ALS_b;
			delay_ms(ALS_Delay_Time);
			sprintfU4("%drn",luxVal);
			if(luxresult<luxVal) luxresult=luxVal;		
	  }
	  if(h_flag==S_F)
	  {
	//	  sprintfU4("Passrn@_@");
	 // }
	 // else
	  //{
		if(luxresult>=SetLux)
		{
			sprintfU4("S LED On %d Passrn@_@",luxresult);
		}
		else
		{ 
			sprintfU4("S LED Off %d Passrn@_@",luxresult);
		}
	}
}
/***************************************************************************
** 函数名称   :   Test_LED_Start
** 功能描述   :  	开始读取光强
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void Test_LED_Start(void)
{
	ALS_num=0;
	memset(ALS_Value,0,sizeof(ALS_Value));
	if(GetLightVal<=0)
	{
		LuxValue=0;
		BH1750_StartSwitch(); 
		delay_ms(150);
		GetLightVal=1;
	}
	printfU4("Test Led Start Passrn@_@");
}	

/***************************************************************************
** 函数名称   :   Test_LED
** 功能描述   :  	处理光强数据
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void Test_LED(void)
{
	u16 i,j;
	u32 temp;
	GetLightVal=0;
	if(ALS_num>5)
	{
		for(i=0;i<ALS_num-1;i++)
		{
			for(j=0;j<ALS_num-i;j++)
			{
			  if(ALS_Value[j]<ALS_Value[j+1])
				{
					temp = ALS_Value[j];
					ALS_Value[j] = ALS_Value[j+1];
					ALS_Value[j+1] = temp;
				}
			}
		}
	LuxValue=(ALS_Value[0]+ALS_Value[1]+ALS_Value[2]+ALS_Value[3]+ALS_Value[4])/5;
	}
	else
	{
		//ALS_Value[0]=0;
		LuxValue=0;		
	}	
	if((LuxValue>SetLux))
	{
		printfU4("Test LED Passrn@_@");
	
	}
	else	
	{
		printfU4("Test LED Failrn@_@");
	}
}
/***************************************************************************
** 函数名称   :   Read_LED
** 功能描述   :  	显示光强数据
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void Read_LED(void)
{
		GetLightVal=0;
	//sprintfU4("Read LED %d Passrn@_@",LuxValue);
	//LuxValue=ALS_k*LuxValue+ALS_b;
	if(ALS_num>5)	sprintfU4("Read Led OK, the value is: %d luxrn@_@",LuxValue);
	else sprintfU4("Read Led OK, the value is: %d luxrn@_@",ALS_Value[0]);
}
/***************************************************************************
** 函数名称   :   ReadLEDlightValue
** 功能描述   :  	扫描光强
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		此函数应该放在主函数中的循环中
***************************************************************************/
void ReadLEDlightValue(void)
{
    uint32_t Tling=0;
    if(GetLightVal==1)
    {
		Tling=BH1750_ReadData();
	//	sprintfU4(": %d luxrn@_@",Tling);
		if(Tling>0)
		{
			ALS_Value[ALS_num++]=BH1750_ReadData();
		}
		else
		{
			ALS_Value[0]=0;		
		}
		if(ALS_Delay_Time>0)delay_ms(ALS_Delay_Time);// MCU采集延迟速度
		if(ALS_num>5000){GetLightVal=0;}
//        if(Tling>LuxValue)
//        {
//            LuxValue=ALS_k*Tling;
//        }
    }
}
/***************************************************************************
** 函数名称   :   Read_BH1750_Data
** 功能描述   :  	打印BH1750读取到的数据
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void Read_BH1750_Data(void)
{
	u16 i;
	if(ALS_num>0)
	{
		for(i=0;i<ALS_num;i++)
		{
			sprintfU4("ALS_Value[%d] = %drn",i,ALS_Value[i]);
		}
	}
	else printfU4("All BH1750 Data =0rn");	
	printfU4("Read BH1750 Data Passrn@_@");	
}
/***************************************************************************
** 函数名称   :   Clear_BH1750_Data
** 功能描述   :  	清零BH1750读取到的数据
** 输入变量   :   无
** 返 回 值   :  	无
** 最后修改人 :   xxx
** 最后更新日期:  xxx
** 说    明   :		
***************************************************************************/
void Clear_BH1750_Data(void)
{
	memset(ALS_Value,0,sizeof(ALS_Value));
	ALS_num=0;
	printfU4("Clear BH1750 Data Passrn@_@");
}

1.7.2 BH1750采集速度与MCU采集速度区别

BH1750对于不同的分辨率有不同的采样速率,比如连续低分辨率模式下最低采样速率为16ms,最高24ms,即在该模式下芯片最少每16ms更新一个数据。但是对于MCU从BH1750中读取数据的速度是根据程序而定的,在上述核心代码中的 ReadLEDlightValue()函数中,有对MCU采样延时的功能。

比如MCU与BH1750同时开始各自的采样操作,MCU每1ms向BH1750读取一组数据,那么最少有16组数据是重复值,因为在此期间BH1750没有完成新的采样操作,即MCU一直读取的都是BH1750没有更新的值。

1.8 ADC芯片 - AD7780应用

datasheet:https://atta.szlcsc.com/upload/public/pdf/source/20200709/C651541_5E3E20831A51574C2A25451AAEBCECD9.pdf

AD7780 是ADI 24位ADC,其中最高为符号位,剩余23位位数据位,则有效数据位为 2^23-1 = 8388607 = 7F FFFF,即ADC 数据满量程为83886087 = 7F FFFF .

假设加载在AD7780 上的参考电压为2.5V,那么测量输入AIN+、AIN- 端电压可以知道ADC 输入读取的模拟信号电压,若AIN+、AIN- 端电压为2.5V,表示已达满量程状态。

在这里插入图片描述


  1. 芯片初始化与参数定义
#define AD7780_CLK(n)			{if(n)GPIO_SetBits(GPIOA, GPIO_Pin_15);else GPIO_ResetBits(GPIOA, GPIO_Pin_15); } // AD7780 CLK 时钟引脚
#define AD7780_RST(n)			{if(n)GPIO_SetBits(GPIOA, GPIO_Pin_12);else GPIO_ResetBits(GPIOA, GPIO_Pin_12); } // AD7780 PDRST 复位引脚
#define AD7780_DATA				GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_10) // AD7780 DOUT/RDY 数据输出/数据准备完毕引脚

// #define AD7780_ADIN(n)		{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_12);else GPIO_ResetBits(GPIOC, GPIO_Pin_12);} // 无实际连接
#define AD7780_DCRSW(n)		{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_8);else GPIO_ResetBits(GPIOC, GPIO_Pin_8);} // ET4/ET6 继电器(0关1开)
#define AD7780_LSCSDCR(n)	{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_7);else GPIO_ResetBits(GPIOC, GPIO_Pin_7);}

#define CSDCR_SW1(n)			{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_11);else GPIO_ResetBits(GPIOC, GPIO_Pin_11);}
#define CSDCR_SW2(n)			{if(n)GPIO_SetBits(GPIOD, GPIO_Pin_1);else GPIO_ResetBits(GPIOD, GPIO_Pin_1);}


extern u8 curr_Cyclone;
//float ResValue =0;
//float Res_OffValue = 0;
__IO float Res_mAValue = 50;
__IO float Res_mVValue = 2526;
__IO float DCR_Line_K = 1;
__IO float DCR_Line_B = 0;
__IO float SRDCR_Line_K = 1;
__IO float SRDCR_Line_B	= 0;
//Cs DCR
__IO float resRefValue[3]= {499,10000,10000000};


void AD7780_IO_init(void)
{
    u8 *p;
    u8 temp = 0;
    GPIO_InitTypeDef GPIO_InitStructure;	//GPIO
    /* 打开GPIO时钟 */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA|RCC_AHB1Periph_GPIOB|RCC_AHB1Periph_GPIOC|RCC_AHB1Periph_GPIOD, ENABLE);
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_15;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_5;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_11|GPIO_Pin_12;
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
    GPIO_Init(GPIOD, &GPIO_InitStructure);
	
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    AD7780_ADIN(1);//OFF SIGNAL IN
    //AD7780_GAIN(1);
    AD7780_DCRSW(0);//OFF DCR FUCTION
    AD7780_LSCSDCR(0);//DCR CONNECT TO LS
    CSDCR_SW1(0);
    CSDCR_SW2(0);

    temp = AT24CXX_ReadOneByte(AD7780_FLAG_Addr);
    if(temp==0x0A)
    {
        AD7780_ReadConfig();
    }
    else
    {
        CLearAD7780Config();
        AD7780_WriteConfig();
    }
    AD7780_Reset();
}

1.8.1 AD7780 小电阻与大电阻测量方法

一般小电阻采用恒流源测试精度更高; 大电阻采用分压电路 + 分挡位测试精度更高、范围更大;

在这里插入图片描述

  1. 小电阻恒流源方法测试代码
#define AD7780_CLK(n)			{if(n)GPIO_SetBits(GPIOA, GPIO_Pin_15);else GPIO_ResetBits(GPIOA, GPIO_Pin_15); }
#define AD7780_RST(n)			{if(n)GPIO_SetBits(GPIOA, GPIO_Pin_12);else GPIO_ResetBits(GPIOA, GPIO_Pin_12); }
#define AD7780_DATA				GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_10)

#define AD7780_ADIN(n)		{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_12);else GPIO_ResetBits(GPIOC, GPIO_Pin_12);}
#define AD7780_DCRSW(n)		{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_8);else GPIO_ResetBits(GPIOC, GPIO_Pin_8);}
#define AD7780_LSCSDCR(n)	{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_7);else GPIO_ResetBits(GPIOC, GPIO_Pin_7);}

// 读取DCR 值
float Read_LSDcr(void)	//mOhm
{
    float ResValue=-1.0;
    COIL1_TO_COIL2;
    AD7780_DCRSW(1);		//ON DCR TEST
    AD7780_LSCSDCR(0);	//CONNECT TO LS DCR
    AD7780_ADIN(0);			//ON SIGNAL IN
	
	while(ResValue<0)
	{
		switch(LCR_BOARD_TYPE)
		{
			case 1:
			case 2:ResValue = 1000.0*Res_mVValue*(AD7780_Read_NEW()&0x7FFFFF)/(8388608.0)/Res_mAValue;break;//CONFIRM CONNECT NORMAL、AD7780_Read_NEW() 就是调用多次AD7780_Read()
			default:ResValue = 1000.0*Res_mVValue*(AD7780_Read()&0x7FFFFF)/(8388608.0)/Res_mAValue;break;
			// 1. 首先调用ad7780_read()函数读取AD7780芯片的输出值,该值是一个24位的有符号整数。
			// 2. 通过按位与运算符&和0x7fffff,将该值的最高位符号位去掉,得到一个23位的无符号整数。
			// 3. 将该值除以8388608,得到一个0到1之间的小数,表示AD7780芯片输出值所代表的电压值。
			// 4. 将该小数乘以10000、res_mvvalue和res_mavalue三个系数,得到最终的电压值,单位为毫伏。
		}
	}
	
    ResValue = DCR_Line_K * ResValue + DCR_Line_B; // 计算K、B值
    AD7780_ADIN(1);			//OFF SIGNAL IN
    AD7780_DCRSW(0);		//OFF DCR TEST
    AD7780_LSCSDCR(0);	//CONNECT TO LS DCR

    return ResValue;
}


uint32_t AD7780_Read(void)
{
    uint32_t total=0;
	uint32_t data[10];
    u8 i,j;
    for(i=0; i<10; i++)
    {
        data[i]= AD7780_ReadAd();
		if(data[i]&&0x80==0&&i>0)i--;			//V0x0080008D
	}
	return data[9];
	for(i=0;i<9;i++) // 循环取10 次数据,并进行冒泡排序
	{
			for(j=i+1;j<10;j++)
			{
					if(data[j]<data[i])
					{
							total = data[i];
							data[i] = data[j];
							data[j] = total;
					}
			}
	}
	total=0;
	for(i=2;i<7;i++)total+=data[i];		// 冒泡后,取其中第2到第6个数据求和,再求平均
    return (total/5);
}

uint32_t AD7780_ReadAd(void)
{
    static unsigned char adc_err_cnt;
    uint8_t i;
    uint32_t t=0;
    AD7780_CLK(1);
    bsp_DelayUS(10);
    while(AD7780_DATA) {} //等待数据转换完成
    bsp_DelayUS(10);
    for(i=0,t=0; i<32; i++)
    {
        AD7780_CLK(0);
        bsp_DelayUS(15);
        AD7780_CLK(1);
        bsp_DelayUS(15);
        t <<=1;
        if(AD7780_DATA)t++;
    }
    if((t&0xff)==0x4d)
    {
        t>>=8;
        return t;//t&0x7FFFFF;
    }
    else
    {
        adc_err_cnt++;
        if(adc_err_cnt>5)AD7780_Reset();
        return t>>8;
    }
}
  1. 大电阻分挡分压方法测试代码
__IO float resRefValue[3]= {499,10000,10000000}; // 三个上拉电阻挡位

#define COIL1_TO_GND		{GPIO_SetBits(GPIOC, GPIO_Pin_6);GPIO_SetBits(GPIOC, GPIO_Pin_9);GPIO_ResetBits(GPIOD, GPIO_Pin_15);GPIO_ResetBits(GPIOA, GPIO_Pin_8);} //  setbit即打开继电器
#define COIL2_TO_GND		{GPIO_SetBits(GPIOC, GPIO_Pin_6);GPIO_SetBits(GPIOC, GPIO_Pin_9);GPIO_SetBits(GPIOD, GPIO_Pin_15);GPIO_ResetBits(GPIOA, GPIO_Pin_8);}

#define AD7780_ADIN(n)		{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_12);else GPIO_ResetBits(GPIOC, GPIO_Pin_12);}
#define AD7780_DCRSW(n)		{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_8);else GPIO_ResetBits(GPIOC, GPIO_Pin_8);}
#define AD7780_LSCSDCR(n)	{if(n)GPIO_SetBits(GPIOC, GPIO_Pin_7);else GPIO_ResetBits(GPIOC, GPIO_Pin_7);}


float Read_CSDCR(u8 ch)	//ohm
{
    float value;
    OpenCsDCRTest(ch);
    value= Read_CSDCRByCh(2);
    if(value>400000) {
        if(value>10000000)value=10000000;
    }
    else
    {
        value= Read_CSDCRByCh(1);
        if(value<3000) {
            value = Read_CSDCRByCh(0);
        } 
    }
	
		CloseCsDCRTest(ch);
    return	value;
}

void OpenCsDCRTest(u8 ch)
{
    if(ch==1) {
        COIL1_TO_GND;
    }
    else if(ch==2) {
        COIL2_TO_GND;
    }
    AD7780_DCRSW(1);		//ON DCR TEST
    AD7780_LSCSDCR(1);	//CONNECT TO CS DCR
    AD7780_ADIN(0);			//ON SIGNAL IN
		bsp_DelayMS(100);		
}

float Read_CSDCRByCh(u8 ch)	//ohm
{
    float CSDcrValue;
    u32 AD_Value;
    if(ch==2)
    {
        CSDCR_SW1(1);
        CSDCR_SW2(1);
				bsp_DelayMS(100);
				AD_Value = AD7780_Read();
				if(AD_Value & 0x800000)
					AD_Value  &= 0x7fffff;
				else
					AD_Value = 0;
        CSDcrValue = 1.0* AD_Value*resRefValue[2]/(8388608.0-AD_Value);
    }
    if(ch==1)
    {
        CSDCR_SW1(0);
        CSDCR_SW2(1);

				bsp_DelayMS(100);

				AD_Value = AD7780_Read();
				if(AD_Value & 0x800000)
					AD_Value  &= 0x7fffff;
				else
					AD_Value = 0;
        CSDcrValue = 1.0* AD_Value*resRefValue[1]/(8388608.0-AD_Value);
    }
    if(ch==0)
    {
        CSDCR_SW1(1);
        CSDCR_SW2(0);
				bsp_DelayMS(100);
		
				AD_Value = AD7780_Read();
				if(AD_Value & 0x800000)
					AD_Value  &= 0x7fffff;
				else
					AD_Value = 0;
        CSDcrValue = 1.0* AD_Value*resRefValue[0]/(8388608.0-AD_Value);
    }

    return CSDcrValue;
}

1.9 解决ADC 分辨率不够导致测试进度不够的解决方案

在软件中对ADC 进行多次采集,多个数据求平均值,可解决ADC 分辨率不够导致测试进度不够的问题;

2. 存储


2.1 EEPROM芯片 - AT24Cxx

AT24Cxx常用的IIC通讯的EEPROM 器件;

2.1.1 写操作

字节写操作

在该模式下,主器件发送IIC起始信号(通知从器件开始工作)和从器件地址信息(选择与哪个从器件进行通信)给从器件,从器件回应主器件以应答信号后,主器件发送CAT24WC01/02/04/08/16的字节地址(EEPROM内存储单元的地址),从器件回应主器件以应答信号后,主器件发送要写入的数据到被寻址的存储单元,CAT24WC01/02/04/08/16回应主器件以应答信号后,主器件发送IIC停止信号(通知从器件停止工作)给从器件。

在这里插入图片描述

页写操作

页写操作同理于字节写操作,只是写入一个字节后不产生停止信号,主器件被允许发送P个额外的字节(CAT24WC01:P=7,CAT24WC02/04/08/16:P=15,即使用写页操作下,CAT24WC0一次可写8个字节,CAT24WC02/04/08/16一次可写16个字节)。

注意,若在发送停止信号前主器件发送的字节超过P+1个,地址计数器将自动翻转,先前写入的数据将被覆盖。

在这里插入图片描述

2.1.2 读操作

字节读操作

在该模式下,主器件发送IIC起始信号(通知从器件开始工作)和从器件地址信息(选择与哪个从器件进行通信)给从器件,从器件回应主器件以应答信号后,主器件发送CAT24WC01/02/04/08/16的字节地址(EEPROM内存储单元的地址),CAT24WC01/02/04/08/16回应主器件以应答信号后,从器件向主器件返回所要求的一个字节数据,此后主器件不发送应答,但产生一个停止信号。

在这里插入图片描述

页读操作

页读操作就是在字节读操作读完一个字节后,主器件不发送停止信号给从器件,而是发送一个应答信号表示要求进一步读取下一个字节信号,直到主器件向从器件发送停止信号。

注意,若主器件读取的字节超过E个,地址计数器将自动翻转,计数器将翻转到零并继续输出字节数据。(24WC01,E=127;24WC02,E=255;24WC04,E=511;24WC08,E=1023;24WC16,E=2047;)

2.1.3 核心代码

以下代码理论上支持24Cxx系列芯片(地址引脚设置为0),24Cxx的型号定义可在头文件24cxx.h中查看。

函数索引:
void AT24CXX_Init(void); // 芯片初始化
u8 AT24CXX_ReadOneByte(u16 ReadAddr); // 读一个字节数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite); // 写一个字节数据
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len); // 写多个字节数据
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len); // 读多个字节数据
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead); // 读任意位数据
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite); // 写任意位数据
u8 AT24CXX_Check(void); // 检查芯片是否正常工作
void IIC_Write_4Byte(unsigned int RomAddress,unsigned int udata,u8 ch); //连续写四个字节数据
unsigned int IIC_Read_4Byte(unsigned int RomAddress,u8 ch); //连续读写入四个字节数据
void IIC_Write_double(unsigned int RomAddress,double udata,u8 ch); // 写一个双精度浮点数数据
double IIC_Read_double(unsigned int RomAddress,u8 ch); // 读一个双精度浮点数数据
void AT24CXX_ReadFloat(u16 ReadAddr,u8 *p); //读一个浮点数数据
void AT24CXX_WriteFloat(u16 WriteAddr,u8 *p); // 写一个浮点数数据
void IIC_Write_Nbyte(unsigned char *pc,unsigned int Addr,unsigned char number,u8 ch);  // 写多个字节数据
void IIC_Read_Nbyte(unsigned char *pc,unsigned int Addr,unsigned char number,u8 ch); // 读多个字节数据
void testCode_writeEeprom(void); // 数据写入测试代码
void testCode_readEeprom(void); // 数据读取测试代码

  • 初始化24Cxx
// 即初始化IIC
void AT24CXX_Init(void)
{ 
	 GPIO_InitTypeDef GPIO_InitStructure;	//GPIO
    /* 打开GPIO时钟 */
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE|RCC_AHB1Periph_GPIOC|RCC_AHB1Periph_GPIOB, ENABLE);
		
	/* 配置芯片IIC引脚 */
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;
	
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_Init(IIC_SDA1_PORT, &GPIO_InitStructure);
	
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_Init(IIC_SCL1_PORT, &GPIO_InitStructure);
}
  • 在24Cxx的指定地址中读取一个数据
//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;
}
  • 在 24Cxx的指定地址写入一个数据
//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);
}
  • 在24Cxx的指定地址写入长度为 Len 的数据
//该函数用于写入Len个字节的数据.
//WriteAddr :开始写入的地址 
//DataToWrite:数据数组首地址,,最大支持32位
//Len :要写入数据的字节数(8位为一个字节)
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);
	} 
}
  • 在24Cxx的指定地址读出长度为 Len 的数据
//该函数用于读出Len个字节的数据.
//ReadAddr :开始读出的地址
//返回值 :数据,最大支持32位
//Len :要读出数据的字节数
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;
}
  • 在24Cxx的指定地址读出指定个数的数据
//ReadAddr :开始读出的地址 对 24c02 为 0~255
//pBuffer :数据数组首地址
//NumToRead:要读出数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{ 
	while(NumToRead)
	{ 
		*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
		NumToRead--;
	}
}
  • 在24Cxx的指定地址写入指定个数的数据
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{ 
	while(NumToWrite--)
	{ 
		AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
		WriteAddr++;
		pBuffer++;
	}
}
  • 检查24Cxx是否正常工作
//这里用了 24XX 的最后一个地址(第255个字节)来存储标标志.
//如果用其他 24C 系列,这个地址要修改
//返回 1:检测失败
//返回 0:检测成功
u8 AT24CXX_Check(void)
{
	u8 temp;
	temp=AT24CXX_ReadOneByte(255); //避免每次开机都写 AT24CXX 
	if(temp==0X55)return 0; 
	else //排除第一次初始化的情况
	{ 
		AT24CXX_WriteOneByte(255,0X55);
		temp=AT24CXX_ReadOneByte(255); 
		if(temp==0X55)
			return 0;
	}
	return 1; 
}
  • 在指定地址连续写入4个字节数据
void IIC_Write_4Byte(unsigned int RomAddress,unsigned int udata,u8 ch)	
{		
	unsigned char i;
	IIC_Channel = ch;	
	for(i=0;i<=24;i+=8)
	{
	  AT24CXX_WriteOneByte(RomAddress,((unsigned char)(udata>>i)),IIC_Channel); 
	  RomAddress++; 
	}  
}
  • 在指定地址连续读取4个字节数据
unsigned int IIC_Read_4Byte(unsigned int RomAddress,u8 ch)
{
	unsigned char BufData,i;
	unsigned long data=0;
	IIC_Channel = ch;	
	for(i=0;i<=24;i+=8)
	{
	  BufData=AT24CXX_ReadOneByte(RomAddress,IIC_Channel); 
	  RomAddress++; 
	  data|=((unsigned long)BufData)<<i;
	}
	return data;
}
  • 在指定地址写入一个双精度浮点数数据
void IIC_Write_double(unsigned int RomAddress,double udata,u8 ch)
{
	unsigned char i;
	void *p;
	IIC_Channel = ch;
	p=&udata;	
	for(i=0;i<sizeof(double);i++)
	{AT24CXX_WriteOneByte(RomAddress+i,*((char *)p+i),IIC_Channel);}
  
}
  • 在指定地址读取一个双精度浮点数数据
double IIC_Read_double(unsigned int RomAddress,u8 ch)
{
	unsigned char i;
	double data=0.0;
	void *p;
	IIC_Channel = ch;
	p=&data;
	for(i=0;i<sizeof(double);i++)
	{*((char *)p+i)=AT24CXX_ReadOneByte(RomAddress+i,IIC_Channel);}	
	return data;
}
  • 在指定地址读取/写入一个浮点数数据
#define FLOAT_BYTE_NUM 4 // float类型占用的字节数
// #define FLOAT_BYTE_NUM sizeof(float)

// *p:一个指向float类型变量的指针
void AT24CXX_ReadFloat(u16 ReadAddr,u8 *p)
{
    u8 t;
    for(t=0; t<FLOAT_BYTE_NUM; t++)
    {
        *(p+3-t)=AT24CXX_ReadOneByte(ReadAddr+3-t); // 将读取到的数据保存到指针p指向的数据
    }
}

void AT24CXX_WriteFloat(u16 WriteAddr,u8 *p)
{
    u8 t;
    for(t=0; t<FLOAT_BYTE_NUM; t++)
    {
        AT24CXX_WriteOneByte(WriteAddr+t,*(p+t));
    }
}

应用举例:

float floatData=1.0;

u8 *p;
p = (u8 *)(&floatData); 
AT24CXX_WriteFloat(floatDataAddr,p);
AT24CXX_ReadFloat(floatDataAddr,p);
  • 在指定地址连续写入N个字节数据
void IIC_Write_Nbyte(unsigned char *pc,unsigned int Addr,unsigned char number,u8 ch) 
{
	unsigned char i;		
 IIC_Channel = ch;   
	for(i=0;i<=number;i++)
	{
		AT24CXX_WriteOneByte(Addr,pc[i],IIC_Channel);
		Addr++;
	}
}
  • 在指定地址连续读取N个字节数据
void IIC_Read_Nbyte(unsigned char *pc,unsigned int Addr,unsigned char number,u8 ch) 
{
	unsigned char i;
	IIC_Channel = ch;
	for(i=0;i<number;i++)
	{
		pc[i]=AT24CXX_ReadOneByte(Addr,IIC_Channel);
		Addr++;
	}		
} 

  • 应用例程
const u8 TEXT_Buffer[]={"somthing data"}; // 数据缓冲区
#define SIZE sizeof(TEXT_Buffer) // 数据长度

int main(void)
{
	AT24CXX_Init();
	LED_Init();
	
	while(AT24CXX_Check()) //检测24Cxx是否正常工作,若正常则跳出该循环
	{
		LED0=!LED0; //LED闪烁
	}
	AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE) // 在24Cxx的指定地址写入指定个数据
	AT24CXX_Read(0,datatemp,SIZE); //在24Cxx的指定地址读取指定个数据
}
  • EEPROM数据写入/读取测试代码:
void testCode_writeEeprom(void)
{
	unsigned long data = 4294967295;
	
	AT24CXX_WriteLenByte(Name_Add,data,sizeof(data),1);
	sprintfU4("size of data(byte) : %drn", sizeof(data));
	
}

void testCode_readEeprom(void)
{
	unsigned long data;
	
	sprintfU4("size of data(byte) : %drn", sizeof(data));
	data = AT24CXX_ReadLenByte(Name_Add,sizeof(data),1);
	sprintfU4("data : %lurn", data);
}

2.2 延长EEPROM寿命

对于Flash或EEPROM,读操作对其寿命影响不大,写操作(对Nand Flash则为块擦除)次数基本决定了存储器寿命;而且写入寿命在每个位之间是独立的。延长EEPROM寿命有以下几种方法:

  1. 不固定数据存放的地址,而是用一个固定的基地址加上EEPROM内的一个单元的内容(即偏移地址)作为真正的地址;若发现存储单元已坏(写入和读出的内容不同),则偏移地址加1,重新写入。此方法有一弊端:当某一个EEPROM单元被写坏再用下一个单元时,后面到所有数据都会被覆盖。
  2. 从第一个存储单元开始存储数据N次,然后转到下一个单元再存N次,依次类推,当最后一个单元存放N次之后,再转到第一个单元重新开始。即不重复读写(擦写)某几个存储单元,尽量用到EEPROM上的所有存储单元,防止某几个存储单元反复擦写导致损坏。
  3. 在每次写入EEPROM时,附带写入一个标志位。AT24CXX_WriteOneByte(DATA_FLAG_Addr,0x0A);,上电初始化EEPROM时,读取该位并进行比较,查看EEPROM是否被写入过。(其实该方法对EEPROM对寿命对延长作用不大,更多的是处理初始化数据的作用)
temp = AT24CXX_ReadOneByte(DATA_FLAG_Addr);
if(temp==0x0A)
{
	// 读取EEPROM中的数据,并写入到程序到变量中
}
else
{
	// 手动写入一些初始值到程序变量中
}

2.3 关于存储器读写地址

存储器写入首先不能超过存储器的最高数据位,如AT24C02是2k(256*8)即2k bit(256个字节)的存储芯片,其最高字节地址为256,最后一个位的地址为2048. 其次是每个地址要定义好用于存储多少位的数据,千万不能产生数据存储错乱,比如定义为用于存储一个字节数据的地址,却写入了两个字节数据,那么不仅在下次读该字节地址中的值产生错误,还会导致多出的一个字节数据写入到下一个字节数据地址里去,造成下一个字节数据地址的数据错乱。

#define xxx0_Addr	0 // xxx0_Addr该地址用于存储一个字节的数据
#define xxx1_Addr	8 // 那么下一个字节数据就要相隔一个字节,xxx1_Addr 该地址用于存储两个字节的数据
#define xxx1_Addr	16 // 那么下一个字节数据就要相隔两个字节,如此类推

参考:AT24C02使用详解

3. 中断


中断是指通过硬件来改变CPU 的运行方向。单片机在执行程序的过程中,外部设备向CPU 发出中断请求信号,要求CPU 暂时中断当前程序的执行而转去执行相应的处理程序,待处理程序执行完毕后,再继续执行原来被中断的程序。这种程序在执行过程中由于外界的原因而被中间打断的情况称为“中断”;

在这里插入图片描述

  • 主程序:原来正常运行的程序
  • 中断源:引起中断的原因,或者能发出中断请求的来源
  • 中断请求:中断源要求服务的请求称为中断请求(或中断申请)
  • 断点:主程序被断开的位置(或者地址)
  • 中断入口地址:中断服务程序的首地址
  • 中断服务程序:cpu响应中断后,转去执行的相应处理程序

  • 中断的特点
    1. 同步工作:中断是CPU 和接口之间的信息传递方式之一,它使CPU 与外设同步工作,较好地解决了快速CPU 与慢速外设之间的匹配问题;
    2. 异常处理:针对难以预料的异常情况,如掉电、存储出错、运算溢出等,可以通过中断系统由故障源向CPU 发出中断请求,再由CPU 转到相应的故障处理程序进行处理;
    3. 实时处理:CPU 能够及时处理应用系统的随机事件,实时性大大增加;

3.1 STM32 的中断

Cortex-M3(CM3)内核MCU 最多支持256个中断,其中包含了16 个内核中断和240个可屏蔽中断,最多具有 256 级的可编程中断设置,根据不同型号单片机,其支持的中断数量不同,具体可查看对应芯片的数据手册中的中断向量表

如::STM32F1 系列芯片只用了CM3内核的部分资源,共有84个中断,包括16个内核中断和68个可屏蔽中断,具有16级可编程的中断优先级;而STM32F103系列又只有60个可屏蔽中断,如下图:

在这里插入图片描述

中断优先级数字越小,对应中断优先级越高;


STM32 外部中断功能实现细分如下图:
在这里插入图片描述
下面进行逐一讲解:

3.1 NVIC 嵌套向量中断控制器

NVIC(嵌套向量中断控制器) 控制整个芯片中断的相关功能;

在这里插入图片描述

3.1.1 中断优先级分组(中断管理方法)

STM32F1系列芯片通过配置应用中断与复位控制寄存器Application interrupt and reset control register AIRCR[10:8] 来对MCU 的中断优先级分组进行配置,中断优先级分组的作用就是决定把IP bit[7:4]这4个位如何分配给抢占优先级和子优先级
在这里插入图片描述

  • 配置中断优先级的功能通过函数NVIC_PriorityGroupConfig() 实现,它定义在源文件misc.c中,其函数定义如下:
/**
  * @brief  Configures the priority grouping: pre-emption priority and subpriority.
  * @param  NVIC_PriorityGroup: specifies the priority grouping bits length. 
  *   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
  */
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;
}



// 如将MCU 的中断优先级分组配置为组0,即IP bit 第4~ 7位为0 位抢占优先级,4位响应优先级:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

中断优先级分组配置应该在MCU 初始化的同时配置好,中途不能对其进行修改,以免造成中断混乱;


  • 抢占优先级和响应优先级的区别
    • 抢占 = 打断别人,高优先级的抢占优先级可以打断正在进行的低抢占优先级中断(值越小说明级越高);
    • 响应 = 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断;
      在这里插入图片描述
  • 若需要挂起/解挂中断,查看中断当前激活状态,分别调用相关函数;

应用举例:

  1. 假设MCU 设置中断优先级组为2,然后对以下三个中断进行配置:
  2. 中断4(FLASH中断 )抢占优先级为2,响应优先级为1
  3. 中断5(RCC中断)抢占优先级为3,响应优先级为0
  4. 中断6(EXTIO中断) 抢占优先级为2,响应优先级为0

则,3个中断的优先级顺序为: 中断6>中断4>中断5

3.1.2 NVIC 参数结构体

NVIC的所有需要配置的参数都列举在结构体NVIC_InitTypeDef中,它定义在源文件misc.c中,其结构体如下:

/** 
  * @brief  NVIC Init Structure definition  
  */

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;

3.2 外部中断/事件控制器 EXTI

EXTI(External interrupt/event controller - 外部中断/事件控制器):管理着中断控制器的 20个中断/事件线。每个中断/事件线都对应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。 EXTI 可实现对每个中断/事件线进行单独配置,可单独配置为中断或者事件,以及触发事件的属性。EXTI 是NVIC的一个中断/事件传入器

  • EXTI 可分为两大部分功能:

    1. 产生中断:如下图,红色线路1-2-4-5是产生中断的过程
    2. 产生事件:如下图,绿色线路1-2-3-6-7-8是产生事件的过程,其中标黄的 20/ 代表着有20条相同的线路
  • EXTI功能框图:
    EXTI功能框图


关于EXTI 的代码都在固件库的 stm32f10x_exti.hstm32f10x_exti.c 文件中;

STM32 的每个 IO 都可以作为外部中断的中断输入口,比如STM32F103 的中断控制器支持 19 个外部中断/事件请求。每个中断/事件都有独立的触发和屏蔽设置。

其中,STM32F103 的19 个外部中断为:

  1. 线 0~15:对应外部 IO 口的输入中断。
  2. 线 16:连接到 PVD 输出。
  3. 线 17:连接到 RTC 闹钟事件。
  4. 线 18:连接到 USB 唤醒事件。

可见,stm32f103中外部IO 口的输入中断有16条线,但STM32 的GPIO却远远不止16个,其分配方式如下图:

在这里插入图片描述

以线 0 为例:它对应了 GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、GPIOE.0、GPIOF.0、GPIOG.0 . 而中断线每次只能连接到 1个 IO 口上,这就需要通过配置来决定对应的中断线配置到哪个 GPIO 上;

  • STM32 EXTI 中断向量表
    在这里插入图片描述
    从上图可知,IO 口外部中断在中断向量表中只分配了7个中断向量也就是只能使用7个中断服务函数,从表中可以看出,外部中断线5~ 9分配一个中断向量,共用一个服务函数;外部中断线10~ 15分配一个中断向量,共用一个中断服务函数;

  • 对于每个中断线,可以设置相应的触发方式:上升沿触发,下降沿触发,边沿触发;

3.3 中断服务函数

中断被成功出发后,代码就会执行中断服务函数中的代码。

每个中断都有其固定的中断服务函数名,只有在这个函数名下编写中断服务函数才是有效的。所有中断服务函数都可在stm32f10x_it.c 的中断向量表中查找。其中EXTI线0到EXTI线4线都是单独的中断函数名、EXTI线5到EXTI线9共用一个中断函数名、EXTI线10线到EXTI线15线共用一个中断函数名。

// 例程
void EXTI0_IRQHandler(void)
{
	if(EXTI_GetITStatus(KEY1_EXTI_LINE)!=RESET) // 读取中断是否执行
	{
		LED1_TOGGLE;   //LED1的亮灭状态反转
	}
	EXTI_ClearITPendingBit(KEY1_EXTI_LINE); //清除中断标志位
}

在 《STM32中文参考手册 V10》 - 第九章的表55 其它STM32F10xxx产品(小容量、中容量和大容量)的向量表中可查看所有的中断通道;
在这里插入图片描述

3.4 STM32 中断优先级寄存器配置及其参考代码

STM32 中断优先级配置一共设计以下7个寄存器:
在这里插入图片描述

  1. SCB_AIRCR:32 位寄存器,有效位为第8到10位,用于设置5种中断优先级分组;
  2. IP240个8位寄存器,每个中断使用一个寄存器来确定优先级,每个8位寄存器有效位为第4到7位,用于设置抢占优先级与响应优先级;

如STM32F10x系列一共60个可屏蔽中断,那么它就只使用了IP[59]~IP[0l;

在这里插入图片描述

  1. ISER:8个32位寄存器,每个位控制一个中断的使能,写1使能,写0无效;

如STM32F10x系列一共60个可屏蔽中断,所以它只使用了其中的ISER[0]和ISER[1]; ISER[0]的bito~ bit31分别对应中断0~ 31、ISER[1]的bit0~ bit27对应中断32~ 59;

  1. ICER:8个32位寄存器,每个位控制一个中断的失能,写1失能,写0无效;

如STM32F10x系列一共60个可屏蔽中断,所以它只使用了其中的ICER[0]和ICER[1]; ICER[0]的bito~ bit31分别对应中断0~ 31、ICER[1]的bit0~ bit27对应中断32~ 59;

  1. ISPR:8个32位寄存器,每个位控制一个中断的挂起,写1失能,写0无效;

如STM32F10x系列一共60个可屏蔽中断,所以它只使用了其中的ISPR[0]和ISPR[1]; ISPR[0]的bito~ bit31分别对应中断0~ 31、ISPR[1]的bit0~ bit27对应中断32~ 59;

  1. ICPR:8个32位寄存器,每个位控制一个中断的解挂,写1失能,写0无效;

如STM32F10x系列一共60个可屏蔽中断,所以它只使用了其中的ICPR[0]和ICPR[1]; ICPR[0]的bito~ bit31分别对应中断0~ 31、ICPR[1]的bit0~ bit27对应中断32~ 59;

  1. IABR:8个32位寄存器,只读寄存器,每个位指示一个中断的激活状态,读1表示中断正在执行;

如STM32F10x系列一共60个可屏蔽中断,所以它只使用了其中的IABR[0]和IABR[1]; IABR[0]的bito~ bit31分别对应中断0~ 31、IABR[1]的bit0~ bit27对应中断32~ 59;


  • 参考代码:以配置中断5作为外部中断为例,GPIO 口选择PE9:
  1. 输入配置为浮空输入
  2. 若涉及到端口复用问题,还需要打开相应的端口复用时钟
void EXTI_Configuration(void) // 配置外部中断/事件
{
	EXTI_InitTypeDef EXTI_InitStructure;  // 定义外部中断参数结构体
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource9); // 外部中断线配置,PE9
	
	EXTI_InitStructure.EXTI_Line=EXTI_Line9; // 外部中断线
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 外部中断模式
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发
	EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能外部中断
	EXTI_Init(&EXTI_InitStructure); //初始化外部中断线参数
}


void NVIC_Configuration(void) // 配置NVIC
{
	NVIC_InitTypeDef NVIC_InitStructure;  // 定义中断配置参数结构体
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);          // 设置中断组为0
	NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn;       // 设置中断来源(中断通道) ,外部中断线5
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;  // 设置抢占优先级为 0
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;         // 设置子优先级为3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;           // 使能中断
	NVIC_Init(&NVIC_InitStructure); //初始化NVIC参数
}

void EXTI_init(void) //外部中断初始化
{
	NVIC_Configuration();
	EXTI_Configuration();
}

void EXTI9_5_IRQHandler(void)   // 外部中断5 中断服务函数,记得中断服务函数的函数名是内核规定的,不是自定义的!
{
	if(EXTI_GetITStatus(EXTI_Line9) != RESET) // 注意这里不要写错NVIC的 EXTI9_5_IRQn !!
	sprintfU4("外部中断5已被触发rn");
	EXTI_ClearITPendingBit(EXTI_Line9); //清除 LINE9 上的中断标志位
}	

3.5 其他:

  1. 基于CORTEX-M3内核的硬件因素,清除中断标志不会马上生效,需要一段时间,如果你的中断服务程序时间很短,就会出现中断重复进入的异常;这种情况,可以在程序中增加去抖动和延时功能;

参考:STM32 官方参考手册;

最后

以上就是美满黑裤为你收集整理的STM32理论 —— ADC、存储、定时器、时钟、中断1. ADC2. 存储3. 定时器4. 时钟5. 中断的全部内容,希望文章能够帮你解决STM32理论 —— ADC、存储、定时器、时钟、中断1. ADC2. 存储3. 定时器4. 时钟5. 中断所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(75)

评论列表共有 0 条评论

立即
投稿
返回
顶部