STM32

时间单位

1HZ的周期是1S,1MHz表示每秒钟有1百万个周期,也可以理解为每个周期持续1/1MHz = 1微秒

STM32C语言知识

在STM32中FLash是只读不能改的

而用const定义的全局变量也是只读不能改的

所以用const定义的全局变量是定义在flash中的

当我们定义一些不用修改的变量就可以用const定义,这样可以节省RAM的空间

在STM32中,全局变量通常存储在SRAM中。这是因为SRAM是易于访问和修改的存储器类型,并且可以提供较快的读写速度。此外,SRAM也适用于存储栈和堆等数据结构

Flash则通常用于存储程序代码和常量数据,如常量字符串、数组等。它是一种非易失性存储器,不会因为断电而丢失数据,因此适用于存储程序代码,以便下次开机时CPU能够恢复执行。

需要注意的是,如果全局变量被声明为const类型(即只读常量),则它们将存储在Flash中。这是因为const变量是只读的,它们的值无法在运行时修改,因此可以安全地存储在Flash中。


const uint32_t aa = 0x66;//全局变量
int main()
{
    const uint32_t bb = 0x66;
    
    OLED_Init();
    OLED_ShowHexNum(1,1,aa,2);
    OLED_ShowHexNum(2,1,(uint32_t)&aa,8);//打印地址 为080开头
    OLED_ShowHexNum(3,1,(uint32_t)&bb,8);//打印地址 为20开头

宏定义


/*错误示范*/
#define f2(x) (x)*30
#define f1(x) (x*30)

int main(void) {
    printf("f1:%d\n",f1(5+2));//5+2*30=65  
    printf("f2:%d",180/f2(1));//180/1*30
    return 0;
}

/*运行结果*/
f1:65
f2:5400
/*正确示范*/
#define f2(x) ((x)*30)
int main(void) {
    printf("f1:%d\n",f2(5+2));//(5+2)*30 
    printf("f2:%d",180/f2(1));//180/(x*30)
    return 0;
}

/*运行结果*/
f1:210
f2:6

位运算打印二进制


int main(){
unsigned int mask =0x80000000 ;
unsigned int number  = 0xffffffff;
for(; mask; mask>>=1)
{
    printf("%d",number & mask? 1:0);
}
    return 0;
}

位段


void PriBin(unsigned char a); //定义一个8位寄存器的结构体
    typedef struct
    {
        unsigned char leading : 3; //从最低位开始,占3个bit,下面一样按顺序
        unsigned char bit1:1; //占1位
        unsigned char bit2:1;//占1位
        unsigned char empty: 3;
    }Reg;

int main(){
    Reg MyReg;
    MyReg.leading = 1; //0000 0(001)
    MyReg.bit1 = 1; //0000 (1)00
    MyReg.bit2 = 1;//000(1) 0000
    MyReg.empty = 0;

    PriBin(*(char*)&MyReg);
    return 0;
}

void PriBin(unsigned char a)
{
    unsigned char mask =0x80 ;
    for(; mask; mask>>=1)
    {
        printf("%d",a & mask? 1:0);
    }
}

/*打印结果*/
00011001

STM32如何通过结构体范围寄存器地址


OLED_ShowHexNum(3,1,(uint32_t)&ADC1->DR,8);

/*打印结果*/
4001244C //0x4001 2400为外设寄存器起始地址 而0x4c为DR的偏移量

库函数定义文件中


/*以下构成了ADC1的基地址*/ 
//定义ADC1为指针的ADC1_BASE,为ADC1的起始地址
#define ADC1     ((ADC_TypeDef *) ADC1_BASE) 

#define ADC1_BASE        (APB2PERIPH_BASE + 0x2400) //地址为0x40012400

#define APB2PERIPH_BASE   (PERIPH_BASE + 0x10000) //地址为0x40010000

#define PERIPH_BASE       ((uint32_t)0x40000000)  //地址为0x40000000

/*所以通过指针类型,指向ADC1地址0x40012400,在这地址里面创建成员,刚好为ADC1中的寄存器的偏移地址*/
typedef struct
{
  __IO uint32_t SR;
  __IO uint32_t CR1;
  __IO uint32_t CR2;
  __IO uint32_t SMPR1;
  __IO uint32_t SMPR2;
  __IO uint32_t JOFR1;
  __IO uint32_t JOFR2;
  __IO uint32_t JOFR3;
  __IO uint32_t JOFR4;
  __IO uint32_t HTR;
  __IO uint32_t LTR;
  __IO uint32_t SQR1;
  __IO uint32_t SQR2;
  __IO uint32_t SQR3;
  __IO uint32_t JSQR;
  __IO uint32_t JDR1;
  __IO uint32_t JDR2;
  __IO uint32_t JDR3;
  __IO uint32_t JDR4;
  __IO uint32_t DR;
} ADC_TypeDef;

配置寄存器基础知识

CR(Control Register)--控制寄存器

DR(Data Register)--数据寄存器

SR(State Register)--状态寄存器

EV(Event)--事件

STM32外设硬件

ICode指令总线,Dcode数据总线,System系统总线

ICode指令总线,Dcode数据总线,主要连接Flash闪存

AHB系统总线挂载外设复位和时钟控制,然后两个桥接接其他外设

性能AHB72MHZ>APB272MHZ>APB136MHZ

DMA:是一种通过硬件控制的方式,实现高速数据传输的技术,可以大大减轻CPU的负担,提高系统的效率。使用DMA时,可以通过配置寄存器的方式,指定数据传输的源地址、目的地址、传输长度等参数,然后启动DMA传输,系统就会通过硬件自动完成数据传输,无需CPU的干预,从而提高系统的效率。

最小系统电路

上电瞬间,电容为充电状态,可视为短路,NRST接低被强下拉,而NRST低电平电路复位,等待电容充满电后为断路状态,所以NRST被拉到高电平

K1:为按键复位电路,按下NRST接地,强下拉为低电平

一般用跳线帽连接13引脚,BOOTO接VCC,启动,接35反之

库函数点灯


#include "stm32f10x.h"                  // Device header

int main()
{
    /*RCC模块控制系统时钟和外设时钟的使能和配置
    )函数是RCC模块提供的一个函数,用于控制APB2总线上外设时钟的使能和禁用。*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//RCC模块
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    /*STM32中用于初始化GPIO引脚的函数。通过该函数,我们可以对GPIO的各项参数进行配置,
例如GPIO的工作模式、输出模式、输入模式、输出类型、输入类型、上拉/下拉等。*/
    GPIO_Init(GPIOC, &GPIO_InitStructure);
    GPIO_SetBits(GPIOC,GPIO_Pin_13);
    GPIO_ResetBits(GPIOC,GPIO_Pin_13);
    while(1){}
}

GPIO

GPIO和AFIO的区别

AFIO(Alternate Function Input/Output)和 GPIO(General Purpose Input/Output)是 STM32 微控制器中两种不同类型的引脚。

AFIO 引脚是可编程的、复用的引脚,通常用于连接芯片内部的各种外设,例如定时器、串口、I2C、SPI 等。使用 AFIO 引脚需要将引脚重映射(Pin Remapping)到对应的外设上,以实现外设与引脚的连接。在 STM32 中,AFIO 的相关配置和控制寄存器可以通过 AFIO 控制器进行设置。

GPIO 引脚则是通用的输入输出引脚,可用于连接外部设备,例如按键、LED 等。STM32 微控制器中的 GPIO 引脚通常是可编程的,可以通过 GPIO 控制器进行配置和控制,以实现输入输出等功能。

因此,AFIO 和 GPIO 的主要区别在于它们连接的设备不同。AFIO 引脚通常用于连接芯片内部的各种外设,而 GPIO 引脚则用于连接外部设备,例如按键、LED 等。需要注意的是,有些 STM32 微控制器的引脚可能同时具有 AFIO 和 GPIO 的功能,这时需要根据具体的应用场景来选择相应的引脚模式和配置方式。

GPIO的介绍

所有的GPIO都是挂载在APB2外设总线上,其中GPIO外设的名称是按照GPIOA,GPIOB....等等来进行命名,每个GPIO有16个引脚编号是0-15

stm32是32位的 但引脚都是16位的,高16位是没有数据的

寄存器只负责存储数据

施密特触发器,因为引脚输出输入的信号波有时会失真,出现很多波形(红线)

而施密特触发定义了一个上限和一个下限(绿线),高于上绿线为高电平,低于下限绿线转为低电平

经过比较两个阈值来进行判断,这样就可以避免因输出信号的抖动而产生变化波形

这样就能产生整型后的波形(蓝线),就可以直接输出给寄存器了

驱动发光二极管的

第一种为低电平驱动

PA0口输出低电平与外部3.3v形成电压差,二极管通电

第一种为高电平驱动

PA0口输出高电平与外部0v形成电压差,二极管通电

R1为限流电阻,既可以防止电流过大烧毁LED也可以通过控制R1来控制LED的亮度

如果是推挽输出的话,这两种接法都可以点亮LED,因为推挽输出高低电平都有较强的驱动能力

但一般选择第一种的多,因为一般单片机里很多都认为低电平驱动能力强,高电平驱动能力弱

3-1 LED流水灯


/*APB2时钟使能*/  
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
 /*GPIO引脚初始化*/   
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

    while(1)
    {
        int i = 0;
        for(i=0; i<8; i++)
        {
            GPIO_Write(GPIOA,~(0x0001<<i));
            Delay_ms(100);
        }
    }
}

