STM32基础回顾——详解I²C(GPIO模拟I2C)_stm32 gpio模拟i2c-程序员宅基地

技术标签: stm32  c语言  i2c  智能家居项目  单片机  


前言

这一篇博客是根据自己的需要,以及为了面试岗位的需求,所以专门来再次学习一遍SPI和IIC两个常用的通信协议。这里使用的开发板是STM32F03_MIN
也希望这篇博客能对你有一定的帮助!


IIC协议

I²C ,通常被读作“I方C”,它是一种多主从架构的串行通信总线。在1980年由飞利浦公司设计,用于让主板,嵌入式系统或手机连接低速周边设别。如今在嵌入式领域是非常常见的通信协议。常用于MPU/MCU与外部设备连接通信,数据传输。

IIC由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。每个连接到总线的设备都有一个独立的地址,主机可以通过这个地址来访问不同的设备。因为IIC协议比较简单,常常用GPIO来模拟IIC时序,这种方法称为模拟IIC。如果使用MCU的IIC控制器,设置好IIC控制器,IIC控制器就自动实现协议时序,这种方式称为硬件IIC。因为IIC设备的速率比较低,通常两种方式都可以,模拟IIC方便移植,硬件IIC工作效率相对较高。

关于IIC协议,通过老师传学生足球的例子相信大家可以清晰的理解
在这里插入图片描述把老师看作MCU/MPU,学生看作外设设备。
首先老师将球踢给某学生,即主机发送数据给从机,步骤如下:

  1. 老师:开始了(start);
  2. 老师: A!我要发球给你! (地址/方向);
  3. 学生A:到! (回应);
  4. 老师把球发出去(传输);
  5. A收到球之后,应该告诉老师一声(回应);
  6. 老师:结束(停止);
    MCU发送数据,需要得到设备的回应,当数据发送完成,MCU自动停止发送,设备也无需回应。

接着老师让学生把球传给自己,即从机发送数据给主机,步骤如下:

  1. 老师:开始了(start);
  2. 老师: B!把球发给我! (地址/方向);
  3. 学生B:到!
  4. B把球发给老师(传输);
  5. 老师收到球之后,给B说一声,表示收到球了(回应);
  6. 老师:结束(停止)。

MCU接收外设设备传输的数据时,首先时MCU发送指令让设备发送数据的信号,设备收到后,响应MCU,同时MCU接收到数据时也要回应外设设备。当数据传输完成,MCU自动结束传输。

从上面的例子可知,都是老师(主机)主导传球,按照规范的流程(通信协议),以保证传球的准确性,收发球的流程总结如下:
① 老师说开始了,表示开始信号(start);
② 老师提醒某个学生要发球,表示发送地址和方向(address/read/write);
③ 该学生回应老师(ack);
④ 老师发球/接球,表示数据的传输;
⑤ 收到球要回应:回应信号(ACK);
⑥ 老师说结束,表示IIC传输结束§。

数据有效性

IIC由两条线组成,一条双向串行数据线SDA,一条串行时钟线SCL。SDA线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在SCL线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示五有效数据,此时SDA会进行电平切换,为下一次数据表示做准备。如下图
在这里插入图片描述

开始信号和结束信号

IIC起始信号(S):当SCL高电平时,SDA由高电平向低电平转换;
IIC停止信号(P):当SCL为高电平时,SDA由低电平向高电平转换;

在这里插入图片描述

应答信号

IIC每次传输的8位数据,每次传输后需要从机反馈一个应答位,以确认从机是否正常接收了数据。当主机发送了8位数据后,会再产生一个时钟,此时主机放开SDA的控制,读取SDA电平,在上拉电阻的影响下,此时SDA默认为高,必须从机拉低,以确认收到数据
在这里插入图片描述

完整传输流程

IIC完整传输流程如下:

  1. SDA和SCL开始都为高,然后主机将SDA拉低,表示开始信号;
  2. 在接下来的8个时间周期里,主机控制SDA的高低,发送从机地址。其中第8为如果为0,表示接下来是写操作,即主机传输给从机;如果是1,表示接下来是读操作。即从机传输数据给主机(确定传输方向);另外,数据传输是从高位到最低位,因此传输方式位MSB
  3. 总线中对应从机地址的设备,发出应答信号;
  4. 在接下来的8个时间周期里,如果是写操作,则主机控制SDA的高低;如果是读操作,则从机控制SDA的高低;
  5. 每次传输完成,接收数据的设备,都发出应答信号;
  6. 最后SCL为高时,主机由低拉高,整个传输结束。

