浮点数的坑很深,但不多

视频先行

下面是视频内容的脚本文案原稿分享。

a18c42d6ef84f0ccd6943174692abfa5.jpeg

问题是真实存在的

大家好,我是扔物线朱凯。刚才那个 0.1 + 0.2 不等于 0.3 的情况是真实存在的,不信你可以亲自试一下。我用的是 Kotlin,你换成 Java、JavaScript、Python、Swift 也都是这样的结果。要解决它也简单,在数值的右边加个 f,把它从双精度改成单精度的就可以了:

fbd0a796826508c56451a503a43466b0.png 33cb137c4fe2adc38fe8a3057f9a1a7a.png

但这并不是一个通用的解决方案,比如有的时候情况会反过来:双精度的没有问题,而单精度的却有问题:233e66259dc5c1a8f145388d392dbf50.png70603dda0e0f3b60a4c9f97445cf42cf.png

要知道,这些偏差都是开发里真实会发生的,所以了解这些问题的本质原因和解决方案是非常必要的——比什么内存抖动重要多了。今天咱就说一下这个。不难啊,别害怕,你看进度条不长。

浮点数的范围优势和精度问题

大多数编程语言对小数的存储,用的都是浮点数的形式。浮点数其实就是一种科学计数法,只不过它是电脑用的、二进制的科学计数法:

十进制:500000 科学计数法:5 * 105 二进制:1111010000100100000 浮点数(二进制科学计数法):1.111010000100100000 * 218

科学计数法的好处我们上学的时候老师就说过,可以用较少的数位来记录绝对值非常大或者非常小的数:

1000000000 -> 1 x 109 0.000000001 -> 1 x 10-9

但当你要记录的数位比较长的时候:

1000000001 -> 1.000000001 x 109 1.000000001 -> 1.000000001 x 100

科学计数法的优势就没了。所以科学计数法都会规定有效数字的个数,有效数字之外的就四舍五入掉了:

1000000001 -> 1.0 x 109 // 有效数字 2 位 1.000000001 -> 1.0 x 100 // 有效数字 2 位

这就造成了精度的损失。你看到一个 1.0 x 109,你不知道它是从 10 亿这个数转换过来的,还是从 9.6 亿或者 10.3 亿或者别的什么数转过来的,因为它们都可以写成 1.0 x 109。1.0 x 109 代表的不是 10 亿这一个数,而是它附近范围内的一大堆数,这就是精度的损失。不过这是故意的,科学计数法就是用精度作为代价来换取记录的简洁。计算机的浮点数也是完全一样的道理。它本质上就是一种科学计数法,只不过是二进制的。比如,同样在 JVM 里占 32 位的 float (Float) 和 int (Int),float 却可以表达比 int 更大的整数:

9b04c44e61751e476b255abeb531600b.png32 位的二进制数据只有 232 个取值,再加上还要区分正负,所以 int 的最大值是 231 - 1 也就是这个数:

2,147,483,647

但是 float 同样是 32 位,却能突破这个限制,赋值为这么大的一个数。别说 int 了,它的范围比 64 位的 long 还大:

e05aa0175660e7d4173fcfe4deae839c.png除此之外,它还能表示小数:

小数代码。

怎么做到的?靠牺牲精度来做到的。float 虽然也是 32 位,但它会从里面拿出 8 位来保存指数值,也就是小数点的位置。8 位的指数可以达到 ±27 也就是 ±128,也就是小数点往左或者往右 128 个二进制位,换算成十进制是左右各 38 个十进制位。每移动一位就是放大或者缩小 10 倍,38 位呀,非常大的放大和缩小倍数了——int 只记录整数,不记录小数,但它最大的值也只是一个十位数:

2,147,483,647

这就是为什么 Java 和 Kotlin 的 float (Float) 可以保存某些很大的整数,因为它有专门的指数位:

4e35805f03bd9e3c035735d732bd780d.png但同样,它用这 8 位来保存指数,那么相应的它的有效数字就变短了,所以它的精度是比 int 要低的。这就导致某些 int 能表达的整数, float 却表达不了,比如 50000005(五千万零五): b254cbb524d0b7583d58226663a4dc23.png这个数虽然不长,但是精度——太高了。虽然只是超出精度而不是超出取值范围,所以只显示了黄线警告而不会拒绝编译,但由于 Float 确实表达不了 50000005,所以在运行时只能在它的能力范围内拿一个最接近的数来进行使用,而无法使用这个数本身: bc68cbf5b22a5bb9a02801214da35990.png 2429dfe45f2afc2666b28d7e0391ddc4.png