4-1 光敏传感器控制蜂鸣器

电路中如果电容一端接地,一般为起到过滤作用,让输入的信号更加平滑,分析电路时可以忽略

R1与光敏电阻N1串联主要起到了分压的作用

可以用上下拉电阻原理来分析电路,光线越强N1阻值越大,电流从A0出的更多,反之,电流直接通过N1接地

LM393芯片

主要运用了电压比较器,对模拟输入电压的大小输出数字电压,

IN+口接上一个图的IN+口,然后IN-接可调电阻,通过可调电阻的不断变化,变到和IN+口的电压一致时,就输出到DO口

蜂鸣器初始化代码

/**
  * @brief  初始化GPIOB Pin12的接口
  * @param  无
  * @retval 无
  */
void Buzzer_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    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);
}
/**
  * @brief  关闭蜂鸣器
  * @param  无
  * @retval 无
  */
void Buzzer_OFF()
{
    GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
/**
  * @brief  关闭蜂鸣器
  * @param  无
  * @retval 无
  */
void Buzzer_ON()
{
    GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
/**
  * @brief  转换蜂鸣器状态
  * @param  无
  * @retval 无
  */
void Buzzer_Turn()
{
    if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_13==0))
    {
        GPIO_ResetBits(GPIOB,GPIO_Pin_12);
    }
    else
    {
        GPIO_SetBits(GPIOB,GPIO_Pin_12);
    }
}
光敏电阻初始化

void Lightsensor_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);
}
//放回读取光敏电阻所在引脚的电平状态
uint8_t Lightsensor_Get()
{
   
    return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}

OLED

EXTI外部中断

在STM32中它是用来统一分配中断优先级和管理中断的NVIC是一个内核外设,是CPU的小助手

NVIC通过判断优先级,然后通过右边的一个输出口来告诉CPU你该处理哪个中断了

4位决定,也就是说4位2进制来决定他们可以分配多少个序号

若内核正在执行C的中断服务函数,则它能被抢占优先级更高的中 断A打断,由于B和C的抢占优先级相同,所以C不能被B打断。但如果B和C中断是同时到达的,内核就会首先响应响应优先级别更高的B中 断 。

中断响应是正常的流程,引脚电平变化触发中断

事件响应不会触发中断,而是触发别的外设操作,属于外设之间的联合工作

  • 中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行

  • 中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源

  • 中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前的中断程序,转而去处理新的中断程序,处理完后依次进行返回

  • NVIC:NVIC的中断优先级由优先级寄存器的4位(0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级

  • 抢占优先级高的可以进行中断嵌套,响应优先级高的可以进行优先排队,抢占优先级和响应优先级均相同的按中断号排队

  • EXTI:(Extern Interrupt)外部中断

  • EXTI可以检测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序

  • 支持的触发方式:上升沿/下降沿/双边沿/软件触发

  • 支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断

  • 通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒

  • 触发响应方式:中断响应/事件响应

AFIO选择中断引脚,外部中断的9-5,15-10会触发同一个中断函数,再根据标志位来区分到底是哪个中断进来的

这个梯形为数据选择器,它会从多个输入中选取一个输出

在STM32中AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择

或、与、非门

EXTI框图

中断挂起寄存器:当某一个中断A发生时,正处在另一个中断B的运行中,如果中断B与A同一个优先级或者高于A优先级,则A中断就无法立即得到处理,需要等待B中断运行结束后才能得以运行(严谨地说是等待所有优先级高的中断运行结束),此时A中断在等待过程中就叫做被挂起了。这个挂起的状态就保存在一个寄存器中----称为中断挂起寄存器;

中断屏蔽寄存器:可以这么理解,中断屏蔽寄存器就是中断的总开关,打开了这个寄存器,相当于屏蔽了所有的中断,中断使能寄存器就是打开和关闭中断,这个一般都是对某个中断进行设置的

使用旋转编码器的时候就可以用STM32的外部中断了,因为你一直不去拧动旋转编码器的话,是什么都不会产生的,而当你拧动它的时候,它会立即向STM32发送脉冲,STM32就要立即来接受脉冲信号

光栅式:为了测速还需要配备一个光栅编码盘,当它旋转时,红外传感器的红外光就会出现遮挡、透过、遮挡透过....对应模块输出的高低电平交替的方波,方波的个数代表转过的角度,方波的频率表示转速,于是我们就用中断去读取方波的边沿,以此判断位置和速度,只能测位置和速度

机械触点式:左右两边分别有金属触点,然后通过旋转的金属编码盘,来和触点接触,从而产生交变方波

因为相位差别90度,所以可以判断旋转方向的不同,不适合测电机转速

配置外部中断

步骤如下:

  1. 配置RCC,把我们这里涉及到的外设的时钟都打开,不打开时钟外设是没有办法工作的。

  1. 配置GPIO,选择我们的端口为输入模式。

  1. 配置AFIO,选择我们用的这一路GPIO,连接到后面的EXIT。

  1. 配置EXTI,选择边沿触发方式,比如上升沿,下降沿或双边沿。还有选择触发响应方式,可以选择中断响应和事件响应,这里我们一般都是中断响应。

  1. 配置NVIC,给我们的中断选择一个合适的优先级。最后,通过NVIC,外部中断信号就能进入CPU了。

  1. 最后是找到相应的中断函数。

配置RCC

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//打开GFIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//打开AFIO的时钟
/*EXTI 和 NVIC的时钟默认打开*/
配置GPIO

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//可以查手册
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_Init(GPIOB,&GPIO_InitStructure);
配置AFIO

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource13);
/*EXTI的线路配置,AFIO的函数在GPIO 中*/
配置EXTI

    EXTI_InitTypeDef EXTI_InitStructure;
    EXTI_InitStructure.EXTI_Line = EXTI_Line13; //选择线路
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //选择外部中断模式
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //选择下降沿触发模式
    EXTI_Init(&EXTI_InitStructure);
/*因为EXTI为内核外设所以函数在misic中*/
配置NVIC

    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//分配优先级组
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //10-15的引脚
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //分配抢占优先
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; //分配响应优先
    NVIC_Init(&NVIC_InitStructure);
中断函数

void EXTI15_10_IRQHandler()
{
    if(EXTI_GetITStatus(EXTI_Line13)==SET)//读取引脚挂起状态
    {
        
    }
}
/*在启动文件当中*/

使用中断的建议:最后不要在中断函数中执行时间过长的代码,以及不要在主函数和中断函数中都用来控制同一个硬件或者调用相同的函数,在中断函数中最好是操作标志位和变量

TIM简介

基本定时器

定时器的核心:

1.预分频器:预分频器(Prescaler-PSC)用来将定时器时钟源进行分频输出。

预分频器的值由寄存器TIMx_PSC设定,是一个16位正整数值。

在STM32系统中,定时器的时钟源为内部时钟时,其频率一般都比较高,以STM32F103的TIM1为例,其总线时钟最大为72MHz,体现在16位的定时器上的效果就是从0计数到65535上溢只需要0.9毫秒。如果我们需要更长时间的定时间隔,那么就需要预分频器对时钟进行分频处理,以降低定时器时钟(CK_CNT)的频率。

除此之外,也可以通过配置预分频器,来获取想要的定时器时钟频率。依然以上边的TIM1为例,如果我们想获取一个精确的1ms中断,如果不分频,72MHz的时钟对应每周期1/72us,十分不利于计算。这时候使用预分频器将其72分频后为1MHz,每周期1us,1000个计时周期即为1ms,这样既便于计算,定时也更加精确。

预分频器的工作的工作原理是,定时器时钟源每tick一次,预分频器计数器值+1,直到达到预分频器的设定值,然后再tick一次后计数器归零,同时,CNT计数器值+1。

如果预分频器是0就不分频,输出频率=输入频率=72mhz

1就是二分频,输出频率=输入频率/2=72mhz/2

2就是三分频,输出频率=输入频率/3=72mhz/3

计数器:预分频器每来一个上升沿就加一

自动重装寄存器:就是存储我们的计数目标,计到目标值发出中断信号让计数器自动清零

(折线箭头表示为中断信号称为更新中断,向下的箭头是触发事件)

主模式:如果我们要用DAC去输出波形,如果用,中断去输出的话,会让主函数不断被打断影响了主程序的其他运行,以及其他中断程序,如果用主模式的话,就会采用更新事件的映射到TRGO上,然后TRGO直接接到DAC的触发转换引脚上

通用定时器

通道不同

基本定时器只能用内部时钟,而通用定时器可以通过外部输出时钟,通过TIMx_ETR引脚输出

像寄存器下方有个黑影的,说明它们都配备有数据缓存寄存器(影子寄存器)。

缓存寄存器:就像预分频器改变了分频,但时钟周期还没结束,就会打乱,所以加了一个缓冲寄存器,先把数据存进里面,等时钟周期结束,一个新的时钟开始的同时,再把数据输出改变时钟的频率

自动重载中的缓存寄存器也一样,先把要改变的值存到缓存寄存器,等时钟一个周期结束后,再重新开始

就是为了值的变化和更新事件同步发生,防止运行途中发生数据错误

● 内部时钟(CK_INT)

● 外部时钟模式1:外部输入脚(TIx)

● 外部时钟模式2:外部触发输入(ETR)

● 内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器

定时器配置

时钟信号使能

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
时钟模式源选择

