news 2026/10/5 3:30:04

STM32F407+LwIP+MQTT可靠通信实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407+LwIP+MQTT可靠通信实战指南

1. 为什么在STM32F407上跑MQTT不是“接上线就完事”——从裸机到可靠通信的三道生死关

你手头有一块STM32F407ZGT6开发板,网口接上了DP83848 PHY芯片,Keil MDK-ARM 5.34(AC6编译器)环境已配好,LwIP 2.1.2也通过CubeMX生成了基础工程。你照着某篇博客把mqtt_client.c文件加进去,调用mqtt_connect(),串口打印出"Connected!"——然后呢?
然后你发现:

  • 发送10条消息,服务器只收到7条;
  • 连续运行2小时后,TCP连接静默断开,mqtt_keepalive()没触发重连;
  • 换个网络环境(比如公司内网带防火墙),连第一次都失败,ERR_CONN错误码反复出现;
  • 用Node-RED订阅主题,偶尔收到乱码,Wireshark抓包显示TCP窗口为0,LwIP的pbuf池早被耗尽。

这不是代码写错了,而是你跳过了STM32F407+LwIP+MQTT这条链路上最致命的三个底层约束:内存资源硬边界、TCP/IP栈状态机与MQTT协议语义的错位、以及嵌入式环境下“永远在线”的幻觉。
我用这块板子做过工业温控网关,连续7×24小时运行,MQTT消息投递成功率99.98%(基于阿里云IoT平台实测)。关键不是堆代码,而是把LwIP的内存池配置、MQTT客户端状态机、以及STM32F407的SRAM分配这三件事拧成一股绳。
本文不讲“如何新建工程”,不贴HAL_ETH_Init()函数,而是带你直面:

  • 为什么MEM_SIZE设成16KB会直接导致MQTT心跳超时?
  • 为什么MQTT_CONNECT_TIMEOUT必须大于LwIP的TCP_TMR_INTERVAL?
  • 为什么PA8引脚接USB Type-C的VBUS检测,在MQTT重连逻辑里成了救命稻草?
    这些细节,官方例程不会写,论坛帖子只会说“我改了参数就好了”,但真正卡住你项目的,恰恰是这些藏在.h文件宏定义里的数字和时序关系。

2. LwIP内存池的“精算师”思维:从SRAM布局到pbuf生命周期管理

STM32F407的SRAM总容量192KB,但实际能给LwIP用的远少于这个数。你打开lwipopts.h,看到一堆#define MEM_SIZE 16384、#define MEMP_NUM_PBUF 16……这些数字不是凭空写的,而是要对着你的硬件资源一笔笔算出来的。

2.1 SRAM物理分区与LwIP内存映射的真实约束

STM32F407的SRAM分为两块:

  • SRAM1(112KB):起始地址0x20000000,这是C标准库malloc默认使用的区域;
  • SRAM2(16KB):起始地址0x2001C000,这块RAM支持硬件ECC校验,但不能被GCC的malloc直接管理。

很多初学者把LwIP所有内存池都塞进SRAM1,结果sys_malloc和LwIP的mem_malloc争抢同一片内存,pbuf_alloc()返回NULL时根本分不清是LwIP池满还是系统堆溢出。我的做法是:

// 在stm32f4xx_hal_conf.h中禁用HAL库的动态内存分配 #define HAL_USE_STATIC_ALLOC 1 // 强制HAL使用静态数组 // 在lwipopts.h中,将LwIP内存池全部划到SRAM2 #define MEM_MEM_ALIGN_SIZE 4 #define MEM_SIZE (12*1024) // SRAM2仅16KB,留4KB给HAL DMA描述符 #define MEMP_NUM_PBUF 12 // 每个pbuf约256字节,12×256=3KB #define MEMP_NUM_UDP_PCB 4 // MQTT只用TCP,UDP PCB全砍掉 #define MEMP_NUM_TCP_PCB 2 // 一个用于MQTT连接,一个备用 #define MEMP_NUM_TCP_SEG 16 // TCP分段缓冲,每段约512字节 → 8KB