EEPROM介绍

EEPROM的全称是“电可擦除可编程只读寄存器”。通常用于存放用户配置信息数据,比如在开发板首次运行时,需要屏幕校准,校准后的配置信号就可以保存在EEPROM里,开发板断电后不丢失,下次启动,开发板自动读取EEPROM的校准配置信息,就不需要重新校准。

EEPROM和Flash的本质是一样的,Flash包括MCU内部的Flash和外部扩展的Flash,本开发板就有一个SPI接口的外部Flash,SPI后面会进行介绍。从功能上,Flash通常存放运行代码,运行过程中不会修改,而EEPROM存放用户数据,可能会反复的修改。从结构上,Flash按扇区操作,EEPROM通常按字节操作。两者区别这里就不再过多的讲,理解EEPROM在嵌入式中扮演的角色即可。

结构组成

在这里插入图片描述

设备地址

IIC设备都会有一个设备地址,不同容量的ATC2402,设备地址定义会有所差异,由数据手册可知

在这里插入图片描述
AT24C02的容量为2K,对应上图中的第一行,高四位固定为“ 1010”,中间三位由A2、 A1、 A0引脚的电平决定,比如A2~0引脚全接地,则值为“000”,最后的最低位为读写位, 0代表写命令, 1代表读命令。A2、 A1、 A0引脚电平需要由原理图决定,假设全接电源地,则如果需要向AT24C02写数据,则发送地址“ 1010 0000”,如果需要向AT24C02读数据,则发送地址“ 1010 0001”。
假设开发板有多个AT24C02挂在同一I²C总线上,通过这个规则,只需设计电路时,让A2、 A1、 A0引脚电平不同,即可区分两个AT24C02。

对于容量再大一点的AT24Cxx系列,比如AT24C04,器件地址由A2、 A1引脚决定,数据空间有P0决定。比如对AT24C04的02K空间操作,则P0为0,对2K4K空间操作,则P0为1。

写AT24Cxx

AT24Cxx支持字节写模式和页写模式。字节写模式是一个地址一个数据的写;页写模式是连续写数据,一个地址多个数据的写,但是页写模式不能自动跨页,如果超出一页,超出的数据会覆盖原先写入的数据。

如下图为AT24Cxx字节写模式的时序
在这里插入图片描述在MCU发出开始信号( Start)后,发出8 Bit的设备地址信息(图中读写位为低电平,即写数据),待收到AT24Cxx应答信号后,再发出要写的数据地址,再次等待AT24Cxx应答,最后发出8 Bit数据写数据,待AT24Cxx应答后,发出停止信号( Stop),完成一次单字
节写数据。

AT24C02容量为2K。因此数据地址范围为0x00~0xFF,即0 ~ 255,每个数据地址每次写1Byte,即8bit,也就刚好2048bit。对于1K容量的产品,数据地址范围为0x00 ~ 0x7F,最高位不会用到,因此下图中数据地址的最高位用“*”表示,意思就是不用关心,不会用

在这里插入图片描述

AT24Cxx的页写模式时序,如下图,与字节写模式的差异在于,不是只发送以Byte数据,而是任意多个,需注意,该模式不能跨页写,遇到跨页时,需要重新发送完整的时序。
在这里插入图片描述值得一提的是, 《AT24Cxx.pdf》 里提到每次写完之后,再到下次写之前,需要间隔5ms时间, 以确保上次写操作在芯片内部完成
在这里插入图片描述

读AT24Cxx

AT24Cxx支持当前地址读模式、随机地址读模式和顺序地址读模式。
当前地址读模式:就是在上一次读/写操作之后的最后位置,继续读出数据,比如上次读/写在地址n,接下来可以直接从n+1除读出数据;
随机地址读模式:是指定数据地址,然后读出数据;
顺序读模式:是连续读出多个数据。

在当前地址读模式下,无须发送数据地址,数据地址为上一次读/写操作之后的位置,如下所示
在这里插入图片描述注意:主机接收到数据后,无需产生应答信号