TIM_InternalClockConfig(TIM2);//将时钟源选择内部时钟
时基单元的配置

    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//滤波器不分频
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ;//向上计数模式
    TIM_TimeBaseInitStructure.TIM_Period = 10000-1;//计数到10000就溢出
    TIM_TimeBaseInitStructure.TIM_Prescaler = 7200-1;//以7200的频率计数
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//高级定时器才会用到
    TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
定时器使能

TIM_ClearFlag(TIM2, TIM_FLAG_Update);//避免刚上电就进入中断一次
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);//选择TIM_IT_Update为中断源
  • TIM_IT_Update模式:表示定时器溢出中断模式,当计数器计数到最大值(ARR)时,会产生溢出中断。

  • TIM_IT_CC1模式:表示定时器比较模式1,当计数器的值等于捕获/比较寄存器1(CCR1)的值时,会产生比较中断。

  • TIM_IT_CC2模式:表示定时器比较模式2,当计数器的值等于捕获/比较寄存器2(CCR2)的值时,会产生比较中断。

  • TIM_IT_CC3模式:表示定时器比较模式3,当计数器的值等于捕获/比较寄存器3(CCR3)的值时,会产生比较中断。

  • TIM_IT_CC4模式:表示定时器比较模式4,当计数器的值等于捕获/比较寄存器4(CCR4)的值时,会产生比较中断。

这些模式的区别在于它们触发中断的时机和触发条件不同。以下是一个示例,说明了这些模式的实际应用。

例如,假设我们需要控制一个LED灯以一定的频率闪烁,可以使用TIM定时器的TIM_IT_Update模式。具体步骤如下:

  1. 初始化TIM定时器,设置计数器的时钟源、计数模式、预分频值和自动重载寄存器(ARR)等参数,使得计数器能够按照一定的频率进行计数。

  1. 开启定时器中断,使得当计数器计数到最大值(ARR)时,能够产生溢出中断。

  1. 在中断服务程序中,控制LED灯的状态翻转,实现闪烁效果。

另外,如果需要在特定的时间点产生中断,并执行一些特定的操作,可以使用定时器比较模式(TIM_IT_CC1/TIM_IT_CC2/TIM_IT_CC3/TIM_IT_CC4),将捕获/比较寄存器(CCR1/CCR2/CCR3/CCR4)设置为相应的时间点,当计数器的值达到该时间点时,会触发中断,并执行相应的操作。例如,在控制电机转动的应用中,可以使用定时器比较模式来产生PWM信号,从而控制电机的转速和方向。

NVIC中断配置

    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);

    TIM_Cmd(TIM2, ENABLE);//使能定时器

中断函数

void TIM2_IRQHandler()
{
/*因为一个中断函数里面可能有多个中断源,你要判断是哪一个中断源产生的中断*/
    if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET)//判断中断标志位
    {
        TIM_ClearITPendingBit(TIM2,TIM_IT_Update);//清除中断申请位
    }
}

TIM输出比较

  1. CK_PSC:是指定时器时钟的源。在STM32中,定时器可以使用内部时钟或外部时钟源。CK_PSC参数就是用来选择定时器时钟源的。

  1. PSC:是预分频器的值,用于将定时器时钟源的频率降低到所需要的频率。例如,如果需要50Hz的PWM信号,而定时器时钟源的频率为72MHz,那么需要将频率降低至1.44MHz,此时需要将PSC设置为72000/1440000-1=49。

  1. ARR:自动重装载寄存器,用于设置计数器的计数周期。当计数器的值达到ARR的值时,计数器将自动重置为0,并产生计数溢出中断。

VM是和电机一个电压5v就接5v,VCC和控制端一个电压,例如用stm32就是3.3v

PWM配置

配置PWM周期的方法

根据PWM的工作原理,当定时器的计数值从0计数到TIM_Period的值时,输出一个PWM周期。因此,PWM周期可以通过以下公式计算:

PWM周期 = 时钟周期 × (TIM_Period +

其中,时钟周期由预分频器分频系数和STM32的主频共同决定。

将PWM周期设置为20ms,即20000微秒,代入上述公式,可得:

20000 = 1 / 72MHz × (TIM_Period + 1) × TIM_Prescaler

这个公式是用来计算PWM信号的周期的。下面对该公式进行解释:

  • 20000:表示所需的PWM信号周期,以微秒为单位。在这个例子中,我们将PWM周期设置为20毫秒,即20000微秒。

  • 1 / 72MHz:表示计时器的时钟周期时间。在这个例子中,我们假设计时器的时钟频率为72 MHz,即1秒钟内有72000000个计时周期。

  • TIM_Period:表示计时器的周期值。当计时器的值达到TIM_Period时,计时器会重置。通过调整TIM_Period的值,可以控制PWM信号的周期长度。需要注意的是,实际的TIM_Period值应该比计算得到的值小1,因为计时器是从0开始计数的。

  • TIM_Prescaler:表示计时器的预分频系数。该值决定了计时器的时钟频率。通过调整TIM_Prescaler的值,可以改变计时器的时钟频率,进而影响PWM信号的周期长度。

将TIM_Period和TIM_Prescaler分别设为整数,可以通过暴力搜索的方式来寻找合适的值。

以TIM_Prescaler为例,可以从较大的值开始搜索,例如:

  • 当TIM_Prescaler=719时,TIM_Period应该为2000左右

  • 当TIM_Prescaler=359时,TIM_Period应该为4000左右

  • 当TIM_Prescaler=179时,TIM_Period应该为8000左右

以此类推,可以不断调整TIM_Period和TIM_Prescaler的值,以满足20ms的PWM周期要求。

当TIM_Period为2000时,说明一个周期里面会计数2000次,

假设当TIM_Pulse(CCR比较值)=500时,说计数到500时电平就会进行反转

如果设置的是high,说明前500计数为高电平,500-2000为低电平

配置时钟

需要用到什么外设就要看外设挂在那个时钟线上


//配置时钟线
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
配置输出引脚

读数据手册,看引脚定义,什么引脚可以复用为TIM的输出口


GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
选择时钟源

TIM_InternalClockConfig(TIM2);
配置时基单元

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//不分频
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;        //ARR
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;        //PSC
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
配置定时器输出比较通道

    TIM_OCInitTypeDef TIM_OCInitStructure;
//因为不一定每位成员都会用得到,所以先初始化所有成员
    TIM_OCStructInit(&TIM_OCInitStructure);
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;        //CCR
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);

    TIM_Cmd(TIM2, ENABLE);//定时器使能

void PWM_SetCompare(uint16_t Compare)
{
    TIM_SetCompare1(TIM2, Compare);
}

引脚重映射

TIM2_CH1_ETR,可以从PA0重映射到PA15上

但PA15同时也是JTDI的复用脚功能

所以要解除PA15的复用


//如果是想吧PA15当普通引脚使用,那就先启动AFIO(1)的时钟然后(3)解除JTAGD
//如果是想重映射定时器的引脚,那就先启动(1)AFIO时钟,调用AFIO去(2)重映射引脚
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);

舵机控制

1MHz=1000,000Hz。

PWM直流电机

输入捕获

XOR:异或门一般在无刷电机中应用到

输入了滤波器:对输入的信号进行过滤一下,防止误触发

边沿检测器:用来检测电平的变化

TI1FP1&TI1FP2:能通过两个口输出两个相同的信号可以同时测试PWM的频率以及占空比

信号通过输入滤波器再进入到预分频器中,然后从预分频器中输出就可以然CNT的值转运到CCR中捕获/比较寄存器中,同时可以触发中断以及更新事件,在一次捕获后,要自动将CNT清零,可以用主从触发来操作

TI1FP1信号也会穿到从模式,同时让从模式清零CNT

在一次捕获后,要自动将CNT清零,可以用主从触发来操作

可以通过这样的选择TI1FP1的信号通过硬件自动来让CNT清零

只用一个通道,只能测频率

能同时测占空比和频率

CCR1是计数一个周期的值

CCR2是计数一个高电平的值

所以CCR2/CCR1就是为占空比

脉冲周期测量发

步骤

1:启动外设时钟

2:设置输入IO口

3:捕获单元IC初始化

4:设置定时器时基(用做采样用)

5:触发源选择

6:从模式设置

IC配置

void IC_Init()
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //根据stm32参考手册设置为浮空输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    TIM_InternalClockConfig(TIM3);
    /*设置定时器时基,当时也是用这个定时器作为采样频率*/
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up  ;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //防止溢出给最大值
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1; //采样频率为1Mhz
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
    /*IC初始化设置*/
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICStructInit(&TIM_ICInitStructure);//初始化所有成员
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择一线路
    TIM_ICInitStructure.TIM_ICFilter = 0xF;//滤波器
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//上升沿计数
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//不分频
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//直接输出不走分叉
    TIM_ICInit(TIM3,&TIM_ICInitStructure);
    
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);//触发从模式的触发源选择
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);//触发从模式后,选择清除CNT模式
    
    TIM_Cmd(TIM3,ENABLE);
}

uint32_t IC_GetFreq(void)
{
    return 1000000 / (TIM_GetCapture1(TIM3) + 1); //采样频率1MHz / CNT
}
被测量PWM的配置

void PWM_Init(void)
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
//    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//    GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);
//    GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, 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_InternalClockConfig(TIM2);
    
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;        //ARR
    TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1;        //PSC
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
    
    TIM_OCInitTypeDef TIM_OCInitStructure;
    TIM_OCStructInit(&TIM_OCInitStructure);
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = 0;        //CCR
    TIM_OC1Init(TIM2, &TIM_OCInitStructure);
    
    TIM_Cmd(TIM2, ENABLE);
}