提示:MEMP_NUM_TCP_SEG是最大陷阱!MQTT的PUBLISH报文最大长度默认128字节,但LwIP的TCP分段缓冲区必须容纳整个TCP报文头+MQTT头+有效载荷。实测16是最低安全值,低于此值会导致tcp_output()失败,MQTT消息卡在发送队列。

2.2 pbuf的三种类型与MQTT报文的精准匹配

LwIP的pbuf有PBUF_RAM、PBUF_ROM、PBUF_REF三种类型,MQTT客户端必须用对:

  • PBUF_RAM:数据存于RAM,可读写,适用于MQTT CONNECT/PUBLISH报文构造(需动态拼接);
  • PBUF_ROM:数据存于Flash,只读,适用于MQTT固定头(如0x10CONNECT标志);
  • PBUF_REF:引用外部RAM,适用于大payload(如传感器JSON数据),避免内存拷贝。

我在mqtt_client.c中重写了mqtt_msg_init():

// 原版:所有数据copy到pbuf_ram → 内存浪费 // 新版:固定头用PBUF_ROM,payload用PBUF_REF err_t mqtt_msg_init(struct mqtt_client *client, u8_t type, u8_t dup, u8_t qos, u8_t retain) { struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, MQTT_HEADER_LENGTH, PBUF_ROM); if (!p) return ERR_MEM; // 固定头直接映射到Flash常量区 static const u8_t mqtt_header[2] = {0x10, 0x00}; // CONNECT固定头 p->payload = (void*)mqtt_header; // 直接指向Flash // payload部分用PBUF_REF引用外部buffer struct pbuf *payload_p = pbuf_alloc(PBUF_TRANSPORT, payload_len, PBUF_REF); payload_p->payload = sensor_data_buffer; // 指向DMA接收的原始数据 pbuf_chain(p, payload_p); // 链式拼接,零拷贝 client->outbound_pbuf = p; return ERR_OK; }

实测效果:单次PUBLISH内存占用从896字节降至320字节,MEMP_NUM_PBUF从24降到12,SRAM2剩余空间从1.2KB提升至5.8KB。

2.3 TCP定时器与MQTT Keep-Alive的时序咬合

LwIP的TCP层有独立定时器(tcp_tmr()),默认每250ms执行一次,负责重传、窗口探测、连接保活。而MQTT协议要求客户端在keepalive秒内发送PINGREQ。若两者不同步,会出现:

  • LwIP认为连接正常(TCP窗口未关闭),但MQTT服务器因超时踢掉客户端;
  • 或者LwIP的tcp_slowtmr()因优先级低被延迟,导致MQTT心跳包堆积。

解决方案是强制对齐:

// lwipopts.h #define TCP_TMR_INTERVAL 1000 // 改为1秒,与MQTT keepalive=60s整除 #define MQTT_KEEP_ALIVE 60 // MQTT协议层keepalive设为60秒 // 在main loop中同步调用 void main_loop(void) { static u32_t last_mqtt_tmr = 0; u32_t now = HAL_GetTick(); if (now - last_mqtt_tmr >= 1000) { last_mqtt_tmr = now; mqtt_keepalive(client); // 主动触发MQTT心跳 tcp_tmr(); // 同步调用LwIP TCP定时器 } }

注意:TCP_TMR_INTERVAL不能设得太小!STM32F407主频168MHz,tcp_tmr()每次执行约80μs,若设为250ms,每秒调用4次;设为1000ms后,每秒仅1次,CPU负载下降75%,且避免了定时器中断嵌套导致的sys_now()时间戳错乱。

3. MQTT客户端状态机的“外科手术”:剥离阻塞逻辑,植入事件驱动内核

标准LwIP MQTT示例(如mqtt_example.c)采用阻塞式设计:mqtt_connect()内部死循环等待MQTT_CONNECT_ACCEPTED,这在裸机环境下等于放弃所有其他任务。真正的工业网关必须支持:

  • 网络断开时自动重连(非简单retry);
  • 多主题订阅/取消订阅的原子操作;
  • QoS1消息的本地存储与重发(断电不丢);
  • 与传感器采集线程的无锁通信。

3.1 状态机重构:从“函数调用”到“事件响应”

我把MQTT客户端拆成三个独立模块:

模块职责触发条件
Network LayerTCP连接管理、SSL握手(若启用)ETH PHY Link Up/Down中断
MQTT Core协议解析、报文序列号管理、QoS1缓存netconn_recv()收到数据
Application Bridge主题路由、JSON解析、业务回调mqtt_event()触发用户注册函数

核心改动在mqtt_incoming_publish():

// 原版:直接调用用户callback,阻塞主线程 // 新版:投递到环形缓冲区,由独立线程处理 static void mqtt_incoming_publish(struct mqtt_client *client, const char *topic, u32_t topic_len, const void *payload, u32_t payload_len) { mqtt_msg_t msg; msg.topic = malloc(topic_len + 1); memcpy(msg.topic, topic, topic_len); msg.topic[topic_len] = '\0'; msg.payload = malloc(payload_len); memcpy(msg.payload, payload, payload_len); msg.payload_len = payload_len; // 投递到ring buffer,非阻塞 if (rb_write(&mqtt_rx_ring, &msg, sizeof(msg)) == RB_OK) { osSemaphoreRelease(mqtt_rx_sem); // 通知处理线程 } } // 独立线程处理 void mqtt_rx_thread(void const * argument) { mqtt_msg_t msg; while(1) { osSemaphoreAcquire(mqtt_rx_sem, osWaitForever); while(rb_read(&mqtt_rx_ring, &msg, sizeof(msg)) == RB_OK) { // JSON解析、数据库写入、LED状态更新... process_sensor_data(msg.topic, msg.payload, msg.payload_len); free(msg.topic); free(msg.payload); } } }

3.2 QoS1消息的“断电保险”:SPI Flash作为本地消息队列

QoS1要求消息至少送达一次,但STM32F407掉电后RAM清零。我的方案是用W25Q32JV(4MB SPI Flash)模拟轻量级消息队列:

  • 每条QoS1消息存为固定格式:[4B len][1B qos][1B topic_id][topic_str][payload];
  • 使用wear-leveling算法,避免Flash扇区过早失效;
  • 重连成功后,扫描Flash中未ACK的消息,按sequence_id重发。

关键代码片段:

// Flash写入(带CRC校验) typedef struct { u32_t len; // 总长度 u8_t qos; // 必须为1 u8_t topic_id; // 主题哈希ID,避免重复存储相同topic u8_t data[]; // topic + payload } flash_msg_t; err_t flash_store_qos1(const char *topic, const void *payload, u16_t len) { flash_msg_t hdr = { .len = sizeof(flash_msg_t) + strlen(topic) + 1 + len, .qos = 1, .topic_id = topic_hash(topic) }; u8_t buf[256]; memcpy(buf, &hdr, sizeof(hdr)); memcpy(buf + sizeof(hdr), topic, strlen(topic)+1); memcpy(buf + sizeof(hdr) + strlen(topic)+1, payload, len); // 计算CRC32并追加 u32_t crc = crc32(buf, hdr.len); memcpy(buf + hdr.len, &crc, 4); return w25qxx_write_page(buf, current_page, 0, hdr.len + 4); } // 重连后扫描 void mqtt_resend_from_flash(void) { for (u16_t page = 0; page < FLASH_PAGES; page++) { u8_t hdr_buf[16]; w25qxx_read_page(hdr_buf, page, 0, 16); flash_msg_t *hdr = (flash_msg_t*)hdr_buf; if (hdr->qos == 1 && verify_crc(hdr)) { // 构造PUBLISH报文,设置DUP=1 mqtt_publish_qos1(client, hdr->topic_id, hdr_buf + sizeof(flash_msg_t), hdr->len - sizeof(flash_msg_t)); } } }

实测:在1000次断电测试中,QoS1消息重发成功率100%,Flash寿命预估>10年(按每天100次写入计算)。

3.3 PA8 VBUS检测:让MQTT重连逻辑拥有“物理世界感知力”

PA8引脚在STM32F407上通常复用为USB OTG的VBUS检测。很多人忽略这点,但它是解决“假连接”的关键:

  • 当PHY芯片(DP83848)供电异常时,LwIP可能仍报告NETIF_LINK_UP,但实际无法收发;
  • 此时MQTT客户端不断重连失败,却不知物理层已瘫痪。

我利用PA8做硬件级链路确认:

// 初始化PA8为输入浮空 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 在MQTT重连逻辑中加入VBUS检查 err_t mqtt_reconnect(struct mqtt_client *client) { // 先检查物理层 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_8) == GPIO_PIN_RESET) { // VBUS为低 → 网线未插或PHY供电故障 LOG_WARN("PHY power loss detected on PA8"); return ERR_IF; } // 再检查LwIP链路状态 if (netif_is_up(&gnetif) && netif_is_link_up(&gnetif)) { return mqtt_connect(client, ...); } return ERR_CONN; }

这个设计让设备在网线被拔掉时,0.5秒内停止无意义重连,进入低功耗模式,而非持续消耗CPU和网络资源。

4. Keil AC6编译器下的“隐性杀手”:结构体对齐、浮点运算与中断优先级实战避坑

Keil MDK-ARM 5.34默认使用AC6编译器(ARM Compiler 6),它与旧版AC5在ABI规则上有本质差异。很多MQTT项目在AC5下正常,迁移到AC6后出现:

  • mqtt_connect()返回ERR_ARG(参数错误);
  • pbuf_copy()后payload数据错位;
  • FreeRTOS任务切换时MQTT状态机崩溃。

4.1 结构体对齐:AC6的__packed不再是万能钥匙

AC6严格遵循AAPCS ABI,struct默认按自然对齐(如u32_t按4字节对齐)。而MQTT协议要求字段紧凑排列,例如:

// MQTT CONNECT报文固定头(2字节) // [0] 0x10 | [1] Remaining Length (encoded) // 旧版AC5:__packed struct可强制1字节对齐 // AC6:__packed仅影响成员对齐,不改变整体结构大小 struct __packed mqtt_fixed_header { u8_t type_flags; // 0x10 u8_t remaining_len; // 可变长编码 };

问题在于:AC6编译后sizeof(mqtt_fixed_header)仍为4字节(因编译器插入2字节padding),导致pbuf_copy_partial()读取长度错误。
正确解法:用__attribute__((packed))替代__packed,并显式指定对齐:

struct mqtt_fixed_header { u8_t type_flags; u8_t remaining_len; } __attribute__((packed, aligned(1))); // 强制1字节对齐,sizeof=2

4.2 FPU开启与浮点运算的“双刃剑”

STM32F407的FPU(Floating Point Unit)开启后,printf("%f", x)等浮点操作速度提升10倍,但MQTT协议栈中绝不允许使用浮点数:

  • MQTT报文长度、QoS等级、消息ID全是整数;
  • FPU寄存器保存/恢复增加中断延迟,ETH_IRQHandler响应时间从1.2μs升至3.8μs;
  • AC6编译器在-O2优化下,可能将整数运算误判为浮点路径。

我的编译选项:

--cpu=Cortex-M4.fp --fpu=none --apcs=interwork // 关闭FPU,强制软浮点(虽慢但确定) // 在lwipopts.h中禁用所有浮点相关宏 #undef LWIP_HAVE_INTTYPES_H #undef LWIP_HAVE_STDINT_H #define LWIP_NO_FPU 1

经验:曾因开启FPU导致ethernetif_input()中pbuf_alloc()失败率上升17%,定位发现是sys_now()返回的毫秒计数器被FPU上下文污染。

4.3 中断优先级的“黄金分割点”

STM32F407的NVIC有16级抢占优先级(4bit),必须按实时性分级:

中断源抢占优先级理由
ETH_IRQn0最高,确保以太网帧零丢失
EXTI9_5_IRQn1PA8 VBUS检测,需快速响应物理断连
TIM2_IRQn3MQTT心跳定时器,精度要求±100ms
USART1_IRQn5调试串口,低优先级避免干扰网络

关键配置:

// 在HAL_ETH_MspInit()中设置 HAL_NVIC_SetPriority(ETH_IRQn, 0, 0); // 抢占0,子优先级0 HAL_NVIC_EnableIRQ(ETH_IRQn); // 在MQTT初始化前设置 HAL_NVIC_SetPriority(TIM2_IRQn, 3, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn);

实测数据:当ETH_IRQn优先级设为1时,网络突发流量下mqtt_publish()调用延迟抖动达±45ms;设为0后,稳定在±8ms以内,满足工业现场实时性要求。

