stm32学习笔记

 

目录

1.GPIO输出

1.1 初始化

1.2 方法

1.3 接线

2.GPIO输入

2.1 初始化

2.2 按键消抖和识别

3.中断

3.1 EXTI外部中断

3.1.1 初始化配置

4.TIM定时器

4.1 初始化

5.PWM

5.1 PWM初始化

5.1.1 呼吸灯

5.1.2 舵机

5.1.3 直流电机

6.AD转换--adc

6.1 初始化 

7.DMA传数据

7.1 初始化

8.通信

 8.1 USART串口通信 

8.1.1 串口发送&接收字节

8.1.2 串口发送数据包(led控制)

 8.2 I2C

8.2.1 软件实现I2C

8.2.1.1 I2C.c

8.2.1.2 MPU6050.c

8.2.2 硬件实现I2C 

8.3 SPI

8.3.1 软件实现SPI读写W25Q64

8.3.1.1 MySPI.c

 8.3.1.2 W25Q64.c

8.3.2 硬件实现SPI

8.3.2.1 初始化

8.4 CAN

8.5 USB

9.时钟部分

 9.1 初始化

10.电源控制

11.看门狗

12.引脚图参考(来自江协):


基于b站江科大的stm32学习笔记。记录一下各个模块的初始化防止忘记。硬件电路这些老师讲的非常通透,去b站看即可

1.GPIO输出

GPIO输出有点灯,蜂鸣器等,以点灯为例

1.1 初始化

void LED_Init(void){
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //1
	GPIO_InitTypeDef GPIO_InitStructure;                 //2
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;       //3
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0 | GPIO_Pin_3; //4
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;      //5
	GPIO_Init(GPIOA,&GPIO_InitStructure);                //6
	
	GPIO_SetBits(GPIOA,GPIO_Pin_0 | GPIO_Pin_3);         //7
}

1.时钟配置,选择GPIOA或B,然后使能

2.定义结构体,用于配置各个参数

3.设置输出模式

        stm32学习笔记_第1张图片

4.选择pin口,A几就用pin几,如果有多个口就用 | 或起来 

5.配置频率,不重要

6.标准库里面的GPIO初始化方法,参数是GPIOx和2定义的结构体

7.先将电平置为无效电平

1.2 方法

写个LED1打开的方法:

void LED1_ON(void){
	GPIO_ResetBits(GPIOA,GPIO_Pin_3);
}

LED1熄灭的方法:

void LED1_OFF(void){
	GPIO_SetBits(GPIOA,GPIO_Pin_3);
}

最后在main引入.h调用方法      

1.3 接线

套件里的LED灯长脚为正极,短脚为负极,正极接电源,负极接stm32口

2.GPIO输入

GPIO输入有按键和光敏传感器等,以按键为例

2.1 初始化

void Key_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}

初始化和点灯的一样,模式要改一下上拉输入

2.2 按键消抖和识别

uint8_t Key_GetNum(void)
{
	uint8_t KeyNum=0;
	if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0){
		Delay_ms(20);
		while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0);
		Delay_ms(20);
		KeyNum=1;
	}
	if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0){
		Delay_ms(20);
		while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);
		Delay_ms(20);
		KeyNum=2;
	}
	return KeyNum;
}

定义一个KeyNum来记住按键,如果PIN11口的按键被按下,delay20ms消抖,用while循环防止按键一直按着,按键松开过后再delay20ms,然后将KeyNum置为按键号。

最后main里面定义Key=Key-GetNum();若key=1,{},key=2,{}。

3.中断

中断是指程序在执行过程中,cpu暂停手中的工作(保护现场),转去做其他更重要的工作,然后再回到现场继续工作的过程。(可嵌套,有中断优先级)

stm32中用NVIC来为中断进行优先级选择:

        抢占优先级:cpu直接暂停手中工作,转去做(中断嵌套)

        响应优先级:cpu干完手中的活后,再做(优先排队)

3.1 EXTI外部中断

        指定的GPIO口电平发生变化时,发送中断请求,该方式支持所有pin口,但不能同时使用相同的pin(PA0和PB0)