void PWM_SetCompare1(uint16_t Compare)//设置占空比
{
    TIM_SetCompare1(TIM2, Compare);
}

void PWM_SetPrescaler(uint16_t Prescaler)//设置频率
{
    TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate);
}
测试占空比以及频率

void IC_Init()
{
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //根据stm32参考手册设置为浮空输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    TIM_InternalClockConfig(TIM3);
    /*设置定时器时基,当时也是用这个时钟作为采样频率*/
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
    TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up  ;
    TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //防止溢出给最大值
    TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1; //采样频率为1Mhz
    TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
    /*IC初始化设置*/
    TIM_ICInitTypeDef TIM_ICInitStructure;
    TIM_ICStructInit(&TIM_ICInitStructure);//初始化所有成员
    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//选择一线路
    TIM_ICInitStructure.TIM_ICFilter = 0xF;//滤波器
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//上升沿计数
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;//不分频
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//直接输出不走分叉
    TIM_ICInit(TIM3,&TIM_ICInitStructure);
    
    TIM_PWMIConfig(TIM3,&TIM_ICInitStructure);//把TIM_ICInitStructure成员相反选择
    
    TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);//触发从模式的触发源选择
    TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);//触发从模式后,选择清除CNT模式
    
    TIM_Cmd(TIM3,ENABLE);
}

uint32_t IC_GetFreq(void)
{
    return 1000000 / (TIM_GetCapture1(TIM3) + 1); //采样频率1MHz / CNT
}

uint32_t IC_GetDuty(void)
{
    return (TIM_GetCapture2(TIM3)+1)*100/(TIM_GetCapture1(TIM3)+1);  //占空比CCR2 / CCR1
}

TIM编码器接口

输入脚状态一般怎么选择,看外部输入空闲状态是什么电平,高电平上拉,低电平就下拉

如果不确定外部空闲电平状态就尽量选择浮空输入

ADC模数转换器

通过这个来选择输入口的开关

通过输入口输入的未知电压,我们通过SAR控制一个DAC已知编码的电压,在比较器中进行比较,DAC大了就调小,小了则反之,逐渐逼近未知电压的值,一般采样二分法

EOC :ADC转换结束标志位

START: ADC转换开始

这是STM32中的内部的ADC,外部输入端口为ADCx_IN0-15,内部输出有温度传感器以及Vrefint,进入到模拟至数字转换器,相当上面的DAC以及比较器,然后把转换结果传到数据寄存器中

其中有两种通道:

  1. 注入通道:一次最多有转换四个通道,转换后也有四个通道数据寄存器,存储数据

  1. 规则通道:一次最多可以转换16个通道,但转换后只有一个数据寄存器存储数据,所以每转换一个数据就要用DMA把数据转移走,以免数据被覆盖

触发转换

相当于START,触发ADC转换,外部触发,可以通过外部引脚触发入转换如EXTI_15和EXTI_11

也可以通过定时器定时触发

模拟看门狗

通过用模拟看门狗监视某个通道,超出阈值就可以通过AWD去产生中断

转换模式

每次只能选中一个通道,例如选中通道2触发ADC转换后,转换成功后EOC至1,然后读结果,停止转换,等你下一次的触发转换,才又开始触发转换

想换通过的话就把通道改成其他通道就就可以可以进行触发转换了

自动转换模式,等触发转换后就会一直转换,可以直接读取寄存器获得转换结果

也是每次触发只转换一次,经过一次转换后停止,等下一次触发才开次转换

而这个可以通过扫描模式对多个通道进行转换,不过每读取一个通道就要通过DAM把数据移走,再进行下一次的数据转换

在AD转换中,保持(Hold)是指在采样结束后,将采样保持在一个电容器或保持器中,以便进一步进行模拟信号的转换。保持器中的电压保持不变,直到采样完成并被转换成数字信号。在保持期间,采样保持器必须能够稳定地保持输入信号的值,以确保转换精度。

在采样过程中,采样保持电路负责将输入信号转换为等效电压,并在采样结束后将等效电压保持在一个保持电容器或保持器中。这个保持器保持输入信号的电压不变,直到转换器完成数字化转换并将其输出。在保持期间,采样保持电路必须能够快速充电和放电保持电容器或保持器,以便在下一次采样开始之前准备好。保持电路的质量对于采样和转换精度非常关键,因为任何保持电路中的不良元件或设计都可能导致采样和转换误差。

ADC单个通道模式配置

配置步骤:

  1. 启动时钟

  1. 初始化引脚

  1. 给ADC的时钟进行分频

  1. ADC初始化

  1. ADC使能

  1. ADC校准


void AD_Init(void)
{
    /*启动时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
    /*初始化引脚*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    /*给ADC的时钟进行分频*/
    RCC_ADCCLKConfig(RCC_PCLK2_Div6); //ADC的最大频率为14MHz,而这个分频为12MHZ
    /*ADC初始化*/
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//连续转变模式关闭
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,右对齐
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//外部触发开始 不需要
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent ;//设置是ADC还是双ADC模式
    ADC_InitStructure.ADC_NbrOfChannel = 1;//选择通道个数
    ADC_InitStructure.ADC_ScanConvMode  = DISABLE;//不需要扫描模式
    ADC_Init(ADC1,&ADC_InitStructure);
    
    ADC_Cmd (ADC1,ENABLE);
    
    ADC_ResetCalibration(ADC1);//复位校准寄存器
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);//在复位校准过程中Status一直被置Set
    ADC_StartCalibration(ADC1);//启动校准寄存器
    while (ADC_GetCalibrationStatus(ADC1) == SET);//在启动校准过程中Status一直被置Set
}

int16_t ADC_GetValue()
{
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发开始转变
    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);//如果转变成功了就为Set,RESET说明还在转变过程中
    return ADC_GetConversionValue(ADC1);//转变成功后放回
}

DMA

DMA基本结构

起始地址:就是数据从哪里来,要到哪里去

数据宽度:就是一次能转运多少数据字节8位,半字16位,字32位

地址自增:就是数据每次转运后,是否要移到下一个地址就像指针*p p++;

例如ADC转换中,被读取的寄存器地址是不变不需要自增,而存储寄存器是需要自增的

传输计数器:来计数DMA一个需要数据转运多少次,是数值自减的。例如,给它写了个5,没转运一次,数字就减少1,就说明数据转运5次后就停止,而等数值减到0之后,之前地址自增的地址也会恢复到起始地址的位置,以方便下一轮的数据转换。

自动重装器:就是计算器到0后,是否要会到上次设定的值,例如上次计数器设定的值是5,到0后,是否要重新变回5.如果不重装就是单次模式,如果重装就是循环模式

M2M(Memory to Memory):决定是由那种方式去触发数据开始转运,是硬件还是软件触发。

软件触发:以最快的速度,连续不断地触发DMA,争取迅速把计数器清零,好开始下一轮的触发,软考触发和自动重装不能同时使用,同时使用的话,会一直进入死循环。

硬件触发:如ADC、串口、定时器等,这些转运需要一定的时机,例如ADC转变完成,串口信息发送完成等等

开关控制:DMA_Cmd使能,计数器必须大于0,必须有触发源,

重点:给计数器写数值的时候DMA_Cmd DISABLE,等写好了计数器再ENABLE

DMA请求

硬件只能根据手册选择特定的通道触发,而软件触发什么通道东西

例如:如果你要使用ADC1DMA,就要用ADC1_DAMCmd();把通道打开

选择通道后,经过数据选择器,再进入仲裁器,进行优先级判断

数据宽度

源小目大,就补0,源大目小就舍弃后面的

数据转运配置

起始地址配置为DataA[7]和DataB[7]数组的首地址

数据宽度都为uint8_t

地址自增,一一对应

有七个数据,所以计数器给7,并且不需要自动重装

M2M为1,软件触发

开关控制,DMA_Cmd ENABLE

数据转运的方式为复制,转运后数组A的数据仍然存在


uint8_t MyDMA_Size;

/**
  * @brief  初始化DMA为软件触发
  * @param  AddrA:起源地址 
  * @param  AddrB:目标地址
  * @retval None
  */
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint8_t size)
{
    MyDMA_Size = size; //转运次数
    
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //时钟使能
    
    DMA_InitTypeDef DMA_InitStruture;
    DMA_InitStruture.DMA_PeripheralBaseAddr = AddrB;//外设地址
    DMA_InitStruture.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据大小
    DMA_InitStruture.DMA_PeripheralInc = DMA_PeripheralInc_Enable ;//外设地址自增
    DMA_InitStruture.DMA_MemoryBaseAddr = AddrA;//寄存器地址
    DMA_InitStruture.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//寄存器数据大小
    DMA_InitStruture.DMA_MemoryInc = DMA_MemoryInc_Enable;//寄存器地址自增
    
    DMA_InitStruture.DMA_DIR = DMA_DIR_PeripheralDST; //外设地址为目标地址
    DMA_InitStruture.DMA_BufferSize = size; //转运次数
    DMA_InitStruture.DMA_M2M = DMA_M2M_Enable ;//软件触发转运
    DMA_InitStruture.DMA_Mode = DMA_Mode_Normal;//不连续循环
    DMA_InitStruture.DMA_Priority = DMA_Priority_Medium;//优先级
    DMA_Init(DMA1_Channel1,&DMA_InitStruture);
    
    DMA_Cmd(DMA1_Channel1,DISABLE);
}
/**
  * @brief  把计数器设置为初始化的值
  * @param  无
  * @param  无
  * @retval None
  */