可能跟很多人的直观想法不太一样,为什么末位是 5 不行,但换成 4 就可以了?不是应该换成 0 才行吗?因为这是二进制的。我们看着这个数不够整、精度不够低,这是因为我们是十进制的思维,但只要二进制觉得它挺整的、觉得它没有超出精度范围就够了。

50000000: 10111110101111000010000000 // 有效数字 19 位,holde 得住 50000004: 10111110101111000010000100 // 有效数字 24 位,同样 hold 得住 50000005: 10111110101111000010000101 // 有效数字 26 位,hold 不住,「四舍五入」到 50000004

而 int 在这时候就很靠谱了,它是可以表达自己范围内的任何整数的,50000005 对它完全没问题:

673b3f0d7f2ab425c1c6d0ef6e000f00.png 8ca6648451bcc372f44dd9b7c5ce6ff1.png 13a0d9788f5f941764d02165b2e2d1d4.png这是整数的情况,换成小数也是同样的道理。你用 Float 可以表示 500000,也可以表示 0.05,但无法表示 500000.05,因为它超出精度范围了: 26d1411493d372273af9c1501014bf97.png不过二进制的数值并不能直接照抄十进制的小数点平移,所以你会发现加了小数点之后,500000.05f 会被「四舍五入」到 500000.06 而没有跟前面一样是 04: b7decbff9f498b9fc3394e12976d0a6f.png 6a962fbdf786698d1269be4b35aa56d6.png

而如果把小数点往右移一位,改成 5000000.5,就直接不用「四舍五入」了:21471c036219a20e4c58a0b549f7910e.png0ffdc0cbad65ae640a1092acfca0aeca.png

你看,黄线没了,打印出来也是没问题的。因为 0.5 的二进制格式是 0.1,只用一位小数就表达了,所以整个数的精度没有超:

5000000.5:10011000100101101000000.1 // 有效数字 24 位,hold 得住

同样是 32 位的大小,float 却比 int 少了 8 位有效数字长度,降低了精度,这是浮点数的弱点所在。而这个弱点也是故意的,因为这少了的 8 位用来存储指数了,也就是小数点的位置,改变指数的值就是改变小数点的位置——这也就是「浮点数」这个名字的含义。所以它是用精度作为代价,换来了更大的表达范围,让它可以表达小数,也可以表达很大的整数,远远超过 int 上限的整数。浮点数可以用于表示小数,所以我们通常把它跟小数画等号;但其实对于一些数值特别大但有效数字并不多的整数的情况,也可以考虑使用浮点数。不过就是刚才说过的,有得有失,浮点数的精度比较低。有多低呢?对于 float 来说,它的有效数字换算成十进制是 6-7 位。到了 8 位的时候,有很多数就无法精确表达了,比如 500000.05。而 double 的长度是 float 的两倍,有 64 位,它的精度就比较高了,它的有效数字相当于 15-16 位的十进制有效数字,能应付大部分的需求了——当然了如果你面向的是整数,那直接用 intlong 可能更好。

float vs double

说到这儿呢,咱就说一下关于 float (Float)double (Double) 的选择问题。这其实是一个很容易出错但是经常被忽略的地方:float 的精度是比较低的,对于很多场景都可能会不够用。比如你如果用来做金额的计算,去掉小数点右边的两位之后,只有五位数字可以用了,也就是 10 万级的金额就不能用 float 了。那么在选择浮点数的类型的时候,你要时刻意识到这件事,在精度不够用的时候就选 double。这其实也是为什么在 Java 和 Kotlin 里整数的默认类型虽然是更短的 int (Int) 而不是 long (Long),但浮点数的默认类型却是更长的 double (Double),而不是 float (Float)

3247934f96101ec7859c10faa7dc00de.png

类型是 double (Double)。

