STM32
STM32简介
- STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器
- ARM既指ARM公司,也指ARM处理器内核
片上资源/外设
命名规则
系统结构
引脚定义
启动配置
最小系统电路
型号分类及缩写
新建工程步骤
- 建立工程文件夹,Keil中新建工程,选择型号
- 工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹
- 工程里对应建立Start、Library、User等同名称的分组,然后将文件夹内的文件添加到工程分组里
- 工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹
- 工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER
- 工程选项,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and Run
工程架构
GPIO
GPIO简介
- GPIO(General Purpose Input Output)通用输入输出口
- 可配置为8种输入输出模式
- 引脚电平:0V~3.3V,部分引脚可容忍5V
- 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
- 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等
GPIO基本结构
GPIO位结构
GPIO模式
-
浮空输入模式
-
数据通道中仅接入TTL触发器(作用是将相对缓慢变化的模拟信号变成矩形信号)整形,随后输入输入数据寄存器该种工作模式未接入任何上拉/下拉电阻
-
模式特点:在该引脚悬空(无信号输入)的情况下,读取该端口的电平是不确定的
-
适用场合:外部按键输入/USART RX引脚
-
-
上拉输入模式:
- 与浮空输入模式相比,仅仅是在数据通道前端接入了一个上拉电阻,其余无变化
- 模式特点:在无信号输入时端口电位受上拉电阻钳制,I/O端口输入电平始终保持为高电平;而当端口输入电平为低电平时,I/O端口输入电平为低电平
- 适用场合:需要IO内部上拉电阻输入时,器件的外部中断(IRQ)引脚触发中断条件为下降沿触发/低电平触发,这样在无信号输入时始终保持高电平,如果有事件触发中断IRQ可以输出一个低电平,进而可产生(下降沿/低电平)中断。例如单片无线收发器芯片NRF24L01的IRQ引脚的工作模式即为上拉输入模式
-
下拉输入模式:
- 与浮空输入模式相比,仅仅是在数据通道前端接入了一个下拉电阻,其余无变化
- 模式特点:在无信号输入时端口电位受下拉电阻钳制,I/O端口输入电平始终保持为低电平;而当端口输入电平为高电平时,I/O端口输入电平为高电平
- 适用场合:需要IO内部下拉电阻输入时,器件的外部中断(IRQ)引脚触发中断条件为上升沿触发/高电平触发时,该端口可以选择下拉输入模式
-
模拟输入模式:
- 数据通道不接入任何处理单元(TTL触发器/钳制电阻),直接输入MCU内部的处理单元
- 模式特点:相较于其他输入模式只能读取到逻辑高/低电平(数字量),该模式能读取到细微变化的值(模拟量)
- 适用场合:ADC模拟输入/低功耗下省电
-
推挽输出:
- 输出具有驱动能力,当CPU输出逻辑’0’时,I/O端口输出低电平,而当CPU输出逻辑’1’时,I/O端口输出高电平
- 适用场合:通常作为普通的GPIO用于驱动LED、数码管等电子元器件或输出控制某个信号。
-
开漏输出:
- 适合做电流型的驱动,其吸收电流能力较强。当CPU输出逻辑’0’时,I/O端口输出低电平,而当CPU输出逻辑’1’时,该引脚处于开漏,也就是浮空状态(高阻态),如果想输出高电平则必须接入上拉电阻。同时IO口可以由外部电路改变为低电平或不变,即可读IO输入电平变化,实现了I/O端口的
双向功能
;此外,可以将多路开漏输出的引脚连接到一条线上,通过一个上拉电阻,在不增加任何器件的情况下,形成“与逻辑”关系,这也是I2C,SMBus,等总线判断总线占用状态的原理
- 适合做电流型的驱动,其吸收电流能力较强。当CPU输出逻辑’0’时,I/O端口输出低电平,而当CPU输出逻辑’1’时,该引脚处于开漏,也就是浮空状态(高阻态),如果想输出高电平则必须接入上拉电阻。同时IO口可以由外部电路改变为低电平或不变,即可读IO输入电平变化,实现了I/O端口的
-
复用推挽输出:
- 在STM32中,一个引脚通常可作为普通GPIO来使用,但通常有多个复用模块对应着同一个引脚,那么当这个GPIO作为内置外设引脚时,就叫做复用模式
- 适用场合:常见片内外设(USART TX引脚/SPI/PWM输出等等)
-
复用开漏输出:
- 与开漏输出特性一致,只不过引脚选择了复用功能
- 适用场合:常见片内外设(I2C/SMBus等等)
浮空/上拉/下拉输入
模拟输入
开漏/推挽输出
复用开漏、推挽输出
编程流程
- 使能 GPIO端口时钟
- 初始化 GPIO 目标引脚为推挽输出模式
- 编写简单测试程序,控制 GPIO引脚输出高、低电平
/*定义一个 GPIO_InitTypeDef 类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启 LED 相关的 GPIO 外设时钟*/
RCC_APB2PeriphClockCmd(LED1_GPIO_CLK | LED2_GPIO_CLK | LED3_GPIO_CLK, ENABLE);
/*选择要控制的 GPIO 引脚*/
GPIO_InitStructure.GPIO_Pin = LED1_GPIO_PIN;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为 50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化 GPIO*/
GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure);
/* 关闭所有 led 灯 */
GPIO_SetBits(LED1_GPIO_PORT, LED1_GPIO_PIN);
IO复用
- 打开AFIO时钟
- 重映射端口
- 解除调试端口(当IO口是调试端口时必须三步都在)
中断
中断
-
中断的概念
- 在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
-
中断优先级
- 当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源
-
中断嵌套
- 当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回
中断执行流程
- 在stm32中中断函数的名称是不变的,在stm32f1xx_it头文件中
stm32中断
- stm32中有68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设
- 使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级
- 嵌套向量中断NVIC
NVIC基本结构
NVIC优先级分组
- NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
- 抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队
EXTI外部中断
EXTI简介
- EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序
- 支持的触发方式:上升沿/下降沿/双边沿/软件触发
- 支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断
- 通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
- 触发响应方式:中断响应/事件响应
EXTI基本结构
AFIO复用IO口
- AFIO主要用于引脚复用功能的选择和重定义
- 在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择
EXTI框图
中断事件
编程流程
- 初始化用来产生中断的 GPIO
- 初始化 EXTI
- 配置 NVIC
- 编写中断服务函数
注意:GPIO之前必须开启 GPIO端口的时钟;用到 EXTI必须开启 AFIO 时钟
//嵌套向量中断控制器 NVIC 配置
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 配置 NVIC 为优先级组 1 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/* 配置中断源:按键 1 */
NVIC_InitStructure.NVIC_IRQChannel = KEY1_INT_EXTI_IRQ;
/* 配置抢占优先级:1 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 配置子优先级:1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断通道 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 配置中断源:按键 2,其他使用上面相关配置 */
NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ;
NVIC_Init(&NVIC_InitStructure);
}
//EXTI 中断配置
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
/*开启按键 GPIO 口的时钟*/
RCC_APB2PeriphClockCmd(KEY1_INT_GPIO_CLK,ENABLE);
/* 配置 NVIC 中断*/
NVIC_Configuration();//完成对按键 1、按键 2 优先级配置并使能中断通道
/*--------------------------KEY1 配置---------------------*/
/* 选择按键用到的 GPIO */
GPIO_InitStructure.GPIO_Pin = KEY1_INT_GPIO_PIN;
/* 配置为浮空输入 */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure);
/* 选择 EXTI 的信号源 */
GPIO_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE,KEY1_INT_EXTI_PINSOURCE);//用来指定中断/事件线的输入源
EXTI_InitStructure.EXTI_Line = KEY1_INT_EXTI_LINE;
/* EXTI 为中断模式 */
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
/* 上升沿中断 */
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
/* 使能中断 */
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//EXTI 中断服务函数
void KEY1_IRQHandler(void)//实际上是EXTI0_IRQHandler
{
//确保是否产生了 EXTI Line 中断
if (EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET)
{
// LED1 取反
LED1_TOGGLE;
//清除中断标志位
EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE);
}
}
TIM定时器
-
STM32F1 系列中,除了互联型的产品,共有 8 个定时器
-
定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
-
16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时
-
不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能
-
根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型
定时器类型
高级定时器
通用定时器
-
定时中断参考基本定时器操作流程
-
输出PWM波形
- 输出比较简介
- 输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形
- 每个高级定时器和通用定时器都拥有4个输出比较通道
- 高级定时器的前3个通道额外拥有死区生成和互补输出的功能
-
PWM参数:频率 = 1 / TS 占空比 = TON / TS 分辨率 = 占空比变化步距
-
输出比较模式
- OC(Output Compare)输出比较输出比较可以通过比较CNT与CCR寄存器值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形每个高级定时器和通用定时器都拥有4个输出比较通道高级定时器的前3个通道额外拥有死区生成和互补输出的功能
- 输出比较模式
- pwm基本结构
- 参数计算
- PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
- PWM占空比: Duty = CCR / (ARR + 1)
- PWM分辨率: Reso = 1 / (ARR + 1)
- 输入捕获模式
- 输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数
- 每个高级定时器和通用定时器都拥有4个输入捕获通道
- 可配置为PWMI模式,同时测量频率和占空比
- 可配合主从触发模式,实现硬件全自动测量
- 频率测量
- 测频法(又称“计频法”):在闸门时间T内,对上升沿计次,得到N,则频率 f_x=N / T
- 测周法的测量对象是频率
较高
的被测信号 - 当标准信号频率远低于被测信号频率时,闸门时间T内的被测信号脉冲很多(N1很大),测频法得到的结果就越准确
- 测周法的测量对象是频率
- 测周法(又称“计时法”):两个上升沿内,以标准频率fc计次,得到N,则频率 f_x=f_c / N
- 测周法的测量对象是频率
较低
的被测信号- 当被测信号频率远低于标准信号频率时,两个上升沿内的被测信号脉冲很多(N很大),测周法得到的结果就很准确
- 测周法的测量对象是频率
- 中界频率:测频法与测周法误差相等的频率点 f_m=√f_c / T
- 测频法(又称“计频法”):在闸门时间T内,对上升沿计次,得到N,则频率 f_x=N / T
- 输入捕获通道
- 主从触发模式
- 输入捕获基本结构
- PWMI基本结构
- 编码器
- 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度
- 每个高级定时器和通用定时器都拥有1个编码器接口
- 两个输入引脚借用了输入捕获的通道1和通道2
- 编码器接口基本结构
- 工作模式
- 实例(均不反相)
- 编码器(TI1反相)
基本定时器
-
时基单元
- 预分频器 计数器 自动重装载寄存器 构成
-
定时器中断
- UI 更新中断 产生之后送到nvic嵌套向量中断
-
主从模式触发DAC
- U 更新事件 可以将时间映射到TRGO 不需要CPU的干预 直接输出DAC 不需要软件的参与
-
定时时间的计算
- 等于计数器的中断周期乘以中断的次数
- 计数器在 CK_CNT 的驱动
下,计一个数的时间则是 CK_CLK 的倒数,等于:1/(TIMxCLK/(PSC+1)),产生一次中断的时间则等于:1/(CK_CLK * ARR)。如果在中断服务程序里面设置一个变量 time,用
来记录中断的次数,那么就可以计算出我们需要的定时时间等于: 1/CK_CLK×(ARR+1)×time。 - tout = 预分频值/sysclk×ARR;
定时中断基本结构
定时器中断编程要点
- 开定时器时钟;
- 选择时钟源(通用和高级,基本定时器只有一个时钟源)
- 初始化时基初始化结构体;
- 清理中断标志位;
- 使能中断TIM_ITConfig;
- 配置NVIC;
- 打开定时器;
- 编写中断服务程序;
#include "stm32f10x.h" // Device header
#include "tim.h"
extern int time;
void NVIC_config(void);
void tim_init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 1000;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);
TIM_Cmd(TIM2,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,DISABLE);
}
void NVIC_config(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 3;
NVIC_Init(&NVIC_InitStruct);
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2,TIM_IT_Update) != RESET)
{
time++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
输出PWM编程要点
- 开定时器时钟;
- 选择时钟源(通用和高级,基本定时器只有一个时钟源)
- 初始化时基初始化结构体;
- 初始化输出比较通道(通道结构体的其他不用的成员使用TIM_OCStructInit函数初始化 否则可能出现一些错误)
- 开启GPIO的时钟
- 配置gpio初始化结构体
#include "PWM.h"
#include "stm32f10x.h" // Device header
void PWM_init(void)
{
//RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//´ò¿ª¶¨Ê±Æ÷ʱÖÓ
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; //ʱ»ù½á¹¹Ìå
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //¸´ÓÃÍÆÍìÊä³ö
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIO_Pin_15;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//¼òµ¥¶¨Ê±Æ÷³õʼ»¯
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
//Ñ¡ÔñÄÚ²¿Ê±ÖÓÔ´
TIM_InternalClockConfig(TIM2);
//³õʼ»¯Ê±»ù
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; //ʱÖÓ·ÖÆµ
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //¼ÆÊýģʽ
TIM_TimeBaseInitStruct.TIM_Period = 1000 - 1; //¶¨Ê±Æ÷ÖÜÆÚ ARR
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1; //Ô¤·ÖƵÆ÷ PSC
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0; //ÖØ¸´¼ÆÊýÆ÷
//¶¨Ê±Æ÷³õʼ»¯
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
//Êä³öͨµÀ³õʼ»¯
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_Pulse = 0; //RCC
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OC1Init(TIM2,&TIM_OCInitStruct);
//注意若是高级定时器 需要调用TIM_CtrlPWMOutputs函数,总输出使能
TIM_Cmd(TIM2,ENABLE);
}
void PWM_SetCompare1(short int Compare)
{
TIM_SetCompare1(TIM2,Compare);
}
输入捕获编程要点
- 开启GPIO的时钟
- 配置gpio初始化结构体
- 开定时器时钟;
- 选择时钟源(通用和高级,基本定时器只有一个时钟源)
- 初始化时基初始化结构体;
- 初始化输入捕获通道(通道结构体的其他不用的成员使用TIM_OCStructInit函数初始化 否则可能出现一些错误)
- 配置从模式触发源
- 使能定时器
- 获取CCR的值
- 若获取频率一个通道即可 使用测周法或者计频法
- 若是获取占空比 需要两个通道 CCR2/CCR1
#include "stm32f10x.h" // Device header
#include "IC.h"
/**
* @brief:ÊäÈë²¶»ñ»ù±¾»ú¹¹ ƵÂʲâÁ¿
* @param:ÎÞ
* @retval:ÎÞ
*/
void IC_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
//³õʼ»¯ÊäÈëGPIO¿Ú
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
//ÅäÖÃʱ»ùµ¥Ôª
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);
//ÅäÖò¶»ñ¹ÜµÀ
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3,&TIM_ICInitStruct);
//ÅäÖôÓģʽ´¥·¢Ô´
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
//¶¨Ê±Æ÷ʹÄÜ
TIM_Cmd(TIM3,ENABLE);
}
/**
* @brief:»ñȡƵÂÊ
* @param:ÎÞ
* @retval:·µ»ØÆµÂÊ
*/
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1);//²âÖÜ·¨ ƵÂÊ=fc/N
}
/**
* @brief:ÊäÈë²¶»ñPWMI»ù±¾»ú¹¹ ƵÂʲâÁ¿
* @param:ÎÞ
* @retval:ÎÞ
*/
void IC_PWMI_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
//³õʼ»¯ÊäÈëGPIO¿Ú
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
//ÅäÖÃʱ»ùµ¥Ôª
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);
//ÅäÖò¶»ñ¹ÜµÀ PWMI ÐèÒª2¸öͨµÀ
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_PWMIConfig(TIM3,&TIM_ICInitStruct);//¸ù¾ÝͨµÀ1·´ÏòÅäÖÃͨµÀ2
//ÅäÖôÓģʽ´¥·¢Ô´
TIM_SelectInputTrigger(TIM3,TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_Reset);
//¶¨Ê±Æ÷ʹÄÜ
TIM_Cmd(TIM3,ENABLE);
}
/**
* @brief:»ñȡռ¿Õ±È
* @param:ÎÞ
* @retval:·µ»ØÕ¼¿Õ±È
*/
uint32_t IC_GetDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1); //Õ¼¿Õ±È Duty=CCR2/CCR1
}
编码器编程要点
- 开启GPIO的时钟
- 配置gpio初始化结构体
- 开定时器时钟;
- 选择时钟源(通用和高级,基本定时器只有一个时钟源)
- 初始化时基初始化结构体;
- 初始化2条输入捕获通道(通道结构体的其他不用的成员使用TIM_OCStructInit函数初始化 否则可能出现一些错误)
- 配置输入捕获通道的极性和模式(必须在输入捕获通道之后配置)
- 使能定时器
- 配置定时器中断 每隔一秒到中断函数中获取CCR的值
#include "stm32f10x.h" // Device header
#include "Encoder.h"
void Encoder_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
//³õʼ»¯ÊäÈëGPIO¿Ú
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_InternalClockConfig(TIM3);
//ÅäÖÃʱ»ùµ¥Ôª
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 65536 - 1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);
//ÅäÖò¶»ñ¹ÜµÀ
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInit(TIM3,&TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInit(TIM3,&TIM_ICInitStruct);
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//ÅäÖñàÂëÆ÷ģʽ ÒÔ¼°Á½¸öͨµÀÊÇ·ñ·´Ïà
//ÕâÀïµÄRisingºÍFalling²»´ú±íÉÏÉýÑØ ±íʾÊÇ·ñ·´Ïò
//´Ëº¯Êý±ØÐëÔÚÊäÈë²¶»ñ³õʼ»¯ºó½øÐвÙ×÷ ·ñÔò»á±»³õʼ»¯¸²¸Ç
//¶¨Ê±Æ÷ʹÄÜ
TIM_Cmd(TIM3,ENABLE);
}
/**
* @brief:»ñÈ¡±àÂëÆ÷ÔöÁ¿Öµ
* @param:ÎÞ
* @retval:·µ»ØCCRÖеÄÖµ
*/
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0);
return Temp;
}
I2C
- 同步半双工
- 两条线
- 数据线 SDA
- 时钟线 SCL
- 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制
I2C时序
- 开始时序
- SCL 高电平
- SDA 高->低电平
- 停止时序
- SCL 高电平
- SDA 低->高电平
- 发送数据
- SCL 高电平期间 SDA 不可以改变
- SCL 低电平期间 SDA 可以改变
- 应答
- 当在第九个时序时 SDA 控制权交给接收端
- 拉低SDA线,并在SCL为高电平期间保持SDA线为低电平 应答
- 不要拉低SDA线(此时SDA线为高电平),并在SCL为高电平期间保持SDA线为高电平 非应答
I2C发送/接收
- 开始时序
- 发送从机地址
- 发送R/W
- 发送数据
- 接受应答ack
- 重复4-5
- 当5接受为非应答信号
- 停止时序
I2C复合
- 开始时序
- 发送从机地址
- 发送R/W
- 发送数据(或者为内部存储器地址)
- 再开始时序
- 发送从机地址
- 发送R/W
- 读数据
- 接受应答
- 当5接受为非应答信号
- 停止时序
I2C仲裁
-
遵循3个机制
-
“线与”机制;多主机时,总线具有“线与”的逻辑功能,即只要有一个节点发送低电平时,总线上就表现为低电平
-
SDA回读机制;总线被启动后,多个主机在每发送一个数据位时都要对自己的输出电平进行检测,只要检测的电平与自己发出的电平一致,就会继续占用总线
-
低电平优先机制;由于线与的存在,当多主机发送时,谁先发送低电平谁就会掌握对总线的控制权
-
-
I2C总线上可能在某一时刻有两个主控设备要同时向总线发送数据,这种情况叫做总线竞争。I2C总线具有多主控能力,可以对发生在SDA线上的总线竞争进行仲裁,其仲裁原则是这样的: 假设主控器1要发送的数据DATA1为“101 ……”;主控器2要发送的数据DATA2为“1001 ……”总线被启动后两个主控器在每发送一个数据位时都要对自己的输出电平进行检测,只要检测的电平与自己发出的电平一致,他们就会继续占用总线。在这种情况下总线还是得不到仲裁。当主控器1发送第3位数据“1”时(主控器2发送“0” ),由于“线与”的结果SDA上的电平为“0”,这样当主控器1检测自己的输出电平时,就会测到一个与自身不相符的“0”电平。这时主控器1只好放弃对总线的控制权;因此主控器2就成为总线的唯一主宰者。
I2C编程顺序
- 配置通讯使用的目标引脚为开漏模式
- 使能 I2C外设的时钟
- 配置 I2C外设的模式、地址、速率等参数并使能 I2C外设
- 编写基本 I2C按字节收发的函数
- 编写读写 EEPROM 存储内容的函数
- 编写测试程序,对读写数据进行校验
//I2C 硬件相关宏定义
#define EEPROM_I2Cx I2C1
#define EEPROM_I2C_APBxClock_FUN RCC_APB1PeriphClockCmd
#define EEPROM_I2C_CLK RCC_APB1Periph_I2C1
#define EEPROM_I2C_GPIO_APBxClock_FUN RCC_APB2PeriphClockCmd
#define EEPROM_I2C_GPIO_CLK RCC_APB2Periph_GPIOB
#define EEPROM_I2C_SCL_PORT GPIOB
#define EEPROM_I2C_SCL_PIN GPIO_Pin_6
#define EEPROM_I2C_SDA_PORT GPIOB
#define EEPROM_I2C_SDA_PIN GPIO_Pin_7
/* STM32 I2C 快速模式 */
#define I2C_Speed 400000
/* 这个地址只要与 STM32 外挂的 I2C 器件地址不一样即可 */
#define I2Cx_OWN_ADDRESS7 0X0A
/* AT24C01/02 每页有 8 个字节 */
#define I2C_PageSize 8
//配置 I2C 的模式
/**
* @brief I2C 工作模式配置
* @param 无
* @retval 无
*/
static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
/* I2C 的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* 通信速率 */
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/* I2C 初始化 */
I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(EEPROM_I2Cx, ENABLE);
}
/**
* @brief I2C 外设(EEPROM)初始化
* @param 无
* @retval 无
*/
void I2C_EE_Init(void)
{
I2C_GPIO_Config();
I2C_Mode_Configu();
/* 根据头文件 i2c_ee.h 中的定义来选择 EEPROM 要写入的设备地址 */
/* 选择 EEPROM Block0 来写入 */
EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
}
//向 EEPROM 写入一个字节的数据
/*通讯等待超时时间*/
#define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
/**
* @brief I2C 等待事件超时的情况下会调用这个函数来处理
* @param errorCode:错误代码,可以用来定位是哪个环节出错.
* @retval 返回 0,表示 IIC 读取失败.
*/
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 使用串口 printf 输出错误信息,方便调试 */
EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
return 0;
}
/**
* @brief 写一个字节到 I2C EEPROM 中
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @retval 正常返回 1,异常返回 0
*/
uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
{
/* 产生 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
/*设置超时等待时间*/
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,
I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
}
/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2Cx, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,
I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
}
/* 发送一字节要写入的数据 */
I2C_SendData(EEPROM_I2Cx, *pBuffer);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,
I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
return 1;
}
//多字节写入及状态等待
/**
* @brief 将缓冲区中的数据写到 I2C EEPROM 中,采用单字节写入的方式,
速度比页写入慢
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @param NumByteToWrite:写的字节数
* @retval 无
*/
uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr,
uint16_t NumByteToWrite)
{
uint16_t i;
uint8_t res;
/*每写一个字节调用一次 I2C_EE_ByteWrite 函数*/
for (i=0; i<NumByteToWrite; i++)
{
/*等待 EEPROM 准备完毕*/
I2C_EE_WaitEepromStandbyState();
/*按字节写入数据*/
res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);
}
return res;
}
//等待 EEPROM 处于准备状态
/**
* @brief 等待 EEPROM 到准备状态
* @param 无
* @retval 无
*/
void I2C_EE_WaitEepromStandbyState(void)
{
vu16 SR1_Tmp = 0;
do {
/* 发送起始信号 */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
/* 读 I2C1 SR1 寄存器 */
SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);
/* 发送 EEPROM 地址 + 写方向 */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,
I2C_Direction_Transmitter);
}
// SR1 位 1 ADDR:1 表示地址发送成功,0 表示地址发送没有结束
// 等待地址发送成功
while (!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002));
/* 清除 AF 位 */
I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
//EEPROM 的页写入
/**
* @brief 在 EEPROM 的一个写循环中可以写多个字节,但一次写入的字节数
* 不能超过 EEPROM 页的大小,AT24C02 每页有 8 个字节
* @param
* @param pBuffer:缓冲区指针
* @param WriteAddr:写地址
* @param NumByteToWrite:要写的字节数要求 NumByToWrite 小于页大小
* @retval 正常返回 1,异常返回 0
*/
uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,uint8_t NumByteToWrite)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
}
/* 产生 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
}
/* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2Cx, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (! I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
}
/* 循环发送 NumByteToWrite 个数据 */
while (NumByteToWrite--)
{
/* 发送缓冲区中的数据 */
I2C_SendData(EEPROM_I2Cx, *pBuffer);
/* 指向缓冲区中的下一个数据 */
pBuffer++;
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
}
}
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
return 1;
}
/**
* @brief 从 EEPROM 里面读取一块数据
* @param pBuffer:存放从 EEPROM 读取的数据的缓冲区指针
* @param ReadAddr:接收数据的 EEPROM 的地址
* @param NumByteToRead:要从 EEPROM 读取的字节数
* @retval 正常返回 1,异常返回 0
*/
uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,u16 NumByteToRead)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
}
/* 产生 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2Cx,EEPROM_ADDRESS,I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
}
/*通过重新设置 PE 位清除 EV6 事件 */
I2C_Cmd(EEPROM_I2Cx, ENABLE);
/* 发送要读取的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */
I2C_SendData(EEPROM_I2Cx, ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV8 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
}
/* 产生第二次 I2C 起始信号 */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV5 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
}
/* 发送 EEPROM 设备地址 */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV6 事件并清除标志*/
while (!I2C_CheckEvent(EEPROM_I2Cx,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
}
/* 读取 NumByteToRead 个数据*/
while (NumByteToRead)
{
/*若 NumByteToRead=1,表示已经接收到最后一个数据了,
发送非应答信号,结束传输*/
if (NumByteToRead == 1)
{
/* 发送非应答信号 */
I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE);
/* 发送停止信号 */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
I2CTimeout = I2CT_LONG_TIMEOUT;
while (I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)
{
if ((I2CTimeout--) == 0)
return I2C_TIMEOUT_UserCallback(3);
}
{
/*通过 I2C,从设备中读取一个字节的数据 */
*pBuffer = I2C_ReceiveData(EEPROM_I2Cx);
/* 存储数据的指针指向下一个地址 */
pBuffer++;
/* 接收数据自减 */
NumByteToRead--;
}
}
/* 使能应答,方便下一次 I2C 传输 */
I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);
return 1;
}
模拟I2C程序实现
#include "I2C.h"
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
* @brief:写SDA
* @param:data 写数据
* @retval:无
*/
void I2C_W_SDA(uint8_t data)
{
GPIO_WriteBit(I2C_PORT,I2C_SDA,(BitAction)data);
delay_us(10);
}
/**
* @brief:写SCL
* @param:data 写数据
* @retval:无
*/
void I2C_W_SCL(uint8_t data)
{
GPIO_WriteBit(I2C_PORT,I2C_SCL,(BitAction)data);
delay_us(10);
}
/**
* @brief:读SDA
* @param:无
* @retval:SDA引脚的电平信号
*/
uint8_t I2C_R_SDA(void)
{
uint8_t RData;
RData = GPIO_ReadInputDataBit(I2C_PORT,I2C_SDA);
delay_us(10);
return RData;
}
/**
* @brief:I2C初始化
* @param:无
* @retval:无
*/
void I2C_init(void)
{
//初始化GPIOB时钟
I2C_CLKFUN(I2C_CLKCMD,ENABLE);
//初始化GPIOB
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = I2C_SDA | I2C_SCL;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2C_PORT,&GPIO_InitStruct);
GPIO_SetBits(I2C_PORT,I2C_SDA | I2C_SCL);//将I2C_SDA I2C_SCL默认电平都设置为高电平
}
/**
* @brief:开始时序
* @param:无
* @retval:无
*/
void I2C_Start(void)
{
I2C_W_SCL(1);//释放SDA,确保SDA为高电平
I2C_W_SDA(1);//释放SCL,确保SCL为高电平
I2C_W_SDA(0);//在SCL高电平期间,拉低SDA,产生起始信号
I2C_W_SCL(0);//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
* @brief:停止时序
* @param:无
* @retval:无
*/
void I2C_Stop(void)
{
I2C_W_SDA(0); //拉低SDA,确保SDA为低电平
I2C_W_SCL(1); //释放SCL,使SCL呈现高电平
I2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
/**
* @brief:发送应答时序
* @param:无
* @retval:无
*/
void I2C_SendAck(uint8_t ack)
{
I2C_W_SDA(ack); //主机把应答位数据放到SDA线
I2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
I2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
/**
* @brief:接受应答时序
* @param:无
* @retval:1 应答 0 非应答
*/
uint8_t I2C_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
I2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
I2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = I2C_R_SDA(); //将应答位存储到变量里
I2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
USART
USART
- 通用同步异步收发器
- USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里
- 自带波特率发生器,最高达4.5Mbits/s
- 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)
- 可选校验位(无校验/奇校验/偶校验)
- 支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN
- STM32F103C8T6 USART资源: USART1、 USART2、 USART3
USART框图
USART基本结构
数据帧
- 字长设置
- 停止位
- 起始位侦测
- 数据采样
波特率发生器
- 发送器和接收器的波特率由波特率寄存器BRR里的DIV确定
- 计算公式:波特率 = fPCLK2/1 / (16 * DIV)
数据模式
- HEX模式/十六进制模式/二进制模式:以原始数据的形式显示
- 文本模式/字符模式:以原始数据编码后的形式显示
HEX数据包
文本数据包
HEX数据包接收
- 程序状态机的思想
- 设置三种状态
- 0 等待包头
- 1 接收数据
- 2 等待包尾
- 设置三种状态
void USART1_IRQHandler(void)
{
static uint8_t RState = 0;//读数据的状态
static uint8_t RPtr = 0;//读数据的位置
if((USART_GetITStatus(USART1,USART_IT_RXNE) == SET))
{
RData = USART_ReceiveData(USART1);
if(RState == 0)//0 等待包头
{
if(RData == 0XFF)//判断数据是否为包头数据
{
RState = 1;
RPtr = 0;
}
}
else if(RState == 1)//1 接收数据
{
RDataPacket[RPtr] = RData;
RPtr++;
if(RPtr >= 4)//当接受4个数据后 置状态为2
{
RState = 2;
}
}
else if(RState == 2)//2 等待包尾
{
if(RData == 0XFE)//判断数据是否为包尾数据
{
RState = 0;
RDataFlag = 1;
}
}
}
}
文本数据包接收
UART
- 通用异步收发器
- 串行、异步、全双工
- 不需要通信协议,只需要约定好彼此之间的波特率、起始位、奇偶校验位、停止位、空闲位
USART输出编程流程
- 使能 RX和 TX 引脚 GPIO时钟和 USART时钟;
- 初始化 GPIO,并将 GPIO复用到 USART上;
- 配置 USART 参数;
- 配置中断控制器并使能 USART接收中断;
- 使能 USART;
- 在 USART接收中断服务函数实现数据接收和发送
//NVIC 配置
static void NVIC_Configuration(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* 嵌套向量中断控制器组选择 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
/* 配置 USART 为中断源 */
NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
/* 抢断优先级为 1 */
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
/* 子优先级为 1 */
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
/* 使能中断 */
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
/* 初始化配置 NVIC */
NVIC_Init(&NVIC_InitStructure);
}
//USART 初始化配置
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口 GPIO 的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将 USART Tx 的 GPIO 配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将 USART Rx 的 GPIO 配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
// 配置工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// 串口中断优先级配置
NVIC_Configuration();
// 使能串口接收中断
USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
//字符发送
/***************** 发送一个字符 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
/* 发送一个字节数据到 USART */
USART_SendData(pUSARTx,ch);
/* 等待发送数据寄存器为空 */
while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}
/***************** 发送字符串 **********************/
void Usart_SendString( USART_TypeDef * pUSARTx, char *str)
{
unsigned int k=0;
do {
Usart_SendByte( pUSARTx, *(str + k) );
k++;
} while (*(str + k)!='\0');
/* 等待发送完成 */
while (USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET) {
}
}
//USART 中断服务函数
void DEBUG_USART_IRQHandler(void)//USART1_IRQHandler
{
uint8_t ucTemp;
if (USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
{
ucTemp = USART_ReceiveData( DEBUG_USARTx );
USART_SendData(USARTx,ucTemp);
}
}
重定向
//重定向 prinft 和 和 scanf 函数
//重定向 c 库函数 printf 到串口,重定向后可使用 printf 函数
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到串口 */
USART_SendData(DEBUG_USARTx, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);
return (ch);
}
//重定向 c 库函数 scanf 到串口,重写向后可使用 scanf、getchar 等函数
int fgetc(FILE *f)
{
/* 等待串口输入数据 */
while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USARTx);
}
USART串口输出接受
-
使能 RX和 TX 引脚 GPIO时钟和 USART时钟;
-
初始化 GPIO,并将 GPIO复用到 USART上;
-
配置 USART 参数,配置为输出和输入模式;
-
配置中断控制器并使能 USART接收中断;
-
使能 USART;
-
在 USART接收中断服务函数实现数据接收和发送
#include "stm32f10x.h" // Device header #include "USART.h" uint8_t RData = 0;//接收的数据 uint8_t RDataFlag = 0;//接受标志位 /** * @brief:初始化USART * @param:无 * @retval:无 */ void Usart_Init(void) { //打开GPIO的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); //初始化GPIO GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;//串口的gpio模式设置为复用推挽 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;//pa10串口的TXD GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA,&GPIO_InitStruct); GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;//串口的gpio模式设置为浮空输入 GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;//pa9串口的RXD GPIO_Init(GPIOA,&GPIO_InitStruct); //初始化USART USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate = 115200;//波特率115200 USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//硬件流控 USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_Parity = USART_Parity_No;//校验位 USART_InitStruct.USART_StopBits = USART_StopBits_1;//停止位 USART_InitStruct.USART_WordLength = USART_WordLength_8b;//数据长度8位 USART_Init(USART1,&USART_InitStruct); //打开USART的接受数据中断 USART_ITConfig(USART1,USART_IT_RXNE,ENABLE); //配置NVIC分组 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //初始化NVIC NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;//USART1 中断处理函数 NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //响应优先级 NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2; //抢占优先级 NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;//中断使能 NVIC_Init(&NVIC_InitStruct); //USART1使能 USART_Cmd(USART1,ENABLE); } /** * @brief:发送数据 * @param:Data 发送的数据 * @retval:无 */ void USART_TransferData(uint8_t Data) { USART_SendData(USART1,Data); while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);//等待发送完成 /*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/ } /** * 函 数:获取串口接收标志位 * 参 数:无 * 返 回 值:串口接收标志位,范围:0~1,接收到数据后,标志位置1,读取后标志位自动清零 */ uint8_t USART_GetRFlag(void) { if (RDataFlag == 1) //如果标志位为1 { RDataFlag = 0; return 1; //则返回1,并自动清零标志位 } return 0; //如果标志位为0,则返回0 } /** * @brief:获取接受数据 * @param:无 * @retval:返回接受到的数据 */ uint8_t USART_GetRValue(void) { return RData; } /** * @brief:接收中断处理函数 * @param:无 * @retval:无 */ void USART1_IRQHandler(void) { if((USART_GetITStatus(USART1,USART_IT_RXNE) == SET)) { RData = USART_ReceiveData(USART1); RDataFlag = 1; USART_ClearITPendingBit(USART1,USART_IT_RXNE); } }
int main(void) { Usart_Init(); printf("init success\r\n"); while(1) { if((USART_GetRFlag()) == 1) { USART_TransferData(USART_GetRValue()); USART_ClearFlag(USART1,USART_FLAG_RXNE); } } }
SPI
- 全双工、同步
-
miso 主机输入从机输出
-
mosi 主机输出从机输入
-
sclk 串行时钟信号
-
cs 片选信号
-
SPI通信的四种模式
- 时钟极性 CKP或CPOL
- 将时钟的默认状态设置为高或低
- 0 时钟空闲为低电平
- 1 时钟空闲为高电平
- 时钟相位 CKE或CPHA
- 采集数据时是在时钟信号的具体边沿
- 0 上升沿
- 1 下降沿
ADC
-
ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁
-
12位逐次逼近型ADC,1us转换时间
-
输入电压范围:03.3V,转换结果范围:04095
-
18个输入通道,可测量16个外部和2个内部信号源
-
规则组和注入组两个转换单元
-
模拟看门狗自动监测输入电压范围
-
STM32F103C8T6 ADC资源:ADC1、ADC2,10个外部输入通道
ADC框图
ADC基本结构
输入通道
转换模式
- 单次转换,非扫描模式
-
连续转换,非扫描模式
-
单次转换,扫描模式
- 连续转换,扫描模式
触发控制
数据对齐
- 数据右对齐
- 数据左对齐
转换时间
- AD转换的步骤:采样,保持,量化,编码
- STM32 ADC的总转换时间为:
- TCONV = 采样时间 + 12.5个ADC周期
- 例如:当ADCCLK=14MHz,采样时间为1.5个ADC周期
- TCONV = 1.5 + 12.5 = 14个ADC周期 = 1μs
校准
- ADC有一个内置自校准模式。校准可大幅减小因内部电容器组的变化而造成的准精度误差。校准期间,在每个电容器上都会计算出一个误差修正码(数字值),这个码用于消除在随后的转换中每个电容器上产生的误差
- 建议在每次上电后执行一次校准
- 启动校准前, ADC必须处于关电状态超过至少两个ADC时钟周期
软件触发ADC编程要点
- 打开ADC、GPIO的时钟
- 初始化ADC、GPIO
- 配置规则组通道参数(单次转换,非扫描模式)
- 使能ADC
- ADC校准
- 设置软件触发ADC
- 等待ADC转换
- 读取ADC转化结果
#include "ADC.h"
#include "stm32f10x.h" // Device header
void ADC_init(void)
{
RCC_APB2PeriphClockCmd(ADC_CLOCK,ENABLE);
RCC_APB2PeriphClockCmd(GPIO_CLOCK,ENABLE);
//ÉèÖÃADCʱÖÓ
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//ÉèÖÃADCΪ72/6=12·ÖƵ
//GPIO³õʼ»¯
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStruct.GPIO_Pin = GPIO_PIN_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
//¹æÔò×éͨµÀÅäÖÃ
ADC_RegularChannelConfig(ADCX,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
//ADC³õʼ»¯
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_Init(ADCX,&ADC_InitStruct);
//ADCʹÄÜ
ADC_Cmd(ADCX,ENABLE);
//ADCУ׼
ADC_ResetCalibration(ADCX);
while((ADC_GetResetCalibrationStatus(ADCX) == SET));
ADC_StartCalibration(ADCX);
while((ADC_GetCalibrationStatus(ADCX) == SET));
}
/**
* @brief:»ñÈ¡ADCת»»µÄÖµ
* @param:ÎÞ
* @retval:ADCת»»µÄ½á¹û
*/
uint16_t ADC_GetValue(void)
{
//Èí¼þ´¥·¢ADCת»¯
ADC_SoftwareStartConvCmd(ADCX,ENABLE);
while((ADC_GetFlagStatus(ADCX,ADC_FLAG_EOC) == RESET));
return ADC_GetConversionValue(ADCX);
}
DMA+多通道编程要点
- 打开ADC、GPIO的时钟
- 初始化ADC、GPIO
- 配置规则组通道参数(连续转化,扫描模式)
- 初始化DMA
- DMA使能、开启ADC的DMA
- 使能ADC
- ADC校准
- 设置软件触发ADC
/**
* @brief:ADC¶àͨµÀ Èí¼þ´¥·¢ ³õʼ»¯º¯Êý
* @param:ÎÞ
* @retval:ÎÞ
*/
void ADC_DMA_init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
//´ò¿ªDMAʱÖÓ
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//ÉèÖÃADCʱÖÓ
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//ÉèÖÃADCΪ72/6=12·ÖƵ
//GPIO³õʼ»¯
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
//¹æÔò×éͨµÀÅäÖÃ
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_2,3,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_3,4,ADC_SampleTime_55Cycles5);
//ADC³õʼ»¯
ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_NbrOfChannel = 4;
ADC_InitStruct.ADC_ScanConvMode = ENABLE;
ADC_Init(ADC1,&ADC_InitStruct);
//³õʼ»¯DMA½á¹¹Ìå
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = 4;//»º³åÇø´óС È¡¾öÓÚSize
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//´«Êä·½Ïò ÍâÉè->´æ´¢Æ÷
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStruct.DMA_MemoryDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;//´æ´¢Æ÷µØÖ·×ÔÔö
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)AD_Value;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
DMA_Cmd(DMA1_Channel1,DISABLE);
ADC_DMACmd(ADC1,ENABLE);
//ADCʹÄÜ
ADC_Cmd(ADC1,ENABLE);
//ADCУ׼
ADC_ResetCalibration(ADC1);
while((ADC_GetResetCalibrationStatus(ADC1) == SET));
ADC_StartCalibration(ADC1);
while((ADC_GetCalibrationStatus(ADC1) == SET));
//ADCÈí¼þ´¥·¢
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
DMA
- DMA直接存储器存取
- DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
- 12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)
- 每个通道都支持软件触发和特定的硬件触发
- STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器映像
DMA框图
DMA基本结构
DMA请求
数据宽度与对齐
数据转运+DMA
ADC扫描模式+DMA
DMA运行的条件
- DMA运行的三个条件
- 传输计数器不为0
- MDA使能
- 触发源有信号
- M2M和自动重装不可以同时使能
DMA数据转运编程要点
- 打开DMA时钟
- 初始化DMA
- DMA使能
- 等待DMA完成转运
#include "DMA.h"
#include "stm32f10x.h" // Device header
uint8_t DMA_Size;
/**
* @brief:DMAÊý¾Ý½»»»º¯Êý
* @param:AddrA ÔÊý×éÊ×µØÖ·
* @param:AddrB Ä¿±êÊý×éÊ×µØÖ·
* @param:lenth ×ªÒÆÊý×éµÄ³¤¶È
* @retval:ÎÞ
*/
void DMA_Init(uint32_t AddrA,uint32_t AddrB,uint8_t Size)
{
DMA_Size = Size;
//´ò¿ªDMAʱÖÓ
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//³õʼ»¯DMA½á¹¹Ìå
DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = Size;//»º³åÇø´óС È¡¾öÓÚSize
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//´«Êä·½Ïò ÍâÉè->´æ´¢Æ÷
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;
DMA_InitStruct.DMA_MemoryBaseAddr = AddrA;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;//´æ´¢Æ÷µØÖ·×ÔÔö
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_PeripheralBaseAddr = AddrB;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1,&DMA_InitStruct);
DMA_Cmd(DMA1_Channel1,DISABLE);
}
/**
* @brief:Æô¶¯DMAÍê³ÉDMAתÔË
* @param:ÎÞ
* @retval:ÎÞ
*/
void DMA_Exchange(void)
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,DMA_Size);
DMA_Cmd(DMA1_Channel1,ENABLE);
//µÈ´ýDMA¹¤×÷Íê³É
while((DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET));
//Çå³ýDMAÍê³É¹¤×÷±ê־λ
DMA_ClearFlag(DMA1_FLAG_TC1);
}
看门狗
- 看门狗可以监控程序的运行状态,当程序因为设计漏洞、硬件故障、电磁干扰等原因,出现卡死或跑飞现象时,看门狗能及时复位程序,避免程序陷入长时间的罢工状态,保证系统的可靠性和安全性
- 看门狗本质上是一个定时器,当指定时间范围内,程序没有执行喂狗(重置计数器)操作时,看门狗硬件电路就自动产生复位信号
- STM32内置两个看门狗
- 独立看门狗(IWDG):独立工作,对时间精度要求较低
- 窗口看门狗(WWDG):要求看门狗在精确计时窗口起作用
IWDG框图
IWDG键寄存器
-
键寄存器本质上是控制寄存器,用于控制硬件电路的工作
-
在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来代替控制寄存器写入一位的功能,以降低硬件电路受到干扰的概率
写入键寄存器的值 作用 0xCCCC 启用独立看门狗 0xAAAA IWDG_RLR中的值重新加载到计数器(喂狗) 0x5555 解除IWDG_PR和IWDG_RLR的写保护 0x5555之外的其他值 启用IWDG_PR和IWDG_RLR的写保护
IWDG超时时间
- 超时时间:TIWDG = TLSI × PR预分频系数 × (RL + 1)
- 其中:TLSI = 1 / FLSI
WWDG框图
WWDG工作特性
- 递减计数器T[6:0]的值小于0x40时,WWDG产生复位
- 递减计数器T[6:0]在窗口W[6:0]外被重新装载时,WWDG产生复位
- 递减计数器T[6:0]等于0x40时可以产生早期唤醒中断(EWI),用于重装载计数器以避免WWDG复位
- 定期写入WWDG_CR寄存器(喂狗)以避免WWDG复位
WWDG超时时间
- 超时时间:
- TWWDG = TPCLK1 × 4096 × WDGTB预分频系数 × (T[5:0] + 1)
- 窗口时间:
- TWIN = TPCLK1 × 4096 × WDGTB预分频系数 × (T[5:0] - W[5:0])
- 其中:TPCLK1 = 1 / FPCLK1
IWDG和WWDG对比
IWDG独立看门狗 | WWDG窗口看门狗 | |
---|---|---|
复位 | 计数器减到0后 | 计数器T[5:0]减到0后、过早重装计数器 |
中断 | 无 | 早期唤醒中断 |
时钟源 | LSI(40KHz) | PCLK1(36MHz) |
预分频系数 | 4、8、32、64、128、256 | 1、2、4、8 |
计数器 | 12位 | 6位(有效计数) |
超时时间 | 0.1ms~26214.4ms | 113us~58.25ms |
喂狗方式 | 写入键寄存器,重装固定值RLR | 直接写入计数器,写多少重装多少 |
防误操作 | 键寄存器和写保护 | 无 |
用途 | 独立工作,对时间精度要求较低 | 要求看门狗在精确计时窗口起作用 |
开启时钟 | 不需要 | 需要开启时钟 |
编程要点
- 独立看门狗
- 独立看门狗使能
- 设置预分频值
- 设置重装值
- 重装计数器,喂狗操作
- 独立看门狗使能
- 重装计数器,喂狗操作
- 窗口看门狗
- 开启窗口看门狗的时钟
- 设置预分频
- 设置窗口值
- 使能窗口看门狗并喂狗
- 隔一段时间进行喂狗操作
Unix时间戳
- Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒
- 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量
- 世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间
UTC/GMT
- GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准
- UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
时间戳转换
C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换
函数 | 作用 |
---|---|
time_t time(time_t*); | 获取系统时钟 |
struct tm* gmtime(const time_t*); | 秒计数器转换为日期时间(格林尼治时间) |
struct tm* localtime(const time_t*); | 秒计数器转换为日期时间(当地时间) |
time_t mktime(struct tm*); | 日期时间转换为秒计数器(当地时间) |
char* ctime(const time_t*); | 秒计数器转换为字符串(默认格式) |
char* asctime(const struct tm*); | 日期时间转换为字符串(默认格式) |
size_t strftime(char*, size_t, const char*, const struct tm*); | 日期时间转换为字符串(自定义格式) |
PWR电源控制
- PWR(Power Control)电源控制
- PWR负责管理STM32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能
- 可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务
- 低功耗模式包括睡眠模式(Sleep)、停机模式(Stop)和待机模式(Standby),可在系统空闲时,降低STM32的功耗,延长设备使用时间
PWR电源框图
上电复位和掉电复位
可编程电压监测器
低功耗模式
模式选择
- 执行WFI(Wait For Interrupt)或者WFE(Wait For Event)指令后,STM32进入低功耗模式
睡眠模式
- 执行完WFI/WFE指令后,STM32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- SLEEPONEXIT位决定STM32执行完WFI或WFE后,是立刻进入睡眠,还是等STM32从最低优先级的中断处理程序中退出时进入睡眠
- 在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态
- WFI指令进入睡眠模式,可被任意一个NVIC响应的中断唤醒
- WFE指令进入睡眠模式,可被唤醒事件唤醒
编程要点
- 在循环最后一句添加
__WFI();
- 当有中断来时唤醒MCU
停止模式
- 执行完WFI/WFE指令后,STM32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- 1.8V供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来
- 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
- 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟
- 当电压调节器处于低功耗模式下,系统从停止模式退出时,会有一段额外的启动延时
- WFI指令进入停止模式,可被任意一个EXTI中断唤醒
- WFE指令进入停止模式,可被任意一个EXTI事件唤醒
编程要点
- 开启PWR的时钟
- 设置进入停止模式
- 重新设置系统时钟(重新设置HSE为系统时钟)
待机模式
- 执行完WFI/WFE指令后,STM32进入待机模式,唤醒后程序从头开始运行
- 整个1.8V供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
- 在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
- WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式
编程要点
- 开启PWR的时钟
- 设置进入待机模式
- 等待WKUP、RTC、等等的唤醒
- 如果是WKUP唤醒,需要设置使能WKUP引脚
BKP简介
- BKP可用于存储用户应用程序数据。当VDD(2.03.6V)电源被切断,他们仍然由VBAT(1.83.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
- TAMPER引脚产生的侵入事件将所有备份寄存器内容清除
- RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
- 存储RTC时钟校准寄存器
- 用户数据存储容量:
- 20字节(中容量和小容量)/ 84字节(大容量和互联型)
BKP基本结构
编程要点
- 打开PWR和BKP的时钟
- 备份寄存器BKP的访问使能
- 写入数据到BKP寄存器中
- 从BKP寄存器中读出数据
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
/*备份寄存器访问使能*/
PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问
BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]); //写入测试数据到备份寄存器
Data = BKP_ReadBackupRegister(BKP_DR1); //读取备份寄存器的数据
RTC简介
- RTC(Real Time Clock)实时时钟
- RTC是一个独立的定时器,可为系统提供时钟和日历的功能
- RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.03.6V)断电后可借助VBAT(1.83.6V)供电继续走时
- 32位的可编程计数器,可对应Unix时间戳的秒计数器
- 20位的可编程预分频器,可适配不同频率的输入时钟
- 可选择三种RTC时钟源:
- HSE时钟除以128(通常为8MHz/128)
- LSE振荡器时钟(通常为32.768KHz)
- LSI振荡器时钟(40KHz)
RTC框图
RTC基本结构
硬件电路
RTC操作注意事项
- 执行以下操作将使能对BKP和RTC的访问:
- 设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
- 设置PWR_CR的DBP,使能对BKP和RTC的访问
- 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1
- 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
- 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器
编程要点
- 打开PWR和RTC的时钟
- 备用寄存器BKP访问使能
- 判断RTC是否为初次配置
- 若为初次配置
- 开启LSE的时钟并等待开启成功
- 设置RTC的是时钟来源是LSE
- 等待时钟同步完成
- 等待上次操作完成(等待上一次对 RTC 寄存器的操作完成)
- 设置RTC的预分频数
- 等待上次操作完成(等待上一次对 RTC 寄存器的操作完成)
- RTC设置时间
- 设置BKP备份寄存器中的数据用于是否为初次配置的判断依据(该数据可为自定义)
- 若为不是初次配置
- 等待时钟同步完成
- 等待上次操作完成(等待上一次对 RTC 寄存器的操作完成)
- 完成对RTC的配置
void MyRTC_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
/*备份寄存器访问使能*/
PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
//if成立则执行第一次的RTC配置
{
RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSE
RCC_RTCCLKCmd(ENABLE); //RTCCLK使能
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1Hz
RTC_WaitForLastTask(); //等待上一次操作完成
MyRTC_SetTime(); //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
}
else //RTC不是第一次配置
{
RTC_WaitForSynchro(); //等待时钟同步
RTC_WaitForLastTask(); //等待上一次操作完成
}
}
//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码,使用LSI当作RTCCLK
//LSI无法由备用电源供电,故主电源掉电时,RTC走时会暂停
/*
void MyRTC_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
RCC_LSICmd(ENABLE);
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(40000 - 1);
RTC_WaitForLastTask();
MyRTC_SetTime();
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
RCC_LSICmd(ENABLE); //即使不是第一次配置,也需要再次开启LSI时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}*/
/**
* 函 数:RTC设置时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
*/
void MyRTC_SetTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_date.tm_year = MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体
time_date.tm_mon = MyRTC_Time[1] - 1;
time_date.tm_mday = MyRTC_Time[2];
time_date.tm_hour = MyRTC_Time[3];
time_date.tm_min = MyRTC_Time[4];
time_date.tm_sec = MyRTC_Time[5];
time_cnt = mktime(&time_date) - 8 * 60 * 60; //调用mktime函数,将日期时间转换为秒计数器格式
//- 8 * 60 * 60为东八区的时区调整
RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中
RTC_WaitForLastTask(); //等待上一次操作完成
}
/**
* 函 数:RTC读取时间
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
*/
void MyRTC_ReadTime(void)
{
time_t time_cnt; //定义秒计数器数据类型
struct tm time_date; //定义日期时间数据类型
time_cnt = RTC_GetCounter() + 8 * 60 * 60; //读取RTC的CNT,获取当前的秒计数器
//+ 8 * 60 * 60为东八区的时区调整
time_date = *localtime(&time_cnt); //使用localtime函数,将秒计数器转换为日期时间格式
MyRTC_Time[0] = time_date.tm_year + 1900; //将日期时间结构体赋值给数组的时间
MyRTC_Time[1] = time_date.tm_mon + 1;
MyRTC_Time[2] = time_date.tm_mday;
MyRTC_Time[3] = time_date.tm_hour;
MyRTC_Time[4] = time_date.tm_min;
MyRTC_Time[5] = time_date.tm_sec;
}
FLASH闪存
- STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程
- 读写FLASH的用途:
- 利用程序存储器的剩余空间来保存掉电不丢失的用户数据
- 通过在程序中编程(IAP),实现程序的自我更新
- 在线编程(In-Circuit Programming – ICP)用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序
- 在程序中编程(In-Application Programming – IAP)可以使用微控制器支持的任一种通信接口下载程序
闪存模块组织
FLASH基本结构
FLASH解锁
- FPEC共有三个键值:
- RDPRT键 = 0x000000A5
- KEY1 = 0x45670123
- KEY2 = 0xCDEF89AB
- 解锁:
- 复位后,FPEC被保护,不能写入FLASH_CR
- 在FLASH_KEYR先写入KEY1,再写入KEY2,解锁
- 错误的操作序列会在下次复位前锁死FPEC和FLASH_CR
- 加锁:
- 设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR
使用指针访问存储器
使用指针读指定地址下的存储器:
uint16_t Data = *((__IO uint16_t *)(0x08000000));
使用指针写指定地址下的存储器:
*((__IO uint16_t *)(0x08000000)) = 0x1234;
其中:
#define __IO volatile
程序存储器编程
程序存储器页擦除
程序存储器全擦除
选项字节
- RDP:写入RDPRT键(0x000000A5)后解除读保护
- USER:配置硬件看门狗和进入停机/待机模式是否产生复位
- Data0/1:用户可自定义使用
- WRP0/1/2/3:配置写保护,每一个位对应保护4个存储页(中容量)
选项字节编程
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的编程操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTPG位为1
- 写入要编程的半字到指定的地址
- 等待BSY位变为0
- 读出写入的地址并验证数据
选项字节擦除
- 检查FLASH_SR的BSY位,以确认没有其他正在进行的闪存操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTER位为1
- 设置FLASH_CR的STRT位为1
- 等待BSY位变为0
- 读出被擦除的选择字节并做验证
#include "stm32f10x.h" // Device header
/**
* 函 数:FLASH读取一个32位的字
* 参 数:Address 要读取数据的字地址
* 返 回 值:指定地址下的数据
*/
uint32_t MyFLASH_ReadWord(uint32_t Address)
{
return *((__IO uint32_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH读取一个16位的半字
* 参 数:Address 要读取数据的半字地址
* 返 回 值:指定地址下的数据
*/
uint16_t MyFLASH_ReadHalfWord(uint32_t Address)
{
return *((__IO uint16_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH读取一个8位的字节
* 参 数:Address 要读取数据的字节地址
* 返 回 值:指定地址下的数据
*/
uint8_t MyFLASH_ReadByte(uint32_t Address)
{
return *((__IO uint8_t *)(Address)); //使用指针访问指定地址下的数据并返回
}
/**
* 函 数:FLASH全擦除
* 参 数:无
* 返 回 值:无
* 说 明:调用此函数后,FLASH的所有页都会被擦除,包括程序文件本身,擦除后,程序将不复存在
*/
void MyFLASH_EraseAllPages(void)
{
FLASH_Unlock(); //解锁
FLASH_EraseAllPages(); //全擦除
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH页擦除
* 参 数:PageAddress 要擦除页的页地址
* 返 回 值:无
*/
void MyFLASH_ErasePage(uint32_t PageAddress)
{
FLASH_Unlock(); //解锁
FLASH_ErasePage(PageAddress); //页擦除
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH编程字
* 参 数:Address 要写入数据的字地址
* 参 数:Data 要写入的32位数据
* 返 回 值:无
*/
void MyFLASH_ProgramWord(uint32_t Address, uint32_t Data)
{
FLASH_Unlock(); //解锁
FLASH_ProgramWord(Address, Data); //编程字
FLASH_Lock(); //加锁
}
/**
* 函 数:FLASH编程半字
* 参 数:Address 要写入数据的半字地址
* 参 数:Data 要写入的16位数据
* 返 回 值:无
*/
void MyFLASH_ProgramHalfWord(uint32_t Address, uint16_t Data)
{
FLASH_Unlock(); //解锁
FLASH_ProgramHalfWord(Address, Data); //编程半字
FLASH_Lock(); //加锁
}
#include "stm32f10x.h" // Device header
#include "MyFLASH.h"
#define STORE_START_ADDRESS 0x0800FC00 //存储的起始地址
#define STORE_COUNT 512 //存储数据的个数
uint16_t Store_Data[STORE_COUNT]; //定义SRAM数组
/**
* 函 数:参数存储模块初始化
* 参 数:无
* 返 回 值:无
*/
void Store_Init(void)
{
/*判断是不是第一次使用*/
if (MyFLASH_ReadHalfWord(STORE_START_ADDRESS) != 0xA5A5) //读取第一个半字的标志位,if成立,则执行第一次使用的初始化
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS, 0xA5A5); //在第一个半字写入自己规定的标志位,用于判断是不是第一次使用
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, 0x0000); //除了标志位的有效数据全部清0
}
}
/*上电时,将闪存数据加载回SRAM数组,实现SRAM数组的掉电不丢失*/
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
Store_Data[i] = MyFLASH_ReadHalfWord(STORE_START_ADDRESS + i * 2); //将闪存的数据加载回SRAM数组
}
}
/**
* 函 数:参数存储模块保存数据到闪存
* 参 数:无
* 返 回 值:无
*/
void Store_Save(void)
{
MyFLASH_ErasePage(STORE_START_ADDRESS); //擦除指定页
for (uint16_t i = 0; i < STORE_COUNT; i ++) //循环STORE_COUNT次,包括第一个标志位
{
MyFLASH_ProgramHalfWord(STORE_START_ADDRESS + i * 2, Store_Data[i]); //将SRAM数组的数据备份保存到闪存
}
}
/**
* 函 数:参数存储模块将所有有效数据清0
* 参 数:无
* 返 回 值:无
*/
void Store_Clear(void)
{
for (uint16_t i = 1; i < STORE_COUNT; i ++) //循环STORE_COUNT次,除了第一个标志位
{
Store_Data[i] = 0x0000; //SRAM数组有效数据清0
}
Store_Save(); //保存数据到闪存
}
器件电子签名
- 电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名
- 闪存容量寄存器:
- 基地址:0x1FFF F7E0
- 大小:16位
- 产品唯一身份标识寄存器:
- 基地址: 0x1FFF F7E8
- 大小:96位
硬件外设
LED和蜂鸣器
- LED:发光二极管,正向通电点亮,反向通电不亮
- 有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定
- 无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音
硬件电路
按键
- 按键:常见的输入设备,按下导通,松手断开
- 按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动
传感器模块简介
传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出
硬件电路
OLED简介
电机编码器
- 增量式
- 增量式编码器可利用光电转换原理输出A、B和Z三组方波脉冲;A、B两组脉冲相位差90度,能够判断出电机的旋转方向
- 而Z相为每转一圈输出一个脉冲,用于基准点定位。此编码器原理构造简单,机械平均,并且寿命可达几万小时,具有较强的抗干扰能力,可靠性高。但是是无法输出轴转动的绝对位置信息
- 绝对式
- 绝对式编码器每一个位置对应一个确定的数字码,因此它的示值只与测量的起始和终止位置有关,而与测量的中间过程无关。其位置是由输出代码的读数确定的。当电源断开时,绝对型编码器并不与实际的位置分离。重新上电时,位置读数仍是当前的
- 绝对编码器能够直接进行数字量大的输出,在码盘上会有若干的码道,码道数就是二进制位数。在每条码道上都会由透光与不透光的扇形区域组成,通过采用光电传感器对信号进行采集。在码盘两侧分别设置有光源和光敏元件,这样光敏元件则能够根据是否接受到光信号进行电平的转换,输出二进制数。并且在不同位置输出不同的数字码。从而可以检测绝对位置。但是分辨率是由二进制的位数来决定的,也就是说精度取决于位数。优点:可以直接读出角度坐标的绝对值,没有累积误差,电源切除后位置信息不会丢失。编码器的抗干扰特性、数据的可靠性大大提高了
- 混合式绝对值编码器
- 混合式绝对值编码器,它输出两组信息:一组信息用于检测磁极位置,带有绝对信息功能;另一组则完全同增量式编码器的输出信息
- 旋转变压器
旋转编码器
- 旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向
- 类型:机械触点式/霍尔传感器式/光栅式
硬件电路
舵机
- 舵机是一种根据输入PWM信号占空比来控制输出角度的装置
- 输入PWM信号要求:周期为20ms,高电平宽度为0.5ms~2.5ms
直流电机
- 直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转
- 直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作
- TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向
DHT11温度湿度
概述
- 含有已校准数字信号输出的温湿度复合传感器
- DHT11采用单总线协议
- 温度测量范围为 0~50℃,误差在±2℃
- 湿度的测量范围为 20%~90%RH,误差在±5%RH
引脚定义
Pin | 名称 | 注释 |
---|---|---|
1 | VDD | 供电3-5.5v |
2 | DATA | 串行数据线,单总线 |
3 | GND | 接地 |
协议及数据格式
- 单片机发送一次复位信号后,DHT11 从低功耗模式转换到高速模式,等待主机复位结束后,DHT11 发送响应信号,并拉高总线准备传输数据。
一次完整的数据为 40bit,按照高位在前,低位在后的顺序传输
- 数据格式为:
8bit 湿度整数数据+8bit 湿度小数数据+8bit 温度整数数据+8bit 温度小数数据+8bit 校验和
,一共 5 字节(40bit)数据。由于 DHT11 分辨率只能精确到个位,所以小数部分是数据全为 0。校验和为前 4 个字节数据相加,校验的目的是为了保证数据传输的准确性 DHT11 只有在接收到开始信号后才触发一次温湿度采集
,如果没有接收到主机发送复位信号
,DHT11 不主动进行温湿度采集。当数据采集完毕且无开始信号后,DHT11 自动切换到低速模式
注意:由于 DHT11 时序要求非常严格,所以在操作时序的时候,为了防止中断干扰总线时序,先关闭总中断,操作完毕后再打开总中断
操作时序
- 复位信号(开始信号)
- DHT11 的初始化过程同样分为复位信号和响应信号
- 首先主机拉低总线至少 18ms,然后再拉高总线,延时 20~40us,取中间值 30us,此时复位信号发送完毕
- 从模式下,DHT11接收到开始信号触发一次温湿度采集,
如果没有接收到主机发送开始信号,DHT11不会主动进行温湿度采集
。采集数据后转换到低速模式
- 响应信号
- DHT11 检测到复位信号后,触发一次采样,并拉低总线 80us 表示响应信号,告诉主机数据已经准备好了
- 然后 DHT11 拉高总线 80us,之后开始传输数据。如果检测到响应信号为高电平,则 DHT11 初始化失败,请检查线路是否连接正常
- 接收数据
- 当复位信号发送完毕后,如果检测到总线被拉低,就每隔 1us 计数一次,直至总线拉高,计算低电平时间(DHT响应信号)
- 当总线被拉高后重新计数检测 80us 的高电平。如果检测到响应信号之后的80us 高电平,就准备开始接收数据(DHT拉高准备发送三个数据)
实际上 DHT11 的响应时间并不是标准的 80us,往往存在误差,当响应时间处于 20~100us 之间时就可以认定响应成功
- 数据传输
- DHT11 在拉高总线 80us 后开始传输数据。每 1bit 数据都以 50us 低电平时隙开始,告诉主机开始传输一位数据了
- DHT11 以高电平的长短定义数据位是 0 还是 1,当 50us 低电平时隙过后拉高总线,高电平持续 26~28us 表示数据“0”;持续 70us 表示数据“1”
- 当 最后 1bit 数据传送完毕后,DHT11 拉低总线 50us,表示数据传输完毕,随后总线由上拉电阻拉高进入空闲状态
区分数据0/1的巧法
数据“0”的高电平持续 26~28us,数据“1”的高电平持续70us,每一位数据前都有 50us 的起始时隙。如果我们取一个中间值 40us 来区分数据“0”和数据“1”的时隙
当数据位之前的 50us 低电平时隙过后,总线肯定会拉高,此时延时 40us 后检测总线状态,如果为高,说明此时处于 70us 的时隙,则数据为“1”;如果为低,说明此时处于下一位数据 50us 的开始时隙,那么上一位数据肯定是“0”
为什么延时 40us?
由于误差的原因,数据“0”时隙并不是准确 26~28us,可能比这短,也可能比这长。
当数据“0”时隙大于 26~28us 时,
如果延时太短,无法判断当前处于数据“0”的时隙还是数据“1”的时隙;
如果延时太长,则会错过下一位数据前的开始时隙,导致检测不到后面的数据
代码示例
#ifndef __DHT11_H
#define __DHT11_H
#include "stm32f10x.h" // Device header
#define dht11_high GPIO_SetBits(GPIOB, GPIO_Pin_12)
#define dht11_low GPIO_ResetBits(GPIOB, GPIO_Pin_12)
#define Read_Data GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_12)
void DHT11_GPIO_Init_OUT(void);
void DHT11_GPIO_Init_IN(void);
void DHT11_Start(void);
unsigned char DHT11_REC_Byte(void);
void DHT11_REC_Data(void);
#endif
#include "stm32f10x.h" // Device header
#include "dht11.h"
#include "delay.h"
//数据
unsigned int rec_data[4];
//对于stm32来说,是输出
void DH11_GPIO_Init_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
//对于stm32来说,是输入
void DH11_GPIO_Init_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
//主机发送开始信号
void DHT11_Start(void)
{
DH11_GPIO_Init_OUT(); //输出模式
dht11_high; //先拉高
delay_us(30);
dht11_low; //拉低电平至少18us
delay_ms(20);
dht11_high; //拉高电平20~40us
delay_us(30);
DH11_GPIO_Init_IN(); //输入模式
}
//获取一个字节
char DHT11_Rec_Byte(void)
{
unsigned char i = 0;
unsigned char data;
for(i=0;i<8;i++) //1个数据就是1个字节byte,1个字节byte有8位bit
{
while( Read_Data == 0); //从1bit开始,低电平变高电平,等待低电平结束
delay_us(30); //延迟30us是为了区别数据0和数据1,0只有26~28us
data <<= 1; //左移
if( Read_Data == 1 ) //如果过了30us还是高电平的话就是数据1
{
data |= 1; //数据+1
}
while( Read_Data == 1 ); //高电平变低电平,等待高电平结束
}
return data;
}
//获取数据
void DHT11_REC_Data(void)
{
unsigned int R_H,R_L,T_H,T_L;
unsigned char RH,RL,TH,TL,CHECK;
DHT11_Start(); //主机发送信号
dht11_high; //拉高电平
if( Read_Data == 0 ) //判断DHT11是否响应
{
while( Read_Data == 0); //低电平变高电平,等待低电平结束
while( Read_Data == 1); //高电平变低电平,等待高电平结束
R_H = DHT11_Rec_Byte();
R_L = DHT11_Rec_Byte();
T_H = DHT11_Rec_Byte();
T_L = DHT11_Rec_Byte();
CHECK = DHT11_Rec_Byte(); //接收5个数据
dht11_low; //当最后一bit数据传送完毕后,DHT11拉低总线 50us
delay_us(55); //这里延时55us
dht11_high; //随后总线由上拉电阻拉高进入空闲状态。
if(R_H + R_L + T_H + T_L == CHECK) //和检验位对比,判断校验接收到的数据是否正确
{
RH = R_H;
RL = R_L;
TH = T_H;
TL = T_L;
}
}
rec_data[0] = RH;
rec_data[1] = RL;
rec_data[2] = TH;
rec_data[3] = TL;
}
BH1750光照传感器
概述
- 产生16位二进制数据
- I2C协议
引脚定义
引脚 | 名称 | 说明 |
---|---|---|
1 | VCC | 供电电压源正极 |
2 | SCL | IIC时钟线,时钟输入引脚,由MCU输出时钟 |
3 | SDA | IIC数据线,双向IO口,用来传输数据 |
4 | ADDR | IIC地址线,接GND时器件地址为0100011 ,接VCC时器件地址为1011100 |
5 | GND | 供电电压源负极 |
BH1750的通讯过程
- 发送上电命令
发送的过程和第2步基本一致。就是把测量命令(0x10)改成上电命令(0x01)
- 发送测量命令
发送的测量命令是“连续高分辨率测量(0x10)”。
发送数据的过程和之前讲的OPT3001写入的过程基本一样,先是“起始信号(ST)”,接着是“器件地址+读写位”(器件地址我在上面引脚定义那里有写),然后是应答位,紧接着就是测量的命令“00010000”(关于测量命令,下面会详细说明),然后应答,最后是“结束信号(SP)”。(相比于OPT3001的写入过程,BH1750少了一个发送寄存器地址的步骤,因为它只有一个寄存器,所以就没必要了)
- 等待测量结束
高分辨率连续测量需要等待的时间最长,手册上面写的是平均120ms,最大值180ms,所以为了保证每次读取到的数据都是最新测量的,程序上面可以延时200ms以上,当然也不用太长,浪费时间。如果你用别的测量模式,等待时间都比这个模式要短
- 读取数据
先是“起始信号(ST)”,接着是“器件地址+读写位”,然后是应答位,紧接着接收1个字节的数据(单片机在这个时候要把SDA引脚从输出改成输入了),然后给BH1750发送应答,继续接收1个字节数据,然后不应答(因为我们接收的数据只有2个字节,收完就可以结束通讯了),最后是“结束信号(SP)”
- 计算结果
接收完两个字节还不算完成,因为这个数据还不是测量出来的光照强度值,我们还需要进行计算,计算公式是:光照强度 =(寄存器值[15:0] * 分辨率) / 1.2(单位:勒克斯lx);
因为我们从BH1750寄存器读出来的是2个字节的数据,先接收的是高8位[15:8],后接收的是低8位[7:0],所以我们需要先把这2个字节合成一个数,然后乘上分辨率,再除以1.2即可得到光照值;
BH1750的命令
代码示例
//上面讲了IIC的几个基本的函数,包括了发送1字节和接收1字节
//但是和BH1750通讯,不仅仅是发送1个字节或者接收1个字节那么简单
//我们对BH1750发送命令的时候,是要先发送器件地址+写入位,然后发送指令
//读取数据的时候,需要先发送器件地址+读取位,然后连续读取2个字节
//如果我上面说的你都懂了,那么你就可以去看代码了,如果能跟时序图一一对应上,你就理解代码了
//另外,如果你用的不是BH1750,而是别的IIC通讯的芯片,这两个函数的写法可能也是不同的
//比如OPT3001,发送命令的时候不仅发发送命令数据还需要发送寄存器地址,所以一般函数定义的时候就要定义两个变量
//又或者一些命令是两个字节的,那么你定义的变量类型就需要注意了,函数里面也需要多发送一个字节数据
//这里不理解也无所谓,不影响学习BH1750的驱动,以后你做项目用到了别的芯片你可能就突然理解了
//写入指令
void Single_Write_BH1750(uchar REG_Address)//REG_Address是要写入的指令
{
BH1750_Start(); //起始信号
BH1750_SendByte(SlaveAddress); //发送设备地址+写信号
BH1750_SendByte(REG_Address); //写入指令
BH1750_Stop(); //发送停止信号
}
//读取指令
void mread(void)
{
uchar i;
BH1750_Start(); //起始信号
BH1750_SendByte(SlaveAddress+1); //发送设备地址+读信号
//注意:这里的for函数的i<2和下面的if函数的i==2,我发现以前的工程写的居然是3
//这里其实我们只需要读取2个字节就行了,后面的合成数据也是只用了BUF的前2个字节
//工程文件我没改,这个驱动程序以前也用在了多个项目上,读取3个字节肯定是也可以正常运行的
//但是我觉得还是改成2比较好,你们可以测试一下改成2有没有问题,测试之后一定要告诉我结果,谢谢!!
for (i=0; i<2; i++) //连续读取2个数据,存储到BUF里面
{
BUF[i] = BH1750_RecvByte(); //BUF[0]存储高8位,BUF[1]存储低8位
if (i == 1)
{
BH1750_SendACK(1); //最后一个数据需要回NOACK
}
else
{
BH1750_SendACK(0); //回应ACK
}
}
BH1750_Stop(); //停止信号
delay_ms(5);
}
//初始化BH1750,根据需要请参考pdf进行修改****
void Init_BH1750()
{
GPIO_InitTypeDef GPIO_InitStruct;
/*开启GPIOB的外设时钟*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = sda | scl ;
GPIO_Init(bh1750_PORT,&GPIO_InitStruct);
Single_Write_BH1750(0x01);
delay_ms(180); //延时180ms
}
float read_BH1750(void)
{
int dis_data; //变量
float temp1;
float temp2;
Single_Write_BH1750(0x01); //发送上电命令(0x01)
Single_Write_BH1750(0x10); //发送高分辨率连续测量命令(0x10)
delay_ms(200); //等待测量结束,其实延时180ms就行了,延时200ms只是预留多一点时间,保证通讯万无一失
mread(); //连续读出数据,存储在BUF中
dis_data=BUF[0];
dis_data=(dis_data<<8)+BUF[1]; //2个字节合成数据
temp1=dis_data/1.2;//计算光照度
temp2=10*dis_data/1.2;//把光照度放大10倍,目的是把小数点后一位数据也提取出来
temp2=(int)temp2%10;//求余得到小数点后一位
OLED_ShowString(87,2,".",12); //OLED显示小数点
OLED_ShowNum(94,2,temp2,1,12);//OLED显示小数
return temp1;//返回整数部分
}
//这里写的程序还是有点乱的,小数部分直接在read_BH1750()显示,整数部分返回,在main()函数调用的时候显示
//这...其实最好是要么都在这个函数显示,要么把temp1和temp2改成全局变量,然后都在main函数显示
//这个变量的名字也是[捂脸],算了算了,往事不堪回首。要吐槽的地方有点多,也没时间去一一改了
//不过其实也不影响你们学IIC通讯的编程方式,就这样吧
证每次读取到的数据都是最新测量的,程序上面可以延时200ms以上,当然也不用太长,浪费时间。如果你用别的测量模式,等待时间都比这个模式要短
- 读取数据
先是“起始信号(ST)”,接着是“器件地址+读写位”,然后是应答位,紧接着接收1个字节的数据(单片机在这个时候要把SDA引脚从输出改成输入了),然后给BH1750发送应答,继续接收1个字节数据,然后不应答(因为我们接收的数据只有2个字节,收完就可以结束通讯了),最后是“结束信号(SP)”
- 计算结果
接收完两个字节还不算完成,因为这个数据还不是测量出来的光照强度值,我们还需要进行计算,计算公式是:光照强度 =(寄存器值[15:0] * 分辨率) / 1.2(单位:勒克斯lx);
因为我们从BH1750寄存器读出来的是2个字节的数据,先接收的是高8位[15:8],后接收的是低8位[7:0],所以我们需要先把这2个字节合成一个数,然后乘上分辨率,再除以1.2即可得到光照值;
BH1750的命令
[外链图片转存中…(img-fPJayKLM-1706423987134)]
代码示例
//上面讲了IIC的几个基本的函数,包括了发送1字节和接收1字节
//但是和BH1750通讯,不仅仅是发送1个字节或者接收1个字节那么简单
//我们对BH1750发送命令的时候,是要先发送器件地址+写入位,然后发送指令
//读取数据的时候,需要先发送器件地址+读取位,然后连续读取2个字节
//如果我上面说的你都懂了,那么你就可以去看代码了,如果能跟时序图一一对应上,你就理解代码了
//另外,如果你用的不是BH1750,而是别的IIC通讯的芯片,这两个函数的写法可能也是不同的
//比如OPT3001,发送命令的时候不仅发发送命令数据还需要发送寄存器地址,所以一般函数定义的时候就要定义两个变量
//又或者一些命令是两个字节的,那么你定义的变量类型就需要注意了,函数里面也需要多发送一个字节数据
//这里不理解也无所谓,不影响学习BH1750的驱动,以后你做项目用到了别的芯片你可能就突然理解了
//写入指令
void Single_Write_BH1750(uchar REG_Address)//REG_Address是要写入的指令
{
BH1750_Start(); //起始信号
BH1750_SendByte(SlaveAddress); //发送设备地址+写信号
BH1750_SendByte(REG_Address); //写入指令
BH1750_Stop(); //发送停止信号
}
//读取指令
void mread(void)
{
uchar i;
BH1750_Start(); //起始信号
BH1750_SendByte(SlaveAddress+1); //发送设备地址+读信号
//注意:这里的for函数的i<2和下面的if函数的i==2,我发现以前的工程写的居然是3
//这里其实我们只需要读取2个字节就行了,后面的合成数据也是只用了BUF的前2个字节
//工程文件我没改,这个驱动程序以前也用在了多个项目上,读取3个字节肯定是也可以正常运行的
//但是我觉得还是改成2比较好,你们可以测试一下改成2有没有问题,测试之后一定要告诉我结果,谢谢!!
for (i=0; i<2; i++) //连续读取2个数据,存储到BUF里面
{
BUF[i] = BH1750_RecvByte(); //BUF[0]存储高8位,BUF[1]存储低8位
if (i == 1)
{
BH1750_SendACK(1); //最后一个数据需要回NOACK
}
else
{
BH1750_SendACK(0); //回应ACK
}
}
BH1750_Stop(); //停止信号
delay_ms(5);
}
//初始化BH1750,根据需要请参考pdf进行修改****
void Init_BH1750()
{
GPIO_InitTypeDef GPIO_InitStruct;
/*开启GPIOB的外设时钟*/
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStruct.GPIO_Pin = sda | scl ;
GPIO_Init(bh1750_PORT,&GPIO_InitStruct);
Single_Write_BH1750(0x01);
delay_ms(180); //延时180ms
}
float read_BH1750(void)
{
int dis_data; //变量
float temp1;
float temp2;
Single_Write_BH1750(0x01); //发送上电命令(0x01)
Single_Write_BH1750(0x10); //发送高分辨率连续测量命令(0x10)
delay_ms(200); //等待测量结束,其实延时180ms就行了,延时200ms只是预留多一点时间,保证通讯万无一失
mread(); //连续读出数据,存储在BUF中
dis_data=BUF[0];
dis_data=(dis_data<<8)+BUF[1]; //2个字节合成数据
temp1=dis_data/1.2;//计算光照度
temp2=10*dis_data/1.2;//把光照度放大10倍,目的是把小数点后一位数据也提取出来
temp2=(int)temp2%10;//求余得到小数点后一位
OLED_ShowString(87,2,".",12); //OLED显示小数点
OLED_ShowNum(94,2,temp2,1,12);//OLED显示小数
return temp1;//返回整数部分
}
//这里写的程序还是有点乱的,小数部分直接在read_BH1750()显示,整数部分返回,在main()函数调用的时候显示
//这...其实最好是要么都在这个函数显示,要么把temp1和temp2改成全局变量,然后都在main函数显示
//这个变量的名字也是[捂脸],算了算了,往事不堪回首。要吐槽的地方有点多,也没时间去一一改了
//不过其实也不影响你们学IIC通讯的编程方式,就这样吧