void MyDMA_Transfer(void)
{
    DMA_Cmd(DMA1_Channel1,DISABLE);//失能在能设置DMA的计数器
    DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);//设置计数器
    DMA_Cmd(DMA1_Channel1,ENABLE);//使能
    
    while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);//等待转变,完全转变完成就为Set
    DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
}

起始地址:DMA外设地址源设备地址设为ADC_DR的地址,目标地址设为一个存放数据的数组地址

数据宽度:uint16_t

地址自增:目标地址设为地址自增

触发:设置为硬件触发,每个通道ADC转换完成就触发DMA申请


uint16_t Data[4] ;
void AD_Init(void)
{
    /*启动时钟*/
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);                        //时钟使能
    
    /*初始化引脚*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    /*给ADC的时钟进行分频*/
    RCC_ADCCLKConfig(RCC_PCLK2_Div6); //ADC的最大频率为14MHz,而这个分频为12MHZ
    
    /*ADC初始化*/
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//连续转变模式开启
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐,右对齐
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//外部触发开始 不需要
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent ;//设置是ADC还是双ADC模式
    ADC_InitStructure.ADC_NbrOfChannel = 4;//选择通道个数
    ADC_InitStructure.ADC_ScanConvMode  = ENABLE;
    ADC_Init(ADC1,&ADC_InitStructure);
    
    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);
    /*DMA初始化*/
    DMA_InitTypeDef DMA_InitStruture;
    DMA_InitStruture.DMA_PeripheralBaseAddr = (uint32_t)Data;                                            //外设地址
    DMA_InitStruture.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设数据大小
    DMA_InitStruture.DMA_PeripheralInc = DMA_PeripheralInc_Enable ;                //外设地址自增
    DMA_InitStruture.DMA_MemoryBaseAddr = (uint32_t)&ADC1->DR;                                                    //寄存器地址
    DMA_InitStruture.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;                //寄存器数据大小
    DMA_InitStruture.DMA_MemoryInc = DMA_MemoryInc_Disable;                                //寄存器地址自增
    
    DMA_InitStruture.DMA_DIR = DMA_DIR_PeripheralDST;                                            //外设地址为目标地址
    DMA_InitStruture.DMA_BufferSize = 4;                                                                //转运次数
    DMA_InitStruture.DMA_M2M = DMA_M2M_Disable ;                                                        //软件触发转运
    DMA_InitStruture.DMA_Mode = DMA_Mode_Circular;                                                    //连续循环
    DMA_InitStruture.DMA_Priority = DMA_Priority_Medium;                                    //优先级
    DMA_Init(DMA1_Channel1,&DMA_InitStruture);

    DMA_Cmd(DMA1_Channel1,ENABLE);
    ADC_DMACmd(ADC1,ENABLE);
    ADC_Cmd (ADC1,ENABLE);
    
    ADC_ResetCalibration(ADC1);//复位校准寄存器
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);//在复位校准过程中Status一直被置Set
    ADC_StartCalibration(ADC1);//启动校准寄存器
    while (ADC_GetCalibrationStatus(ADC1) == SET);//在启动校准过程中Status一直被置Set
    
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);//软件触发开始转变
}

USART串口协议

USART 串口发送数据

USART初始化


#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

void Serial_Init(void)
{
    /*启动时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    /*初始化引脚*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    /*初始化化USART*/
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;//波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不选择硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Tx;//选择发送模式
    USART_InitStructure.USART_Parity = USART_Parity_No;//不选择奇偶校验位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;//一位停止位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;//接口传输或接收的每个数据帧中的位数
    USART_Init(USART1, &USART_InitStructure);
    
    USART_Cmd(USART1, ENABLE);
}

USART打印输出函数定义


/**
  * @brief  发送一个字节
  * @param  Byte:需要发送的一个字节
  * @retval None
  */
void Serial_SendByte(uint8_t Byte)
{
    USART_SendData(USART1, Byte);
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
/**
  * @brief  发送一个uint8_t类型的数组
  * @param  Array:需要发送的数组的首地址
  * @param  Length:数组长度
  * @retval None
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
    uint16_t i;
    for (i = 0; i < Length; i ++)
    {
        Serial_SendByte(Array[i]);
    }
}
/**
  * @brief  发送一个字符串
  * @param  String:需要发送的字符串
  * @retval None
  */
void Serial_SendString(char *String)
{
    uint8_t i;
    for (i = 0; String[i] != '\0'; i ++)
    {
        Serial_SendByte(String[i]);
    }
}
/**
  * @brief x的y次方 
  * @param  X
  * @param  Y
  * @retval None
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
    uint32_t Result = 1;
    while (Y --)
    {
        Result *= X;
    }
    return Result;
}
/**
  * @brief  发送一串数字
  * @param  Number:需要发送的数字
  * @param  Length:数字的长度
  * @retval None
  */
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
    uint8_t i;
    for (i = 0; i < Length; i ++)
    {                            
        Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
    }
}
/**
  * @brief  fputc函数的作用通常是将数据通过串口发送出去
  * @param  
  * @param  
  * @retval None
  */
int fputc(int ch, FILE *f)
{
    Serial_SendByte(ch);
    return ch;
}
/**
  * @brief  发送一个字符串
  * @param  char *format:需要发送的数据类型 如"%d,%c"等
  * @param  可变参数,需要打印的数据
  * @retval None
  */
void Serial_Printf(char *format, ...)
{
    char String[100];
    va_list arg;
    va_start(arg, format);
    vsprintf(String, format, arg);
    va_end(arg);
    Serial_SendString(String);
}

在STM32中,fputc函数的作用通常是将数据通过串口发送出去。这是因为在STM32的开发中,常常使用串口进行通信,而串口通信的数据传输是基于字节(byte)的,因此需要将数据转换为单个字节并逐个发送。

在这种情况下,fputc函数会被重定义为一个向串口发送数据的函数,以便在程序中可以通过调用fputc函数将数据发送到串口。具体而言,当使用标准库函数进行输出时,比如printf函数,输出的字符会被逐个传递给fputc函数,由fputc函数将每个字符发送到串口上,实现串口输出的功能。

需要注意的是,为了让fputc函数正常工作,需要在程序中初始化串口,并将其与stdout绑定。这样才能将标准库函数的输出重定向到串口上。

使用printf函数还要勾选上这个

打印中文时有时会报错所以填上这个

--no-multibyte-chars

USART接收一个Byte


#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

uint16_t Serial_ReceiveData;
uint8_t Serial_ReceiveFlag;

void Serial_Init(void)
{
    /*启动时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    /*初始化TX引脚*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    /*初始化RX引脚*/
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    
    /*初始化化USART*/
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;//波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不选择硬件流控制
    USART_InitStructure.USART_Mode = USART_Mode_Tx|USART_Mode_Rx;//选择发送模式
    USART_InitStructure.USART_Parity = USART_Parity_No;//不选择奇偶校验位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;//一位停止位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;//接口传输或接收的每个数据帧中的位数
    USART_Init(USART1, &USART_InitStructure);
    /*开启USART中断通道*/
    USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
    /*NVIC配置*/
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel= USART1_IRQn;//Interupt ReQuest,即“中断要求”
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_Init(&NVIC_InitStructure);
    
    USART_Cmd(USART1, ENABLE);
}
/**
  * @brief  接收到数据标志位
  * @param  无
  * @param  无
  * @retval return 0 or 1,1表示接收到数据,0反之
  */
uint8_t Serial_GetRexFlag()
{
    if(Serial_ReceiveFlag==1)
    {
        Serial_ReceiveFlag=0;
        return 1;
    }
    return 0;
}
/**
  * @brief  放回接收到的一个字节数据
  * @param  无
  * @param  无
  * @retval 接收到的一个Byte
  */
uint16_t Serial_GetRexData()
{
    return Serial_ReceiveData;
}

void USART1_IRQHandler()
{
    if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
    {
        Serial_ReceiveData = USART_ReceiveData(USART1);
        Serial_ReceiveFlag = 1;
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);
    }
}

USART串口数据包

数据包就是把多个字节打包起来,方便我们传输多个数据

发送一个Hex数据包


/*—————————————-----Serial发送Hex数据包——————————————————*/
void Serial_SendPacket()
{
    Serial_SendByte(0XFF);
    Serial_SendArray(Serial_TxPacket,4);
    Serial_SendByte(0XFE);
}

接受一个Hex数据包


/*—————————————-----Serial接受Hex数据包——————————————————*/
void USART1_IRQHandler()
{
    static uint8_t RxState; //Rx接受,状态0--接受到包头0xFF-->状态1--接受到4个字节
                            //-->状态3--接受到包尾0FE-->状态0
    uint8_t RxData;
    static uint8_t pRxData;//表示接收到第几位数据了
    if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
    {
        RxData = USART_ReceiveData(USART1);
        if(RxState==0)
        {
            if(RxData==0xFF)
            {
                RxState = 1;
            }
        }
        else if(RxState==1)
        {
            Serial_RxPacket[pRxData] = RxData;
            pRxData++;
            if(pRxData==4)
            {
                RxState = 2;
            }
        }
        else if(RxState==2)
        {
            if(RxData==0xFE)
            {
                RxState = 0;
                Serial_ReceiveFlag = 1; 
            }
        }
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);
    }
}