因为 float (Float) 的适用场景过于受限了。当然了如果你明确知道在某个场景下 float 够用了,那肯定用 float 更好,省内存嘛。不过说到省内存我又要说了,不用过于纠结,对于很多场景来说,double 的双倍内存占用带来的性能损耗其实是很小的,小到完全可以忽略——你想想,32 位和 64 位才差多少?差 32 位,也就是 4 个字节,4 个 B,你省 1000 个才 4K 的大小——所以如果你真的想懒省事,全都用 double 大多数时候也是没有任何问题的。一个字符都 16 位了,也没见谁因为这个去精简软件界面的文字啊是吧。不过一些计算密集型或者内存密集型的工作,比如高频的循环过程或者某些需要大量复用的数据结构,还是得慎重考虑数值类型的啊,能用 float 就用 float。何止是 float 呀,在性能要求高的场景里,你甚至可能需要考虑要不要用单个 int 或者 long 变量来代替多个 boolean 变量去进行联合存储,以此来节约内存。而对于一般场景,double 虽然占双倍内存,但其实影响不大。

0.1 + 0.2 != 0.3 的问题

除了精度,浮点数还有个问题是,它是二进制的。这对整数还好,但对于小数来说,有很多十进制小数是无法转换成有限的二进制小数的。二进制只有 0 和 1,所以它的 0.1 就是 2 的 -1 次方,也就是十进制的 0.5——二进制的 0.1 跟十进制的 0.5 是相等的;同理,它的 0.01 就是 2 的 -2 次方,也就是十进制的 0.25;而它俩相加的结果 0.11,对应的就是十进制的 0.75 。总之,你用 1 去反复地除以二,这些结果——以及这些结果的加和——都可以被二进制的小数完美表示。但如果一个小数无法被拆成这种形式,那它就无法被完美转换成二进制,比如——0.1。可能有点反直觉,但十进制的 0.1 是无法被转换成一个有限的二进制小数的,它只能用一个无限循环小数来表达:

0.00011001100110011...

而且,浮点数并不会真的把它当做无限循环小数来保存,而是在自己的精度范围内进行截断,把它当一个有限小数来保存。这就造成了一定的误差。我们用的各种编程语言和运行时环境会对这种问题进行针对性优化,让我们尝试打印 0.1 的时候依然可以正常打印出 0.1,但在进行了运算之后,叠加的误差可能就会落在这种优化的范围之外了,这就是为什么在很多语言里,0.1 + 0.2 不等于 0.3,而是等于 0.300000……4:65a04da4cb015064c95ba7c8371503d6.png3dc34e7313c1918f2c8982e8ebaa140a.png同样的例子还有很多,比如 0.7 / 5.0 不等于 0.14:a5ac6d4f24b647f8bdcded384b188529.png6ac2a9e5c0cc60c65026d6369b825207.png注意了,我这里用的是不带 f 的小数,也就是用的 double。如果我给它们加上 f 也就是改用 float 的话,就恢复正常了:7cc83f1d26ccf99b7769f3a7e54b6b7d.png7ec4dc013eee3f890b48901bce33b9e5.png这是为啥?这可不是因为 float 的精度比较低所以误差被掩盖了,而是对于这两个算式来说,恰好 float 的精度在截断之后的计算结果,误差依然在优化范围内,而 double 的掉到了优化范围之外而已。我如果把这个 0.1 换成 0.15,那状况就相反了,float 出现了问题,而 double 反而没问题了:1b742606f9ef0ad25757f85c8eff8ea9.pngd0d82172dd4060a8b962bab1866b7cdf.png为啥?因为这次 float 掉到范围之外了。所以,这种计算之后出现数值偏差的问题,是普遍存在的,它甚至不是精度太低而导致的,而就是因为十进制小数无法往二进制进行完美转换所导致的,不管你用多高精度的都会出这种问题,只要你用的是浮点数。我们用的各种编程语言的浮点数的实现,遵循的都是同一套标准,这个标准是 IEEE 推出的——要怪怪它去。

应对一:主动限制精度

那怎么办呢?一般来说有两种应对方向。第一种是在计算之后、输出或者比较的时候,主动限制精度:

val a = ((0.1 + 0.2) * 1000).round() / 1000 // 0.3
if (abs(0.1 + 0.2 - 0.3) < 0.001) {
    ...
}

看着有点憨是吧?甚至有一丝羞耻。没办法,写吧!我也这么写的,大家都这么写的。浮点数就这样!

应对二:不用浮点数(不是开玩笑)