5. 从“连得上”到“靠得住”:真实产线验证的12项硬指标清单

项目交付不是“能ping通”,而是经得起产线7×24小时拷打。我整理了STM32F407 MQTT客户端必须通过的12项验证指标,每项都附实测方法:

序号指标测试方法合格标准实测数据
1首次连接耗时上电后记录mqtt_connect()返回时间≤3.5s2.1s(DP83848冷启动)
2断网重连时间拔掉网线10s后恢复,记录重连完成时刻≤8s5.3s(含PA8检测+TCP重试)
3QoS1消息投递率发送1000条QoS1,服务端统计接收数≥99.9%99.98%(阿里云IoT平台)
4内存泄漏连续运行72小时,监控mem_free()变化波动≤0.5KB+0.2KB(LwIP内存碎片)
5CPU占用率FreeRTOSuxTaskGetSystemState()统计≤18%12.3%(Idle任务72.1%)
6温度稳定性-20℃~70℃环境箱内运行,监测HAL_ETH_GetLinkStatus()无链路闪断0次(DP83848工业级PHY)
7电磁兼容3V/m 80MHz-1GHz辐射抗扰度测试MQTT连接不中断通过(EN61000-4-3 Class A)
8断电恢复模拟电源跌落(<100ms),检查Flash消息队列未ACK消息100%重发100%(W25Q32JV)
9多主题并发同时订阅5个主题,各发送100条消息无消息丢失/错乱0错误(主题路由表查表O(1))
10TLS握手耗时启用mbedtls,连接TLS MQTT服务器≤6.2s5.8s(RSA-2048证书)
11低功耗待机网络断开后进入Stop模式,PA8唤醒待机电流≤15μA12.7μA(RTC+PA8中断)
12OTA升级兼容通过MQTT接收固件包,校验后跳转升级后MQTT自动重连100%成功(CRC32+SHA256双校验)

其中第6项(温度稳定性)最容易被忽视。DP83848在-40℃下PHY寄存器读取会超时,必须在ethernetif_update_config()中加入:

// 低温补偿:增加PHY读写超时 #define PHY_READ_TIMEOUT 0x000FFFFF // 从0x0000FFFF改为更大值 #define PHY_WRITE_TIMEOUT 0x000FFFFF

否则在北方冬季户外设备中,-25℃以下会出现“Link Up但无法通信”的诡异现象。

6. 工业现场的“最后一公里”:DP83848硬件设计与PCB布线铁律

再完美的软件,遇上糟糕的硬件设计也会崩盘。STM32F407+DP83848组合的常见翻车点,90%源于PCB:

6.1 PHY芯片供电的“隐形地雷”

DP83848需要三组独立电源:

  • AVDD(模拟电源):必须用LDO单独供电(如AMS1117-2.5V),纹波<10mV;
  • DVDD(数字电源):可与STM32共用3.3V,但需加磁珠隔离;
  • VDDIO(I/O电源):与STM32的VDD_IO一致(3.3V),严禁接AVDD。

错误设计:用同一颗LDO给AVDD/DVDD供电 → AVDD噪声耦合到DVDD → PHY寄存器读写失败率飙升。
正确做法:

STM32 VDD → AMS1117-3.3V → DVDD + VDDIO AMS1117-2.5V(低噪声)→ AVDD AVDD与DVDD之间加10μF钽电容 + 100nF陶瓷电容

6.2 RMII接口的“信号完整性生死线”

DP83848用RMII接口与STM32连接,8根信号线(REF_CLK、TX_EN、TXD[1:0]、RXD[1:0]、CRS_DV)必须:

  • 等长控制:最长与最短线长差≤50mil(≈1.27mm);
  • 阻抗匹配:走线阻抗50Ω,参考平面完整;
  • 远离干扰源:距离晶振、DC-DC电源≥10mm。

我曾遇到一个案例:TXD0/TXD1长度差120mil,导致在100Mbps满负荷时,ETH_IRQHandler频繁触发ETH_DMA_ERROR_IT,抓包显示大量FCS错误帧。修正后,误码率从10⁻³降至10⁻⁹。

6.3 变压器与RJ45的“接地艺术”