/**
  * @brief  接收到数据标志位
  * @param  无
  * @param  无
  * @retval return 0 or 1,1表示接收到数据,0反之
  */
uint8_t Serial_GetRxFlag()
{
    if(Serial_ReceiveFlag==1)
    {
        Serial_ReceiveFlag=0;
        return 1;
    }
    return 0;
}

接受一个文本数据包


/*—————————————-----Serial接受文本数据包——————————————————*/
void USART1_IRQHandler()
{
    static uint8_t RxState; //Rx接受,状态0--接受到包头0xFF-->状态1--接受到4个字节
                                                    //-->状态3--接受到包尾0FE-->状态0
    uint8_t RxData;
    static uint8_t pRxData;//表示接收到第几位数据了
    if(USART_GetITStatus(USART1,USART_IT_RXNE)==SET)
    {
        RxData = USART_ReceiveData(USART1);
        if(RxState==0 && Serial_ReceiveFlag==0)
        {
            if(RxData=='@')
            {
                RxState = 1;
                pRxData = 0;
            }
        }
        else if(RxState==1)
        {
            if(RxData=='\r')
            {
                RxState = 2;
            }
            else
            {
                Serial_RxPacket[pRxData] = RxData;
            pRxData++;
            }
        }
        else if(RxState==2)
        {
            if(RxData=='\n')
            {
                RxState = 0;
                Serial_ReceiveFlag = 1; //表示已经数据接收完毕
                Serial_RxPacket[pRxData] = '\0';
            }
        }
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);
    }
}

通过接受一个文本数据控制单片机


int main(void)
{
    OLED_Init();
    Serial_Init();
    LED_Init();
    OLED_ShowString(1,1,"TxPacket");
    OLED_ShowString(3,1,"RxPacket");
    while (1)
    {
        if(Serial_ReceiveFlag==1)
        {
            if(strcmp(Serial_RxPacket,"LED_ON")==0)//strcmp(a,b)字符串比较函数,如果a==b则为0,
                                                                                            //使用前需要添加<string.h>
            {
                LED1_ON();
                Serial_SendString("LED1_ON_OK");
                OLED_ShowString(2,1,"                ");
                OLED_ShowString(4,1,"                ");
                OLED_ShowString(2,1,"LED1_ON_OK\r\n");
                OLED_ShowString(4,1,Serial_RxPacket);
            }
            else if(strcmp(Serial_RxPacket,"LED_OFF")==0)
            {
                LED1_OFF();
                Serial_SendString("LED1_OFF_OK");
                OLED_ShowString(2,1,"                ");
                OLED_ShowString(4,1,"                ");
                OLED_ShowString(2,1,"LED1_OFF_OK\r\n");
                OLED_ShowString(4,1,Serial_RxPacket);
            }
            else
            {
                OLED_ShowString(2,1,"                ");
                OLED_ShowString(4,1,"                ");
                Serial_SendString("ERROR_COMMMAND");
                OLED_ShowString(4,1,"ERROR_COMMMAND");
            }
            Serial_ReceiveFlag = 0;
        }
    }
}

I2C通讯协议

为了避免主机和从机一边输出高电平,一边输出低电平造成短路,所以采用开漏输出和弱上拉电阻控制

开漏输出,大家都只是能输出低电平,空闲的时候,由上拉电阻输出高电平。只要有一方输出低电平,那么整个线路都被拉低,只有大家都不输出的时候才为高电平

在SCL低电平时,主机通过控制SDA在放数据,例如低电平时主机把SDA拉低了(表示0),那么在SCL上升的时候,从机就可以从SDA读取数据。

因为SCL是由主机控制的,所以在读取数据时,从机不知道主机会在什么时候把SCL拉低,要尽快读取数据,所以一般在SCL上升沿期间就把数据给读取了

指定地址写

步骤:

  1. 起始位:发送一个起始,所有从机都会复位等待呼唤

  1. 从机地址位:发送一个7位指定从机地址以及1位(读/写)操作位0写入,1读出

  1. 应答位:这个时候主机释放SDA,从机拉下表示应答,应答后,从机再释放SDA

  1. 从机寄存器地址位:指定写进取从机那个寄存器

  1. 应答位:这个时候主机释放SDA,从机拉下表示应答,应答后,从机再释放SDA

  1. 数据位:发送数据

  1. 应答位:这个时候主机释放SDA,从机拉下表示应答,应答后,从机再释放SDA

  1. 终止位:如果主机不想在发送数据了那就在stop前先拉低SDA,为后面SDA上升沿做准备

当前地址读

因为没有传进指定地址,所以是读取指针当前指向寄存器地址的值,每写/读一个寄存器,指针都会指向下一个寄存器,例如:我刚在0x1a地址写完数据,就用当前地址读,就会读取0x1b的值

MPU6050

三轴传感器:三轴加速度

六轴传感器:三轴加速度+三轴角速度

九轴传感器:三轴加速度+三轴角速度+三轴磁场强度

10轴传感器:三轴加速度+三轴角速度+三轴磁场强度+一个气压强度

I2C从机地址:110 1000 0x68 而在写入的时候 需要左移移位按位与上一个1

例如:(0x68<<1)| 0x01 这为读出 (0x68<<1)为写入 所以就是0xD0是写地址 0xD1是读地址

自测单元:就是芯片给一个模拟压力的值,然后和静态的值做比较,看差值在不在范围内

电荷泵:就是用电源和一个电容进行串并联切换,输出更高的电压

例如:一个5v的电源和电容并联给电容冲满电后,就改为串联,这时输出的电压就变成了10v,但电容的电会很快消耗完,所以需要不断串并联切换,然后再给输出的电压一个稳定滤波就有了电荷泵升压原理

软件模拟I2C通讯通讯

I2C配置

SCL和SDA封装

/*----------------将SCL和SDA封装成一个函数,方便调用-----------------*/
/**
  * @brief  设置SCL引脚的状态
  * @param  BitValue:1表示高电平、0表示低电平
  * @retval None
  */
void MyI2C_W_SCL(uint8_t Bit)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)Bit);//BitAction枚举类型,表示只有Bit_RESET,Bit_SET
    Delay_us(10);
}
/**
  * @brief  设置SDA引脚的状态
  * @param  BitValue:1表示高电平、0表示低电平
  * @retval None
  */
void MyI2C_W_SDA(uint8_t Bit)
{
    GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)Bit);
    Delay_us(10);
}
/**
  * @brief  读取SDA引脚的状态
  * @param  无
  * @retval BitValue:1表示高电平、0表示低电平
  */
uint8_t MyI2C_R_SDA()
{
    uint8_t SDAVal;
    SDAVal = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);//读取电平的函数
    Delay_us(10);
    return SDAVal;
}

BitAction是一个枚举类型,它定义了GPIO引脚的电平状态,包括Bit_RESETBit_SET。这里的类型转换(BitAction)BitValue将函数参数BitValue转换成了BitAction类型的枚举值,以便在GPIO_WriteBit函数中正确地控制引脚的电平状态。

GPIO引脚初始化化

/*----------------初始化化GPIO引脚-----------------*/
void MyI2C_Init()
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
    
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;//设置成开漏输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB,&GPIO_InitStructure);
    
    GPIO_SetBits(GPIOB,GPIO_Pin_11);//默认上拉输出
    GPIO_SetBits(GPIOB,GPIO_Pin_10);
数据帧定义

/*----------------I2C数据帧的定义-----------------*/
/**
  * @brief  开始帧
  * @param  无
  * @retval None
  */
void MyI2C_Start()
{
    MyI2C_W_SDA(1);
    MyI2C_W_SCL(1);
    MyI2C_W_SDA(0);
    MyI2C_W_SCL(0);
}
/**
  * @brief  停止帧
  * @param  无
  * @retval None
  */
void MyI2C_Stop()
{
    MyI2C_W_SDA(0);
    MyI2C_W_SCL(1);
    MyI2C_W_SDA(1);
}
/**
  * @brief  发送一个字节的数据
  * @param  Byte:需要发送的数据
  * @retval None
  */
void MyI2C_SendByte(uint8_t Byte)
{
    for(int i=0; i<8; i++)
    {
        MyI2C_W_SDA(Byte & (0x80>>i));
        MyI2C_W_SCL(1);
        MyI2C_W_SCL(0);//保证每个数据端最后SCL都为0这样容易衔接上
    }
}
/**
  * @brief  接收一个字节
  * @param   无
  * @retval 返回接收到的字节的数据
  */
uint8_t MyI2C_RecieveByte()
{
    uint8_t Byte = 0x00;
    MyI2C_W_SDA(1);
    for(int i=0; i<8; i++)
    {    
        if(MyI2C_R_SDA()==1) Byte|=(0x80>>i);
        MyI2C_W_SCL(1);
        MyI2C_W_SCL(0);
    }
    return Byte;
}
/**
  * @brief  发送成功数据应答位
  * @param  
  * @param  
  * @retval None
  */
void MyI2C_SendASK(uint8_t ACK)
{
        MyI2C_W_SDA(ACK);
        MyI2C_W_SCL(1);
        MyI2C_W_SCL(0);
}
/**
  * @brief  主机接接收到数据应答位
  * @param  无
  * @retval AckBit:
  */