除此之外,另一个应对方向就是,你干脆别用浮点数了,用别的方案。比如 Java 有个叫 BigDecimal 的东西,就是专门进行精确计算用的。不过 BigDecimal 的使用没有浮点数这么简单,运算速度也比浮点数慢,所以大多数情况下,忍一忍,用浮点数还是会好一点。

总结

好,浮点数的东西大概就这么多。下期有点想再说点 Compose 的东西了,不过也不一定,我看情况吧。如果你喜欢我的视频,还请帮我点赞和转发。关注我,了解更多 Android 开发的知识和技能。我是扔物线,我不和你比高低,我只助你成长。我们下期见!694a72c727ec18b38103fde359c26732.jpeg

/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : Main program body ****************************************************************************** * @attention * * Copyright (c) 2025 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 "can.h" #include "gpio.h" #include "string.h" #include "stm32f4xx_hal.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ typedef struct { uint8_t data[16]; // 直接使用 16 字节的数组填充构成 16字节的结构体 }CAN_struct;//储存发送或接收的16字节数据 typedef union { float a; uint8_t megt[16];//megt 无符号字符数组成员名,在共用体里和float类型成员共享内存,实现浮点数与字节数组的转换(硬件不能直接解析,逐字节发送再转换成float;必须把float拆成字节数组填入,接收方再按协议还原) }Unionfloat; typedef union//16字节结构体与字节数组转换,让数据适配硬件通信,存储,协议,(与上述float类似) { CAN_struct DATAStruct;//共用体成员名 uint8_t send[16]; }Union_struct; /* 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 */ CAN_RxHeaderTypeDef Rxhader; //Rxhader:存储接收到的 CAN 消息的头部信息(说明书身份卡)(告诉系统谁发的,发给谁,ID,标准帧,扩展帧,DLC,远程帧标记,反馈错误状态,时间戳) uint8_t RxData[8]; //RxData:用于存储接收到的 CAN 数据 float receivedFloat; //receivedFloat:用于存储接收到的浮点数 uint8_t rxframer=0; //rxframer:用于标记是否接收到完整的16字节数据。防止数据丢包错位,数据越界,解析乱码//rxframer为0(未收全)为1(收全)【后面先判断是否为1,再决定是否解析数据】 uint8_t structBuffer[16]={0}; //structBuffer:用于存储接收到的16字节结构体数据。数组所有元素初始化为0,不会被旧数据干扰 uint8_t buffer[8]; //buffer:用于临时存储接收到的8字节数据。防止没处理完数据就丢了 Unionfloat receiveunionfloat; //receiveunionfloat:用于将接收到的字节数组转换为浮点数。 Union_struct receiveunionstruct; //receiveunionstruct:用于将接收到的字节数组转换为16字节结构体。 CAN_struct receiveStruct; //receiveStruct:用于存储接收到的16字节结构体数据。 CAN_struct h; //h:用于存储要发送的16字节结构体数据。 /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); /* USER CODE BEGIN PFP */ void MyCAN_Transmit_struct(CAN_struct* shuatment); void MyCAN_Transmit_Float(float data); /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ void MyCAN_Transmit_Float(float data) //发送浮点数data { Unionfloat transmitUnionfloat; //共用体定义变量transmitUnionfloat transmitUnionfloat.a=data; //data赋值给联合体成员a,transmitUnionfloat.megt就会存储对应浮点数的字节形式,实现浮点数到字节数组的转换 uint32_t TxMailbox; //存储CAN发送邮箱编号,HAL_CAN_AddTxHeaderTypeDef会用到,告知数据放到哪个发送邮箱 uint32_t TimeOut=0; //初始化超时计数器为0 CAN_TxHeaderTypeDef TxMessage; //CAN_TxHeaderTypeDef类型结构体,用来配置CAN发送报文的头部信息(ID,数据长度,帧类型) //配置CAN发送头部信息 TxMessage.StdId = 0x555; TxMessage.ExtId = 0x0000; TxMessage.IDE = CAN_ID_STD; TxMessage.RTR = CAN_RTR_DATA; //浮点型用数据帧 TxMessage.DLC = 4; //浮点数转4字节数据 TxMessage.TransmitGlobalTime = DISABLE;//关闭全局时间戳功能,发送报文时不附加全局时间信息 //把配置好的报文(TxMessage)和要发送的数据(transmitUnionfloat.megt浮点数转字节数组)放到hcan1对应的CAN外设发送邮箱(TxMailbox),函数返回HAL_StatusTypeDef类型状态 HAL_StatusTypeDef state=HAL_CAN_AddTxMessage(&hcan1, &TxMessage,transmitUnionfloat.megt , &TxMailbox);//状态(发送邮箱) while(state!= HAL_OK) //state!= HAL_OK放入邮箱失败,进入循环重新尝试/检测超时 //HAL_OK成功放入邮箱等待发送 { TimeOut++; if (TimeOut > 100000) //计数器超过100000,认为发送失败,跳出循环 { break; } } } //发送16字节结构体数据 void MyCAN_Transmit_struct(CAN_struct* shuatment) //参数是CAN_struct类型指针shuatment,接收要发送的结构体数据,发送出去 { uint32_t TimeOut=0; uint32_t TxMailbox; Union_struct tx_struct; //Union_struct类型变量定义tx_struct //sizeof(CAN_struct)确保拷贝长度整个结构体大小,数据从入参到共用体变量的转移 memcpy(&tx_struct.DATAStruct, shuatment, sizeof(CAN_struct));//调用memcpy函数(string.h里的内存拷贝函数),把shuatment指针指向CAN_struct结构体内容,拷贝到tx_struct的DATAstruct成员里 CAN_TxHeaderTypeDef TxMessage; //CAN发送头部配置 TxMessage.StdId = 0x666; TxMessage.ExtId = 0x0000; TxMessage.IDE = CAN_ID_STD; TxMessage.RTR = CAN_RTR_DATA; //结构体数据帧 TxMessage.DLC = 8; //一次发送八字节 TxMessage.TransmitGlobalTime = DISABLE; //第一次发送报文 HAL_StatusTypeDef status =HAL_CAN_AddTxMessage(&hcan1, &TxMessage, tx_struct.send, &TxMailbox); while(status!= HAL_OK) { TimeOut++; if (TimeOut > 100000) { break; } } TxMessage.StdId = 0x777; //第二次发送报文(补发或分段发) HAL_CAN_AddTxMessage(&hcan1, &TxMessage, &tx_struct.send[8], &TxMailbox);//发送后续8个字节 while(status!= HAL_OK) { TimeOut++; if (TimeOut > 100000) { break; } } } /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* 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_CAN1_Init(); /* USER CODE BEGIN 2 */ for(int i=0;i<16;i++)//初始化一个16字节结构体数据 h { // CAN_struct h; h.data[i]=i; } /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ MyCAN_Transmit_Float(3.14f); //发送浮点数 MyCAN_Transmit_struct(&h); //发送结构体数据 while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */ } /** * @brief System Clock Configuration * @retval None */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /** Configure the main internal regulator output voltage */ __HAL_RCC_PWR_CLK_ENABLE(); __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); /** 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.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = 16; RCC_OscInitStruct.PLL.PLLN = 192; RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; RCC_OscInitStruct.PLL.PLLQ = 4; 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_DIV4; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_1) != HAL_OK) { Error_Handler(); } } /* USER CODE BEGIN 4 */ uint8_t count=0; void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan1) //CAN 接收FIFO0消息挂起回调函数,处理接收到的数据 { count++; //每收到一条CAN消息就自增1,统计接收报文的次数 Unionfloat floatBuff; if(HAL_CAN_GetRxMessage(hcan1,CAN_RX_FIFO0,&Rxhader,buffer)==HAL_OK) //buffer收到的实际字节数据存在这里 { if(Rxhader.StdId==0x555) //准备转成float数据 { memcpy(floatBuff.megt,buffer,sizeof(floatBuff.megt)); receivedFloat = floatBuff.a; //存储接收到的浮点数 } if(Rxhader.StdId==0x666) { memcpy(structBuffer,buffer,8); //buffer里的八字节数据拷贝到structBuffer rxframer=1; //接收到分片等待拼接,下一步处理拼接逻辑 } if((Rxhader.StdId==0x777)&&rxframer==1) //两个同时满足 { memcpy(&structBuffer[8],buffer,8); //拷到前8个字节后面 memcpy(receiveunionstruct.send,structBuffer,16); //16字节数据拷到send数组成员里 receiveStruct=receiveunionstruct.DATAStruct; //存储接收到的结构体 rxframer=0; //分片处理完毕等待下一次 } } } /* 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 */
最新发布
06-08
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值