3.1.1 初始化配置

AFIO外部引脚选择配置:

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);

配置GPIOB的PIN_14口 

EXTI初始化:

    EXTI_InitTypeDef EXTI_InitStructure;
	EXTI_InitStructure.EXTI_Line = EXTI_Line14;
	EXTI_InitStructure.EXTI_LineCmd = ENABLE;
	EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
	EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
	EXTI_Init(&EXTI_InitStructure);

 同样是定义结构体然后配置参数

配置NVIC:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

选择组(0-4) 我们选择2

初始化:


	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;//1
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//3
	NVIC_Init(&NVIC_InitStructure);

 1.选择通道10-15

2和3.配置抢占优先级和响应优先级

 中断函数:

void EXTI15_10_IRQHandler(void)
{
	if (EXTI_GetITStatus(EXTI_Line14) == SET)
	{
		if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
		{
			CountSensor_Count ++;
		}
		EXTI_ClearITPendingBit(EXTI_Line14);
	}
}

首先判断是否是我们想要的中断源,是则返回set进入if,之后清空标志位,不然会一直申请中断 

4.TIM定时器

定时触发中断,每隔一定时间请求中断

定时器分类:

        高级定时器(APB2):TIM1,TIM8

        通用定时器(APB1):TIM2,TIM3,TIM4,TIM5

        基础定时器(APB1):TIM6,TIM7

4.1 初始化

void Timer_Init(void)
{
	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;
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
	
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);
	
	TIM_Cmd(TIM2, ENABLE);
}

定时器的中断函数通常写在main里面:

void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

外部定时中断:

配置GPIO+TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F); 

5.PWM

单片机只能输出高/低电平,即0或1,要想有呼吸灯的效果,可以使用PWM。我的理解是PWM通过改变高低电平的比例(占空比)实现输出不同的模拟电压,有点像数转模。

5.1 PWM初始化

配置GPIO口和TIM定时器:


	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, 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_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);

ARR和PSC是要计算占空比的

输出比较:

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);

定义一个结构体,将各个参数设置为默认值,配置输出比较模式为PWM1,极性选择高,输出使能,OCx根据引脚图对应决定。

5.1.1 呼吸灯

main函数中: 通过TIM_SetCompare1(TIM2, Compare);将i从0-100赋给CCR,灯就会从暗变亮,形成呼吸灯效果


5.1.2 舵机

在Servo.c中引入PWM头文件

void Servo_Init(void)
{
	PWM_Init();
}

void Servo_SetAngle(float Angle)
{
	PWM_SetCompare2(Angle / 180 * 2000 + 500);
}

初始化PWM

通过计算角度和Compare的映射关系,根据角度计算CCR的值

main函数中按键每按一次Angle加30,调用SetAngle改变CCR(即舵机角度) 


5.1.3 直流电机

电机需要接一个电机驱动匹配电压

初始化PWM,配置PIN4和5作为AB端输入,AB两端一正一负使电机转动,哪个正哪个负用来控制旋转方向

void Motor_SetSpeed(int8_t Speed)
{
	if (Speed >= 0)
	{
		GPIO_SetBits(GPIOA, GPIO_Pin_4);
		GPIO_ResetBits(GPIOA, GPIO_Pin_5);
		PWM_SetCompare3(Speed);
	}
	else
	{
		GPIO_ResetBits(GPIOA, GPIO_Pin_4);
		GPIO_SetBits(GPIOA, GPIO_Pin_5);
		PWM_SetCompare3(-Speed);
	}
}

speed>0,4set5reset,正向转,反之反向转 

在main中,按下一次按键,speed加,改变CCR来控制速度 

6.AD转换--adc

AD转换:用于将引脚上的模拟电压转化为数字信号,比如温度传感器将温度转换为数字变量,之后根据这个数字变量进行显示,阈值判断,值超过设定的阈值就会触发中断等。 

STM32中的ADC资源:ADC1,ADC2,10个外部输入通道,引脚只能接A0-B1(引脚定义图)