在随机地址读模式下,需要先发送设备地址,待读的数据地址,接着再重新发出信号,设备地址,读出数据
在这里插入图片描述
在顺序读模式下,需要先从当前地址读模式或随机地址读模式启动,随后可以连续读多个数据,时序如下
在这里插入图片描述

IIC模拟硬件设计

在这里插入图片描述U6为AT24C02芯片,它的A0、 A1、 A2都接地,因此该设备地址为“ 1010 000X”,当读该设备时, X为1,写该设备时, X为0。

U4的7脚为写保护引脚( Write Protect, WP),当该引脚为高,则禁止写AT24C02,这里直接拉低WP,任何时候都可直接写AT24C02。

此外, I2C的两个脚SCL和SDA都进行了上拉处理,从而保证I2C总线空闲时, 两根线都必须为高电平。
如果没有上拉,在主机发送完数据后,放开SDA,此时SDA的电平状态不确定,可能为高,也可能为低,无法确定是从机拉低给出应答信号。

IIC模拟软件设计

通过GPIO模拟I2C总线时序,对EEPROM设备AT24C02进行读写操作。

  1. 引脚初始化: GPIO端口时钟使能、 GPIO引脚设置为输入/输出模式( PB6、 PB7);
  2. 封装I2C每个环节的时序函数:起始信号、响应信号、读写数据、停止信号;
  3. 使用I2C协议函数,实现对AT24C02的读写;
  4. 主函数,每按一次按键,写一次AT24C02,接着读出来验证是否和写的数据一致;

源码解析

i2c.h
在这里插入图片描述
i2c.c

#include <stdio.h>
#include "i2c.h"
#include "main.h"
#include "timer.h"
#include "usart.h"

#define I2C_Delay()     us_timer_delay(5)

//初始化模拟I2C的引脚为输出状态且SCL/SDA都初始为高电平
void I2C_Init(void){
    
    GPIO_InitTypeDef GPIO_InitStruct={
    0};
    SCL_PIN_CLK_EN();
    SDA_PIN_CLK_EN();
    
    GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull=GPIO_NOPULL;
    GPIO_InitStruct.Pin=SCL_PIN;
    GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH;
    
    HAL_GPIO_Init(SCL_PORT,&GPIO_InitStruct);
    
    GPIO_InitStruct.Pin=SDA_PIN;
    HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct);
    
    SCL_H();
    SDA_H();
}

//配置SDA的引脚为输出。

static void I2C_SDA_OUT(void){
    
    GPIO_InitTypeDef GPIO_InitStruct={
    0};
    
    GPIO_InitStruct.Mode=GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull=GPIO_PULLUP;
    GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Pin=SDA_PIN;
    
    HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct);
}
//配置SDA引脚为输入模式
static void I2C_SDA_IN(void){
    
    GPIO_InitTypeDef GPIO_InitStruct={
    0};
    GPIO_InitStruct.Mode=GPIO_MODE_INPUT;
    GPIO_InitStruct.Speed=GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Pin=SDA_PIN;
    
    HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct);
}
//开始信号
void I2C_Start(void){
    
    I2C_SDA_OUT();//设置为输出模式
    
    SCL_H();//时钟线输出为高
    I2C_Delay();//延迟5ms
    
    SDA_H();//数据线输出为高
    I2C_Delay();
    
    SDA_L();//数据线输出低,由高到底表示开始信号
    I2C_Delay();
}
//结束信号
void I2C_Stop(void){
    
    I2C_SDA_OUT();//输出模式
    
    SDA_L();
    I2C_Delay();
    
    SCL_H();
    I2C_Delay();
    
    SDA_H();
    I2C_Delay();
}
//发出应答信号函数
void I2C_ACK(void){
    
    I2C_SDA_OUT();//设置为接收模式
    
    SCL_L();//时钟线输出为低
    I2C_Delay();
    
    SDA_L();
    I2C_Delay();
    
    SCL_H();
    I2C_Delay();
    
    SCL_L();
    I2C_Delay();
    
}
//发出非应答信号
void I2C_NACK(void){
    
    I2C_SDA_OUT();
    
    SCL_L();
    I2C_Delay();
    
    SDA_H();
    I2C_Delay();
    
    SCL_H();
    I2C_Delay();
    
    SCL_L();
    I2C_Delay();
}