uint8_t MyI2C_RecieveASK()
{
        uint8_t ACK;
        MyI2C_W_SDA(1);
        MyI2C_W_SCL(1);
        ACK = MyI2C_R_SDA();
        MyI2C_W_SCL(0);
    return ACK;
}

MPU6050配置

指定写配置

/**
  * @brief  先MPU6050写一个字节
  * @param  RegAddress:要写入的地址
  * @param  Data:要写的数据
  * @retval None
  */
void MPU6050_WriteReg(uint8_t RegADDRESS,uint8_t Data)
{
    MyI2C_Start();//开始位
    MyI2C_SendByte(MPU6050_ADDRESS);//从机地址
    MyI2C_RecieveASK();//从机接受应答位
    MyI2C_SendByte(RegADDRESS);//从机寄存器地址
    MyI2C_RecieveASK();//从机接受应答位
    MyI2C_SendByte(Data);//需要写入从机寄存器的数据
    MyI2C_RecieveASK();//从机应答位
    MyI2C_Stop();//停止位
}
指定地址读

/**
  * @brief  从MPU6050中读取一个字节
  * @param  RegAddress:要读取的地址
  * @retval Data:读取到的数据
  */
uint8_t  MPU6050_ReadReg(uint8_t RegADDRESS)
{
    uint8_t Data;
    /*因为读取寄存器是根据当前指针指向的位置,而我们先写让指针指向该寄存器再进行读取*/
    
    /*---指定写的寄存器---*/
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);
    MyI2C_RecieveASK();
    MyI2C_SendByte(RegADDRESS);
    MyI2C_RecieveASK();
        /*---读当前指定的寄存器---*/
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS|0x01);
    MyI2C_RecieveASK();
    Data = MyI2C_RecieveByte();//如果不发送应答位,那么指针会读完这个寄存器后指向下一个寄存器,继续读取
    MyI2C_SendASK(1);
    MyI2C_Stop();
    
    return Data;
}
MPU6050初始化

/**
  * @brief  MPU6050初始化
  * @param  无
  * @retval None
  */
void MPU6050_Init()
{
    MyI2C_Init();
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
//用于使能 MPU6050 的电源管理器,并将 MPU6050 的时钟源设置为内部振荡器。
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
//表示启用所有的陀螺仪和加速度计轴
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
//寄存器用于配置加速度计和陀螺仪的低通滤波器,以及数据输出速率和同步采样
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
//该寄存器用于配置陀螺仪的量程和滤波器带宽
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
//MPU6050_ACCEL_CONFIG寄存器用于配置加速度计的测量范围和灵敏度
}

结构体传递读取数据


/*mpu6050.c*/
#include "MPU6050.h"
void MPU6050_GetData(MPU6050_Data *Data)
{

    uint8_t Data_H,Data_L;
    Data_H = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
    Data_L = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
    Data->AccX = (Data_H<<8) | Data_L;
    
    Data_H = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
    Data_L = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
    Data->AccY = (Data_H<<8) | Data_L;
    
    Data_H = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
    Data_L = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
    Data->AccZ = (Data_H<<8) | Data_L;
    
    Data_H = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
    Data_L = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
    Data->GyroX = (Data_H<<8) | Data_L;
    
    Data_H = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
    Data_L = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
    Data->GyroY = (Data_H<<8) | Data_L;
    
    Data_H = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
    Data_L = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
    Data->GyroZ = (Data_H<<8) | Data_L;
    
}


/*mpu6050.h*/
typedef struct
{
  int16_t AccX;
    int16_t AccY;
    int16_t AccZ;
    int16_t GyroX;
    int16_t GyroY;
    int16_t GyroZ;
}MPU6050_Data;

void MPU6050_GetData(MPU6050_Data *Data);//结构体定义必须放在该函数定义前面

int main()
{
    MPU6050_Data Data;
    MPU6050_Data *p_Data;
    OLED_Init();
    MPU6050_Init();
    
//    OLED_ShowHexNum(1,1,MPU6050_ReadReg(0x19),3);

    while(1)
    {
        MPU6050_GetData(&Data);
        p_Data = &Data;
        OLED_ShowSignedNum(2, 1, Data.AccX, 5);
        OLED_ShowSignedNum(3, 1, Data.AccY, 5);
        OLED_ShowSignedNum(4, 1, Data.AccZ, 5);
        OLED_ShowSignedNum(2, 8, Data.GyroX, 5);
        OLED_ShowSignedNum(3, 8, Data.GyroY, 5);
        OLED_ShowSignedNum(4, 8, Data.GyroZ, 5);
        Delay_ms(50);
    }
}

I2C硬件

主机模式

I2C有一主多从的模式,以及多主机模式

I2C一主多从模式:是指在I2C总线上,只有一个主设备可以发起数据传输,而多个从设备则可以接收和发送数据。主设备负责管理总线并控制数据传输。从设备则只有在主设备请求时才能发送数据。这种模式通常用于连接单个控制器和多个从设备的应用场景。

多主机模式:是指在I2C总线上,可以有多个主设备。任何一个主设备都可以控制总线并发起数据传输。多主机模式允许多个控制器同时访问总线,并且可以更好地支持复杂的通信协议。但是,在多主机模式下需要考虑设备之间的协调和冲突问题,以避免数据传输错误。为了避免数据冲突,通常使用仲裁机制来管理多主机模式下的数据传输。(仲裁机制:仲裁机制是一种用于解决多主机模式下设备间访问总线的竞争问题的方法。在多主机模式下,多个主设备可以同时控制总线并发起数据传输,但是如果多个设备同时使用总线,则可能会导致数据冲突或传输错误。为了避免这种情况的发生,I2C总线上引入了仲裁机制。仲裁机制通过在总线上使用一个专门的信号线(SDA线)来实现。当多个主设备同时想要使用总线时,它们会在SDA线上传输自己的地址和操作信息,同时监听总线上的信号,以确定是否有其他设备正在使用总线。如果发现总线上有其他设备正在使用,则它们就会等待并尝试重新获取总线控制权。当一个设备成功获得总线控制权时,它可以发送数据或者接收数据,并释放总线控制权,以便其他设备使用。总之,仲裁机制通过一种协调机制,让多个主设备按照一定的顺序访问总线,避免数据传输的冲突,从而确保数据的可靠传输。)

可变多主机模式:没有主机,所有从机都可以跳出来做主机给其他从机下达命令,而从机操作完后,就退回去当从机,如果有两个从机都想做主机,就要仲裁模式了。

虽然,I2C引脚配置成开漏输出模式,但仍然能够输入,因为输出的时候是通过TTL触发器,复用功能输入到片上外设

我们把数据先写进到数据寄存器中,然后在移位寄存器空的时候,如果移位寄存器为空数据就会立即从数据寄存器转移到移位寄存器中,然后再又移位寄存器进行发送。

EV8_1事件:移位寄存器空,数据寄存器空,说明要我们先把数据写进DR寄存器中,也就是数据寄存器

EV8事件:这时移位寄存器再转移数据,数据寄存器空,就会产生EV8事件,产生EV8事件的时候,就可以写入数据到DR寄存器中了

EV8_2事件


/**
  * @brief  先MPU6050写一个字节
  * @param  RegAddress:要写入的地址
  * @param  Data:要写的数据
  * @retval None
  */
void MPU6050_WriteReg(uint8_t RegADDRESS,uint8_t Data)
{

    I2C_GenerateSTART(I2C2,ENABLE);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);//读取EV5事件
    
    I2C_SendData(I2C2,MPU6050_ADDRESS);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);读取EV6事件
    
    I2C_SendData(I2C2,RegADDRESS);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING );读取EV8事件
    
    I2C_SendData(I2C2,Data);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED );读取EV8_2事件
    
    I2C_GenerateSTOP(I2C2,ENABLE);
}

/**
  * @brief  从MPU6050中读取一个字节
  * @param  RegAddress:要读取的地址
  * @retval Data:读取到的数据
  */
uint8_t  MPU6050_ReadReg(uint8_t RegADDRESS)
{
    uint8_t Data;

    I2C_GenerateSTART(I2C2,ENABLE);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
    
    I2C_SendData(I2C2,MPU6050_ADDRESS);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
    
    I2C_SendData(I2C2,RegADDRESS);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED);
    
    
    I2C_GenerateSTART(I2C2,ENABLE);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT);
    
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
    MPU6050_WaitEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
    
    I2C_AcknowledgeConfig(I2C2, DISABLE);//根据接受数据的序列图,先发送停止位才会产生EV_4事件在读取数据
    I2C_GenerateSTOP(I2C2, ENABLE);
    
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
    Data = I2C_ReceiveData(I2C2);
    
    I2C_AcknowledgeConfig(I2C2, ENABLE);
    
    
    return Data;
}

SPI

因为SPI使用的是推挽输出方式,输出和输入电平的转换速度极快,所以SPI的传输速度很快,

而I2C因为要实现半双工,输出输入经常互相切换,所以只能选择开漏输出,而开漏输出输出低电平快,但输出高电平慢,所以I2C的传输速度远不如SPI。

SPI这样也有一个问题就是,从机输入的时候,因为都是推挽输出所以可能会导致数据冲突,从而有了SS线,SS为高电平的时候,该从机的MISO为高组态不输出任何数据,从而达到了防止数据冲突。

SPI的数据接收本质就是交换字节。

通过波特率发生器发生的数据从而转移移位寄存器里面的值

上升沿时,主机会把左边数据高位数据放到MOSI线上(输出寄存器),下降沿时,从机把MOSI的数据读取到最右边,同理,从机给主机发送数据一样,并且他们是同步进行的。