6.1 初始化 

配置GPIO时钟和AD时钟和CLK配置:

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);

GPIO为模拟输入模式:

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;

 配置多路开关:

ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

ADC1,选择通道0,非扫描选择1(序列一) ,采样时间(参数越小,速度越快,参数越大,越稳定)

 配置ADC转换器:

    ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//2
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//3
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//4
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;//5
	ADC_InitStructure.ADC_NbrOfChannel = 1;//6
	ADC_Init(ADC1, &ADC_InitStructure);

1.ADC独立模式

2.选择右对齐

3. None软件触发,即不使用外部触发

4.单次转换

5.非扫描模式

6.通道选择1个

校准ADC:

    ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);

获取转换:

uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
	return ADC_GetConversionValue(ADC1);
}

 模拟看门狗:

配置阈值和检测通道,TIConfig+NVIC控制优先级触发中断

7.DMA传数据

DMA:直接存储器存取,可以在外设与存储器,存储器与存储器之间进行数据传输,这样cpu就不用负责这一块内容,节省了CPU资源

7.1 初始化

开启时钟:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

 参数配置:

    DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//1
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//2
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//3
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//4
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//5
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//6
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//7
	DMA_InitStructure.DMA_BufferSize = Size;//8
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//9
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//10
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//11
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	DMA_Cmd(DMA1_Channel1, DISABLE);

1-3都是外设的配置,4-6都是存储器站点的配置

1.要转运的数组地址,通常用数组名来获取而不用绝对地址

2.以字节传输

3. 外设地址选择自增

4.要转运的存储器地址,通常用数组名来获取而不用绝对地址

5.以字节传输

6.存储器地址选择自增

7.外设站点作为数据源:SRC,外设站点作为目的地:DST

8.指定传输几次

9.传输模式:存储器到存储器只能使用正常模式

10.Enable软件触发

11.优先级配置

最后使能

要想DMA一直工作,需要一个函数不断改变DMA的使能,这样每调一次该函数就DMA转运一次:

void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);
}

8.通信

 stm32学习笔记_第2张图片


 8.1 USART串口通信 

Tx与Rx 

8.1.1 串口发送&接收字节

        GPIO初始化:

    GPIO_InitTypeDef GPIO_InitStructure;
    //串口发送gpio初始化
	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);
    //串口接收gpio初始化
    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_InitStruture;
    USART_InitStruture.USART_BaudRate=9600; //波特率
    USART_InitStruture.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//不使用流控
    USART_InitStruture.USART_Mode=USART_Mode_Tx | USART_Mode_Rx;//输入模式或输出模式
    USART_InitStruture.USART_Parity=USART_Parity_No;//无校验模式
    USART_InitStruture.USART_StopBits=USART_StopBits_1;//选择一位停止位
    USART_InitStruture.USART_WordLength=USART_WordLength_8b;//字长

        使用中断来接收数据:‘

    //中断的方法接收数据
    USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
    //配置中断
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitTypeDef NVIC_InitStruture;
    NVIC_InitStruture.NVIC_IRQChannel=USART1_IRQn;
    NVIC_InitStruture.NVIC_IRQChannelCmd=ENABLE;
    NVIC_InitStruture.NVIC_IRQChannelPreemptionPriority=1;
    NVIC_InitStruture.NVIC_IRQChannelSubPriority=1;
    NVIC_Init(&NVIC_InitStruture);

最后使能

发送数据

void Serial_SendByte(uint8_t Byte){
    USART_SendData(USART1,Byte);
    while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}

//对sendbyte的封装
//array
void Serial_SendArray(uint8_t *Array,uint16_t Length){
    uint16_t i;
    for ( i = 0; i < Length; i++)
    {
        Serial_SendByte(Array[i]);
    } 
}
//string
void Serial_SendString(char *String){
    uint8_t i;
    for ( i = 0;String[i]!='\0'; i++)
    {
        Serial_SendByte(String[i]);
    }
    
}
//Number