//等待从机的应答信号
uint8_t I2C_GetACK(void){
    
    uint8_t time=0;
    
    I2C_SDA_IN();//设置为输入模式
    
    SCL_L();
    I2C_Delay();
    
    SDA_H();
    I2C_Delay();
    
    SCL_H();
    I2C_Delay();
    
    while(SDA_INPUT()){
    
        time++;
        if(time>250){
    
            SCL_L();
            return 1;
        }
    }
    SCL_L();
    return 0;
}

//发送一个字节的数据
void I2C_SendBYTE(uint8_t data){
    
    uint8_t cnt=0;
    
    I2C_SDA_OUT();
    
    for(cnt=0;cnt<8;cnt++){
    
        SCL_L();
        I2C_Delay();
        
        if(data & 0x80){
    
            SDA_H();
        }else{
    
            SDA_L();
        }
        data<<=1;
        
        SCL_H();
        I2C_Delay();
    }
    SCL_L();//发完数据
    I2C_Delay();
    I2C_GetACK();
}

//读取一个字节的数据

uint8_t I2C_ReadBYTE(uint8_t ack){
    
    uint8_t cnt=0;
    uint8_t data=0xFF;//确定data的值
    
    SCL_L();
    I2C_Delay();
    
    for(cnt=0;cnt<8;cnt++){
    
        SCL_H();  //SCL高,读取数据
        I2C_Delay();
        
        data<<=1;
        
        if(SDA_INPUT()){
    
            data |=0x01;
        }
        SCL_L();
        I2C_Delay();
    }
    //发送应答信号,低为应答,高为非应答
    if(ack==0){
    
        I2C_ACK();
    }
    else{
    
        I2C_NACK();
    }
    return data;
}




eeprom.h

#ifndef _EEPROM_H
#define _EEPROM_H
#include "stm32f1xx_hal.h"

#define EEPROM_DEV_ADDR     (0xA0)
#define EEPROM_WR           (0x00)
#define EEPROM_RD           (0x01)

#define EEPROM_WORD_ADDR_SIZE   (0x08)


void EEPROM_WriteByte(uint16_t addr,uint8_t data);
void EEPROM_ReadByte(uint16_t addr,uint8_t *pdata);
void EEPROM_Write_NByte(uint16_t addr , uint8_t *pdata,uint16_t size);
void EEPROM_Read_NByte(uint16_t addr,uint8_t *pdata,uint16_t size);
#endif

eerom.c

#include "eeprom.h"
#include <stdio.h>
#include"gpio.h"
#include "usart.h"
#include "i2c.h"
#include "timer.h"

//EEPROM写一个字节
void EEPROM_WriteByte(uint16_t addr,uint8_t data){
    
    //开始信号
    I2C_Start();
    
    //发送要写的设备地址
    I2C_SendBYTE(EEPROM_DEV_ADDR | EEPROM_WR);
    
    //要写的数据地址
    if(EEPROM_WORD_ADDR_SIZE==0x08){
    
        I2C_SendBYTE((uint8_t)(addr & 0x00FF));
    }
    else{
    
        I2C_SendBYTE((uint8_t)(addr>>8));
        I2C_SendBYTE((uint8_t)(addr & 0x00FF));
    }
    
    //写数据
    I2C_SendBYTE(data);
    
    //停止信号
    I2C_Stop();
}

//读EEPROM的一个字节
void EEPROM_ReadByte(uint16_t addr,uint8_t *pdata){
    
    //开始信号
    I2C_Start();
    
    //设备地址
    I2C_SendBYTE(EEPROM_DEV_ADDR | EEPROM_WR);
    
    //读取的数据的地址
    if(EEPROM_WORD_ADDR_SIZE==0x08){
    
        I2C_SendBYTE((uint8_t)(addr & 0x00FF));
    }else{
    
        I2C_SendBYTE((uint8_t)(addr>>8));
        I2C_SendBYTE((uint8_t)(addr & 0x00ff));
    }
    
    //开始传输
    I2C_Start();
    
    //设备的地址且设置为读模式
    I2C_SendBYTE(EEPROM_DEV_ADDR | EEPROM_RD);
    
    //读取数据,因为读取数据不需要回应信号
    *pdata=I2C_ReadBYTE(NACK);
    
    //结束
    I2C_Stop();
}