模式0是在SS第一个上升沿的时候就要移入数据了,但移入之前需要移出,所以在第一个SCK之前就已经移出了数据,所以在SS下降沿的时候就要移出数据了

例如,如果需要主机输出的数据为1000 1000,而从机输出的数据位0000 0000

SS下降沿时,主机就要从移位寄存器中把1移出在MOSI上了,同时从机也会把0移出到从机的MOSI(主机的MISO)中,然后在SCK的第一个上升沿,主机的MOSI上的1就会移入到从机的移位寄存器最右边,通过从机放在MOSI上的0也会移动到主机的移位寄存器最右边,而主机的MOSI上的数据也会移动到从机移位寄存器的最右边。

SCK下降沿时,主机和从机又会把移位寄存器中的数据放到MOSI线上

与I2C不同的是,I2C主要是通过直接操作从机的寄存器进行读写数据,所以I2C是先发送写寄存器的地址以及需要写入的内容,而SPI主要是通过发送指令,从机SPI协议中一般都会有指令集,通过指令告诉从机我是写数据还是要读数据或者进行其他的操作

SPI软件模拟


/*********************************引脚定义**********************************/
void MySPI_W_SS(uint8_t BitValue)
{
 GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
 
void MySPI_W_SCK(uint8_t BitValue)
{
  GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
 
void MySPI_W_MOSI(uint8_t BitValue)
{
 GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void)
{
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
/*********************************引脚定义**********************************/


/*********************************SPI初始化**********************************/
#define MySPI_SCK   GPIO_Pin_5
#define MySPI_MOSI  GPIO_Pin_7
#define MySPI_MISO  GPIO_Pin_6
#define MySPI_SS    GPIO_Pin_4
void MySPI_Int()
{
 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
 
 GPIO_InitTypeDef GPIO_InitStructure;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
 GPIO_InitStructure.GPIO_Pin = MySPI_MOSI|MySPI_SCK|MySPI_SS;
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(GPIOA,&GPIO_InitStructure);
 
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
 GPIO_InitStructure.GPIO_Pin = MySPI_MISO;
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(GPIOA,&GPIO_InitStructure);
 MySPI_W_SS(1); //初始化SS
 MySPI_W_SCK(0);//初始化SCK
}
 

/*******************************************************************************
* 函数名       : MySPI_Start
* 功能            : 拉低CS
* 输入            : 无 
* 输出            : 无
*******************************************************************************/
uint8_t MySPI_Start()
{
    MySPI_W_SS(0);
}
/*******************************************************************************
* 函数名       : MySPI_Stop
* 功能            : 拉低CS
* 输入            : 无 
* 输出            : 无
*******************************************************************************/
uint8_t MySPI_Stop()
{
    MySPI_W_SS(1);
}
/*******************************************************************************
* 函数名       : MySPI_WriteReadByte
* 功能            : 发送并接受一个Byte
* 输入            : 需要发送的Byte 
* 输出            : 返回的一个Receive
*******************************************************************************/
uint8_t MySPI_WriteReadByte(uint8_t Byte)
{
    uint8_t Receive=0x00;
    for(int i=0; i<8; i++)
    {
        /*模式0,在上升沿之前把数据放到MOSI*/
        MySPI_W_MOSI(Byte & (0x80>>i));
        /*模式0,在上升沿读取数据*/
        MySPI_W_SCK(1);
        if (MySPI_R_MISO() == 1){Receive |= (0x80 >> i);}
        MySPI_W_SCK(0);
    }
    return Receive;
}

SPI硬件通讯


#define MySPI_SCK   GPIO_Pin_5
#define MySPI_MOSI  GPIO_Pin_7
#define MySPI_MISO  GPIO_Pin_6
#define MySPI_SS        GPIO_Pin_4

/*******************************************************************************
* 函数名       : W25Q64_W_SS
* 功能            : 设置SS的引脚状态
* 输入            : BitValue:引脚状态 
* 输出            : 无
*******************************************************************************/
void W25Q64_W_SS(uint8_t BitValue)
{
 GPIO_WriteBit(GPIOA, MySPI_SS, (BitAction)BitValue);
}
/*******************************************************************************
* 函数名       : W25Q64_Init()
* 功能            : 初始化W25Q64
* 输入            : 无 
* 输出            : 无
*******************************************************************************/
void W25Q64_Init()
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP ;
    GPIO_InitStructure.GPIO_Pin = MySPI_MOSI|MySPI_SCK; 
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ;
    GPIO_InitStructure.GPIO_Pin = MySPI_MISO;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;
    GPIO_InitStructure.GPIO_Pin = MySPI_SS;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
    
    SPI_InitTypeDef SPI_InitStructure;
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
    
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;//模式1,第一个边沿采集数据
    SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low  ;//时钟空闲时的极性
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b ;//每次传输数据帧为8位
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;//全双工输出输入
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;//传输首位为最高有效位
    SPI_InitStructure.SPI_Mode  = SPI_Mode_Master;//主模式
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;//通过软件模拟来进行片选
    SPI_InitStructure.SPI_CRCPolynomial = 0;
    SPI_Init(SPI1,&SPI_InitStructure);
    
    SPI_Cmd(SPI1,ENABLE);
    
    GPIO_SetBits(GPIOA,MySPI_SS);
}

uint8_t W25Q64_Start()
{
    W25Q64_W_SS(0);
}

uint8_t W25Q64_Stop()
{
    W25Q64_W_SS(1);
}

uint8_t W25Q64_WriteReadByte(uint8_t Byte)
{
    
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1,Byte);
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
    return SPI_I2S_ReceiveData(SPI1);
}
 void W25Q64_ReadID(uint8_t *MID,uint16_t *DID)
{
    W25Q64_W_SS(0);
    W25Q64_WriteReadByte(0x9F);
    *MID = W25Q64_WriteReadByte(0xff);
    *DID = W25Q64_WriteReadByte(0xff);
    *DID<<=8;
    *DID |= W25Q64_WriteReadByte(0xff);
    W25Q64_W_SS(1);
}

void W25Q64_WriteEnable()
{
    W25Q64_W_SS(0);
    W25Q64_WriteReadByte(W25Q64_WRITE_ENABLE);
    W25Q64_W_SS(1);
}

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t DataCount)
{
    uint8_t temp;
    W25Q64_W_SS(0);
    W25Q64_WriteReadByte(W25Q64_READ_DATA);
    W25Q64_WriteReadByte(Address>>16);
    W25Q64_WriteReadByte(Address>>8);
    W25Q64_WriteReadByte(Address);
    
    for(int i=0; i<DataCount; i++)
    {
        DataArray[i] = W25Q64_WriteReadByte(0xff);
    }
    W25Q64_Stop();
}

void W25Q64_WaitBusy()
{
    uint32_t Timeout=100000;
    W25Q64_Start();
    W25Q64_WriteReadByte(W25Q64_READ_STATUS_REGISTER_1);
    while((W25Q64_WriteReadByte(0xff)&0X01)==0x01)
    {
        Timeout--;
        if(Timeout==0)
        {
            break;
        }
    }
    W25Q64_Stop();
}

void W25Q64_Page_Program(uint32_t Address, uint8_t *DataArray, uint32_t DataCount)
{
    W25Q64_WriteEnable();
    W25Q64_Start();
    W25Q64_WriteReadByte(W25Q64_PAGE_PROGRAM);
    W25Q64_WriteReadByte(Address>>16);
    W25Q64_WriteReadByte(Address>>8);
    W25Q64_WriteReadByte(Address);
    for(int i=0; i<DataCount; i++)
    {
         W25Q64_WriteReadByte(DataArray[i]);
    }
    W25Q64_Stop();
    W25Q64_WaitBusy();
}

void W25Q64_SectorErase(uint32_t Address)
{
    W25Q64_WriteEnable();
    W25Q64_Start();
    W25Q64_WriteReadByte(W25Q64_SECTOR_ERASE_4KB);
    W25Q64_WriteReadByte(Address>>16);
    W25Q64_WriteReadByte(Address>>8);
    W25Q64_WriteReadByte(Address);
    W25Q64_Stop();
    W25Q64_WaitBusy();
}

void W25Q64_ChipErase()
{
    W25Q64_WriteEnable();
    W25Q64_Start();
    W25Q64_WriteReadByte(W25Q64_CHIP_ERASE);
    W25Q64_Stop();
    W25Q64_WaitBusy();
}

W25Q64

w25q64地址分配

存储器的地址划分有块(block)、扇(Sector)、页(page)

在w25q64中一个块有64kb,一个块有16扇,一扇(4kb)有16页,一页256字节

芯片总内存 8M

块64kb

扇4kb

页256字节

传进的24位数据地址,前2个字节是传进页地址寄存器用来锁定在哪页进行读写操作的

而最后一位字节,是写进字节地址锁存器,用来锁定在哪个字节地址上读取数据

对存储器进行写操作

写数据进芯片的时候,是先缓存在数据缓存器ROM中,因为SPI的传输速度很快,但写进flash 的速度慢,所以先把数据存到数据缓存器中,等一个数据传输结束后,再从数据缓存器中写进对应的flash中,由于缓存器的内存最大为256字节,所以一个时序中最多只能写入256个字节,而缓存器在往flash传送数据的时候,会把状态寄存器置1,这个时候外界不能传数据进来

最小擦除单位是扇

  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值