uint32_t Serial_Pow(uint32_t x,uint32_t y){
    uint32_t result=1;
    while (y--)
    {
        result*=x;
    }
    return result;
}

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');
    }
}

中断接收数据:’

//中断接受变量封装
uint8_t Serial_GetRxFlag(void){
    if (Serial_RxFlag==1)
    {
        Serial_RxFlag=0;
        return 1;
    }
    return 0; 
}

uint8_t Serial_GetRxData(void){
    return Serial_RxData;
}

//中断的启动名字
void USART1_IRQHandler(void){
    if (USART_GetFlagStatus(USART1,USART_IT_RXNE)==SET)
    {
        Serial_RxData=USART_ReceiveData(USART1);
        Serial_RxFlag=1;
        USART_ClearITPendingBit(USART1,USART_IT_RXNE);
    }
}

8.1.2 串口发送数据包(led控制)


 8.2 I2C

同步时序,比较难理解,半双工,有应答,允许cpu在传输过程中中断去做其他事情

SCL:完全由主机控制

SDA:主机在收发时需要放手

stm32学习笔记_第3张图片

stm32学习笔记_第4张图片

8.2.1 软件实现I2C

以读写MPU6050为例,先将I2C时序的六块拼图写出来,再写一个MPU6050.c调用I2C模块完成对MPU6050的读写

接线图中PB10对应SCL,PB11对应SDA

8.2.1.1 I2C.c

初始化

void MyI2C_Init(void){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
    GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);//scl和sda处于高电平即空闲状态
}

六块拼图,其中对pin口的高低电平控制用函数封装,这样代码易修改:

//起始条件
void MyI2C_Start(void){
    MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}
//终止条件
void MyI2C_Stop(void){
    MyI2C_W_SDA(0);
    MyI2C_W_SCL(1);
    MyI2C_W_SDA(1);
}
//发送一个字节
void MyI2C_SendByte(uint8_t Byte){
    for (uint8_t i = 0; i < 8; i++) 
    {
        MyI2C_W_SDA(Byte & (0x80>>i));
        MyI2C_W_SCL(1);
        MyI2C_W_SCL(0);
    } 
}
//读取一个字节
uint8_t MyI2C_ReceiveByte(void){
    uint8_t Byte=0x00;
    MyI2C_W_SDA(1);
    for (uint8_t i = 0; i < 8; i++) 
    {   
        
        MyI2C_W_SCL(1);
        if (MyI2C_R_SDA()==1)
        {
            Byte |= (0x80 >> i);
        }
        
        MyI2C_W_SCL(0);
    } 
    return Byte;
}
//发送应答
void MyI2C_SendAck(uint8_t AckBit){
    
        MyI2C_W_SDA(AckBit);
        MyI2C_W_SCL(1);
        MyI2C_W_SCL(0);
    
}
//接收应答
uint8_t MyI2C_ReceiveAck(void){
    uint8_t AckBit;
    MyI2C_W_SDA(1);
    MyI2C_W_SCL(1);
    AckBit=MyI2C_R_SDA();
    MyI2C_W_SCL(0);
    return AckBit;
}
8.2.1.2 MPU6050.c

每个硬件都不一样,大体就是初始化,然后翻手册查阅地址的宏定义引入.h文件,封装各个方法

写数据:

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}

读数据:

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
	MyI2C_ReceiveAck();
	Data = MyI2C_ReceiveByte();
	MyI2C_SendAck(1);
	MyI2C_Stop();
	
	return Data;
}

 初始化

void MPU6050_Init(void)
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	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);
}

 获取id:

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

 获取字节数据(加速度,水平等):

void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

8.2.2 硬件实现I2C 

硬件的暂时挂着,软件实现比较方便先用着,以后再消化硬件实现


8.3 SPI

感觉SPI学起来比I2C简单,可能是线比较多,没有那么多时序要想

全双工。同步,四条线

SCK:时钟线

MOSI:主机输出从机输出

MISO:主机输入从机输出

SS:设备选择

stm32学习笔记_第5张图片

其中从机没被选择时,MISO为高阻状态

主从机交换数据:

stm32学习笔记_第6张图片