//写N个字节
void EEPROM_Write_NByte(uint16_t addr , uint8_t *pdata,uint16_t size){
    
    uint16_t i=0;
    
    for(i=0;i<size;i++){
    
        EEPROM_WriteByte(addr,pdata[i]);
        addr++;
        HAL_Delay(10);//每次期间需要延迟5ms,这里延迟10ms防止出错
    }
}
//连续读数据
void EEPROM_Read_NByte(uint16_t addr,uint8_t *pdata,uint16_t size){
    
    
    uint16_t i=0;
    for(i=0;i<size;i++){
    
        EEPROM_ReadByte(addr,&pdata[i]);
        addr++;
    }
}

timer.h

#ifndef _TIMER_H
#define _TIMER_H

#include "stm32f1xx_hal.h"

void us_timer_delay(uint16_t t);

#endif

timer.c

#include "timer.h"

void us_timer_delay(uint16_t t){
    
    uint16_t counter=0;
    while(t--){
    
     counter=10;
     while(counter--){
    
     }  
    }
}

main.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2022 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "eeprom.h"
#include<stdio.h>
#include "i2c.h"
#include "timer.h"
#include "string.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_NVIC_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
    
  /* USER CODE BEGIN 1 */
char tx_buffer[20]={
    "Hello IIC"};
char rx_buffer[20]={
    0};
  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  /* Initialize interrupts */
  MX_NVIC_Init();
  /* USER CODE BEGIN 2 */
printf("*****************\r\n");
printf("i2c test\r\n");
printf("*****************\r\n");
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
      if(get_KeyValue()){
    
      //读写字符串并打印
            EEPROM_Write_NByte(0,(uint8_t *)tx_buffer,sizeof(tx_buffer));
            HAL_Delay(1);
          
            EEPROM_Read_NByte(0,(uint8_t *)rx_buffer,sizeof(rx_buffer));
            HAL_Delay(1);
            
          printf("EEPROM Write :%s\r\n",rx_buffer);
          printf("EEPROM Read :%s\r\n",rx_buffer);
          
          //清空接收buffer
          memset(rx_buffer,0,sizeof(rx_buffer));
      }
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
    
  RCC_OscInitTypeDef RCC_OscInitStruct = {
    0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {
    0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    
    Error_Handler();
  }
}

/**
  * @brief NVIC Configuration.
  * @retval None
  */