网络变压器(如Pulse HX1188)的中心抽头接地方式决定EMC性能:

  • 错误:直接接数字地(DGND)→ 高频噪声窜入PHY;
  • 正确:通过100nF电容接模拟地(AGND),并在RJ45外壳接大地(PE)。

PCB布局铁律:

  1. PHY芯片、网络变压器、RJ45接口必须放在PCB同一侧;
  2. 变压器下方禁止铺铜,保持介质厚度一致;
  3. RJ45金属外壳通过单点连接到机壳大地,绝不接PCB数字地。

实测对比:按错误方式设计,设备在变频器旁工作时,MQTT连接每3分钟断开一次;按正确方式,通过IEC61000-4-4 EFT测试(4kV),连接稳定无中断。

最后分享一个真实教训:某款温控网关量产500台后,客户反馈“凌晨3点自动重启”。排查发现是DP83848的PHY_REG_BMCR寄存器在低温下被意外写入0x0000(全功能关闭),根源在于RESET信号线上未加100nF去耦电容,电网波动导致PHY复位异常。加装电容后,问题彻底消失。
嵌入式开发没有银弹,只有把每一个电阻、每一行寄存器配置、每一个编译选项都当作生死攸关的决策——这才是STM32F407跑MQTT的真相。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 3:30:04

基于Python的招聘数据分析可视化系统设计与实现

做招聘数据分析这个项目&#xff0c;不是因为缺一个课设题目&#xff0c;而是因为招聘数据本身太适合练手了。它不像股票数据那样需要实时接口&#xff0c;也不像电商数据那样涉及复杂的用户行为埋点&#xff0c;一份爬虫抓下来的岗位信息表&#xff0c;字段足够多、脏数据足够…

作者头像 李华
网站建设 2026/10/5 3:28:53

从选型到切换:磐维数据库双中心流复制容灾集群搭建全记录

今年年初我们数据库团队接了一个硬任务&#xff1a;把跑在单机房的磐维数据库&#xff0c;改成一套双中心容灾的流复制集群。当时方案选型、参数调优、切换演练加在一起差不多干了一个月&#xff0c;中间踩了不少坑。这篇文章我把整套搭建过程从头到尾理一遍——为什么选流复制…

作者头像 李华
网站建设 2026/10/5 3:28:37

Oracle联表查询与集合运算:JOIN语法、踩坑与性能优化

今天想聊清楚Oracle联表查询这件事。不管你做业务开发、写报表还是做数据分析&#xff0c;只要跟Oracle打交道&#xff0c;早晚会遇到一张表装不下所有信息的情况&#xff1a;订单在A表、客户在B表、明细在C表&#xff0c;想一次性把关联数据都取出来&#xff0c;就得靠联表查询…

作者头像 李华
网站建设 2026/10/5 3:28:36

Oracle联表查询与集合操作:JOIN、UNION、MINUS的实战避坑指南

1. 联表查询与集合操作&#xff1a;一对常被混淆的兄弟做Oracle开发的人&#xff0c;基本都会遇到一个场景&#xff1a;报表数据对不上。明明逻辑看着没问题&#xff0c;Left Join也加了&#xff0c;条件也写了&#xff0c;结果不是多出来几行&#xff0c;就是死活少几条数据。…

作者头像 李华
网站建设 2026/10/5 3:26:58

插件系统加载失败排查指南:从plugin.json到激活机制

1. 从“plugins”这个标题说起&#xff1a;一个被低估的工程话题“plugins”这个词看起来平平无奇&#xff0c;但如果你最近在折腾 Cursor、Codex CLI、ZCode CLI 这类工具&#xff0c;或者被failed to load plugins web boot: 2 entries did not activate这类报错卡住过&#…

作者头像 李华
网站建设 2026/10/5 3:26:51

函数到底是什么?从基础概念到工程实战一次讲透

函数这个词&#xff0c;我在写代码、处理数据、做算法调优的这几年里几乎天天碰。它不是什么高深的概念&#xff0c;但你拆开任何一个软件项目、一条SQL查询、甚至一张Excel表格&#xff0c;底层全是函数在撑场面。最近逛技术社区&#xff0c;跟函数相关的帖子特别密集&#xf…

作者头像 李华