在时钟上升沿:主机第一位放到MOSI,从机第一位放到MISO,移位寄存器里的数据都向右移一位

时钟下降沿:MOSI的数据放到 从机末尾,MISO的数据放到主机末尾

依次循环八次即可交换一个字节,如果只是单纯的收或发,则随便使用一个字节来交换即可,一般发送00或FF

stm32学习笔记_第7张图片

8.3.1 软件实现SPI读写W25Q64

8.3.1.1 MySPI.c
void MySPI_Init(void){
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
//SS置高电平
    MySPI_W_SS(1);
    MySPI_W_SCK(0);
}

其中 MySPI_W_SS(1)和MySPI_W_SCK(0)为引脚封装

开始,传数据,停止三块拼图:

//起始信号
void MySPI_Start(void){
    MySPI_W_SS(0);
}
//停止信号
void MySPI_Stop(void){
    MySPI_W_SS(1);
}
//交换一个字节,此处使用模式0
uint8_t MySPI_SwapByte(uint8_t ByteSend){
    uint8_t i, ByteReceive = 0x00;
	for (i = 0; i < 8; i ++)
	{
		MySPI_W_MOSI(ByteSend & (0x80 >> i));
		MySPI_W_SCK(1);
		if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
		MySPI_W_SCK(0);
	}
	return ByteReceive;
}
 8.3.1.2 W25Q64.c

与MPU6050一样,有了MySPI.c的底层初始化就可以了

void W25Q64_Init(void)
{
	MySPI_Init();
}
//读取ID
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_JEDEC_ID);//读id号指令
	*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//抛出一个无用数据
	*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//交换字节
	*DID <<= 8;
	*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	MySPI_Stop();
}
//使能
void W25Q64_WriteEnable(void)
{
	MySPI_Start();
	MySPI_SwapByte(W25Q64_WRITE_ENABLE);
	MySPI_Stop();
}
//等待忙碌
void W25Q64_WaitBusy(void)
{
	uint32_t Timeout;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
	Timeout = 100000;
	while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
	MySPI_Stop();
}
//页编程
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
	uint16_t i;
	
	W25Q64_WriteEnable();
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	for (i = 0; i < Count; i ++)
	{
		MySPI_SwapByte(DataArray[i]);
	}
	MySPI_Stop();
	
	W25Q64_WaitBusy();
}
//擦除
void W25Q64_SectorErase(uint32_t Address)
{
	W25Q64_WriteEnable();
	
	MySPI_Start();
	MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	MySPI_Stop();
	
	W25Q64_WaitBusy();
}

//读取数据
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{
	uint32_t i;
	MySPI_Start();
	MySPI_SwapByte(W25Q64_READ_DATA);
	MySPI_SwapByte(Address >> 16);
	MySPI_SwapByte(Address >> 8);
	MySPI_SwapByte(Address);
	for (i = 0; i < Count; i ++)
	{
		DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
	}
	MySPI_Stop();
}

8.3.2 硬件实现SPI

8.3.2.1 初始化

开启时钟和gpio初始化:

//开启时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
	//SS从机选择引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_4;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//SCK:PA5和MOSI:PA7
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_5 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	//MISO:PA6
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);

SPI初始化:

	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode=SPI_Mode_Master;//当前设备为主机
	SPI_InitStructure.SPI_Direction=SPI_Direction_2Lines_FullDuplex;//双线全双工
	SPI_InitStructure.SPI_DataSize=SPI_DataSize_8b;//8位数据帧
	SPI_InitStructure.SPI_FirstBit=SPI_FirstBit_MSB;//高位先行
	SPI_InitStructure.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;//频率72MHz/128
	SPI_InitStructure.SPI_CPOL=SPI_CPOL_Low;//空闲默认低电平(模式0)
	SPI_InitStructure.SPI_CPHA=SPI_CPHA_1Edge;
	SPI_InitStructure.SPI_NSS=SPI_NSS_Soft;
	SPI_InitStructure.SPI_CRCPolynomial=7;//默认7
	SPI_Init(SPI1,&SPI_InitStructure);	

	SPI_Cmd(SPI1,ENABLE);
	MySPI_W_SS(1);