static void MX_NVIC_Init(void)
{
    
  /* EXTI0_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(EXTI0_IRQn);
  /* USART1_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(USART1_IRQn);
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
    
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
    
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
    
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */


运行结果

在这里插入图片描述

源码

Git

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_56145255/article/details/123030504

智能推荐

0719学习总结(文件流的输入输出)_请综合输入输出流、文件流和字符串流、特别是文件流中指针相关函数进行文件随机位-程序员宅基地

文章浏览阅读482次。1.输入输出流常用的有:iostream 包含了对输入输出流进行操作所需的基本信息:fstream 用于用户管理的文件的I/O操作。strstream 用于字符串流I/O。stdiostream 用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序。iomanip 在使用格式化I/O时应包含此头文件。2.在iostream头文件中定_请综合输入输出流、文件流和字符串流、特别是文件流中指针相关函数进行文件随机位

鸿蒙osbeta2.0上手上手,鸿蒙OS 2.0上手视频曝光,Mate40系列首批无缘,界面与EMUI一致...-程序员宅基地

文章浏览阅读47次。原标题:鸿蒙OS 2.0上手视频曝光,Mate40系列首批无缘,界面与EMUI一致华为在今天正式召开鸿蒙OS 2.0开发者沟通会,此次沟通会将会讨论鸿蒙OS 2.0的各种相关开发问题。对于普通消费者来说,开发者的事情并不是大家关心的。作为消费者最关心的还是鸿蒙OS 2.0啥时候能够适配自己的手机。 在华为召开开发者大会的同时,华为也在今天正式放出了鸿蒙OS 2.0手机开发者Beta版。目前已经有博...

Jenkins首次安装推荐插件出错 No such plugin: cloudbees-folder 超详细解决方案-程序员宅基地

文章浏览阅读6.8k次,点赞2次,收藏12次。我的环境:腾讯云 CentOS7 轻量应用服务器docker run -u root -itd -p 8080:8080 -p 50000:50000 -v /var/jenkins_home:/var/jenkins_home -v /var/run/docker.sock:/var/run/docker.sock --name jenkins-master jenkinsci/blueocean当我首次通过镜像启动一个 Jenkins 容器后,访问服务器 公网ip:8080 访问 Jenkin._no such plugin: cloudbees-folder

JSP入门-基本语法_用于为其他动作提供附加信息的动作是-程序员宅基地

文章浏览阅读392次。JSP入门-基本语法_用于为其他动作提供附加信息的动作是

DNS服务器搭建_server registered via geo dns in ap-east-1-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏59次。本篇是关于DNS服务器的搭建配置教程!_server registered via geo dns in ap-east-1

7.2(stm32以太网)_w25qxx_write_nocheck-程序员宅基地

文章浏览阅读3.2k次。《7.2》1.SPI的写入整个扇区W25QXX_Write_NoCheck()--->W25QXX_Write_Page()--->SPI1_ReadWriteByte(); void W25QXX_Write_NoCheck(pBuffer , addr,num) { u8 pageremain; pageremain = addr - addr%256;_w25qxx_write_nocheck

随便推点

ROS命令_ros查找功能包-程序员宅基地

文章浏览阅读2.7k次。文章目录1.功能包、功能包集相关2.工作空间1.功能包、功能包集相关(1) rospack find turtlesim:查找turtlesim包的路径/opt/ros/noetic/share/turtlesim(2) rosls turtlesim:可获取功能包下面的文件列表cmake images msg package.xml srv(3) rosstack find [stack_name]:查找已经在系统中安装过的某个功能包集(4)roscd turtlesim:进入某个文_ros查找功能包

hadoop_connecting to 192.168.128.130:22... could not conn-程序员宅基地

文章浏览阅读412次。连接xshell错误信息如下Connecting to 192.168.128.130:22...Could not connect to '192.168.128.130' (port 22): Connection failed.最有可能的原因是在配置 vi /etc/sysconfig/network-scripts/ifcfg-eth0 的时候使用的IPADDR =192.168.128.130 , 这个IP地址不在虚拟机VMware8本身的net 连接的ip范围内,解决步骤如下_connecting to 192.168.128.130:22... could not connect to '192.168.128.130' (

CentOS7 安装 PHP7 完全详细教程_centos7 命令安装php7-程序员宅基地

文章浏览阅读2w次,点赞3次,收藏16次。CentOS7的默认PHP版本是PHP5,但是如果我们要安装PHP7,不需要将现有的PHP5删除,只要将PHP升级到PHP7即可。使用 yum provides php 命令可以获取CentOS7的PHP包安装情况。显示的是在现有的安装源中能够安装的最新版本为:php-5.4.16-46.el7.x86_64在安装PHP7之前,建议先升级更新一下CentOS7的安装包:yum -y upd..._centos7 命令安装php7

偏差平方和说明什么_什么是平方误差和均方误差-程序员宅基地

文章浏览阅读7.1k次。展开全部均方误差是指参数估计636f70793231313335323631343130323136353331333431373161值与参数真值之差平方的期望值,记为MSE。MSE是衡量“平均误差”的一种较为方便的方法,MSE可以评价数据的变化程度,MSE的值越小,说明预测模型描述实验数据具有更好的精确度。误差平方和又称残差平方和、组内平方和等,根据n个观察值拟合适当的模型后,余下未能拟合部份..._参参数偏差平方和

nginx mysql php源码编译_snowy +nginx-程序员宅基地

文章浏览阅读87次。lamp架构=Linux/unix/windows(操作系统)+apache/nginx……+mysql/pgsql +php/python/golang。开发能力要自己努力。起码需要一些语言基础。不懂代码何以精通?不堪官方源码何以做到熟悉?nginxnginx有官方网站:www.nginx.comtar zxf nginx-1.18.0.tar.gz ##解压。解压之后里面有configure,表示他是开源的。./configure --help ##查看一些参数 _snowy +nginx

Rancher备份&&还原_rancher还原db-程序员宅基地

文章浏览阅读834次。Rancher备份&&还原一、Rancher 备份1、备份# !/bin/bash# /opt/backup/backup-rancher.shDATE=$(date +%Y%m%d%H%M)echo "备份rancher数据"echo "1.拷贝容器内数据"IMAGE_ID=`docker ps |grep rancher:v2.5.8|awk '{print $1}'`docker cp $IMAGE_ID:/var/lib/rancher /opt/ba_rancher还原db

推荐文章

热门文章

相关标签