交换字节:

uint8_t MySPI_SwapByte(uint8_t ByteSend){
	//等待TXE为1
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
	//写发送的数据到TDR
	SPI_I2S_SendData(SPI1, ByteSend);
	//等待RXNE为1
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
	//读取RDR接收的数据
	return SPI_I2S_ReceiveData(SPI1);
    
}

 这里的ss我们依然使用软件实现,因为硬件里的NSS比较复杂


8.4 CAN


8.5 USB


9.时钟部分

时间戳概念:

由1970年1月1日开始计时秒数,永不进位,这样就是一个16亿多的数字,可以方便的存储在计算机内,不用考虑年月日的转换,也可以很方便的计算,在需要将时间戳转换为人类看的数据格式时,只需要调用c语言中time.h模块的函数。

各种时间表示方式转换:

stm32学习笔记_第8张图片

 9.1 初始化

        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
        PWR_BackupAccessCmd(ENABLE);

        //开启lse时钟
        RCC_LSEConfig(RCC_LSE_ON);//启动外部lse晶振
        while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);
        //选择RTCCLK时钟源
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
        RCC_RTCCLKCmd(ENABLE);
        //等待,防止因为时钟不同步造成bug
        RTC_WaitForSynchro();
        RTC_WaitForLastTask();
        //配置预分频器,lse的频率为32768,因为从0开始,所以要-1
        RTC_SetPrescaler(32768-1);
        RTC_WaitForLastTask();
        //设置初始时间(2023-09-13 15:22)
        RTC_SetCounter(1694591537);
        RTC_WaitForLastTask();

 定义一个数组用来读写time.h转换后的时间:

uint16_t MyRTC_Time[]={2023,9,13,15,29,9};

在.h文件中将其声明为外部可调用:

extern uint16_t MyRTC_Time[];

写入时间:

void MtRTC_SetTime(void){
    time_t time_cnt;
    struct tm tm_date;
    //获取数组的年月日,由于tm结构体已经默认有偏移了,所以这里要减去偏移量
    tm_date.tm_year=MyRTC_Time[0]-1900;
    tm_date.tm_mon=MyRTC_Time[1]-1;
    tm_date.tm_mday=MyRTC_Time[2];
    tm_date.tm_hour=MyRTC_Time[3];
    tm_date.tm_min=MyRTC_Time[4];
    tm_date.tm_sec=MyRTC_Time[5];

    time_cnt=mktime(&tm_date)-8*60*60;
    //写入
    RTC_SetCounter(time_cnt);
    RTC_WaitForLastTask();
}

 读取时间:

void MyRTC_ReadTime(void){
    time_t time_cnt;
    struct tm time_date;
    time_cnt=RTC_GetCounter()+8*60*60;
    time_date= *localtime(&time_cnt);
    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;
}

最后在main.h中使用oled显示出来。

另外,为了防止每次复位都从设定的时间重新计时,使用BKP来判断设备是否完全掉电

BKP:stm32有备用电源,引脚为VBT,只有主电源和备用电源同时断开,BKP里的数才会清除

在初始化函数中为BKP设定一个值,如0x8888,若BKP不等于0x8888,则说明设备完全掉电,则重新从设定的时间进行计时,反之继续计时

if (BKP_ReadBackupRegister(BKP_DR1)!=0x8888)
    {
        //初始化函数。。。
        BKP_WriteBackupRegister(BKP_DR1,0x8888);
    }else
    {
        RTC_WaitForSynchro();
        RTC_WaitForLastTask();
    }

10.电源控制

一般如串口通信时,设备不一定是需要时时刻刻通信的,但主循环却一直在运行,这就会耗电,使用电源控制可以省电。

stm32学习笔记_第9张图片

在考虑可以使用哪种省电模式时,可以参考上面这个表。

11.看门狗

 

12.引脚图参考(来自江协):

你可能感兴趣的:(stm32,学习,笔记)