news 2026/10/26 10:35:21

VxWorks下GPIO模拟MDIO时序驱动PHY寄存器读写实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VxWorks下GPIO模拟MDIO时序驱动PHY寄存器读写实战

简介:面向VxWorks嵌入式开发者的GPIO模拟MDC/MDIO驱动代码,用于在缺乏专用SMI硬件接口的平台上,通过通用GPIO引脚完成对以太网PHY芯片的配置与寄存器读写。资源聚焦MDC时钟生成、MDIO双向数据收发、命令与地址帧时序控制及错误检测等关键环节,适合已具备一定VxWorks驱动基础、需要打通PHY管理通道的工程师参考。压缩包内共1个C源文件,即驱动实现代码,整体约2KB,体量精简,便于直接嵌入BSP工程中对照移植。目前已有1561人学习下载,说明该实践场景在嵌入式网络开发中具有普遍需求。代码围绕GPIO方向配置、延时函数控制时钟周期、上升沿写与下降沿读的时序处理展开,并给出读写PHY寄存器的函数示例,读者可据此理解软件模拟SMI协议的完整流程,快速排查时序偏差与数据异常问题,为无专用MAC管理接口的板卡提供可复用的实现思路。

1. 用两根普通 GPIO 把 MDIO 时序“掰”出来

手上有一块板子,PHY 芯片的 MDC/MDIO 引脚没接到 SoC 的专用 MAC 控制器上,而是随手连到了两个普通 GPIO。硬件同事说“反正就两根线,你软件模拟一下”。于是问题来了:VxWorks 下没有现成的 SMI 控制器可用,寄存器读写全靠自己按时序翻转电平。这份gpio模拟的mdc mdio驱动代码.rar解决的正是这个场景——用软件位操作(bit-banging)实现 MDC 时钟与 MDIO 双向数据线,完成对 PHY 寄存器的读写,进而做链路状态查询、自协商配置、PHY 初始化。适合正在 VxWorks 上做网络底层驱动、手里有“非标准接线”板子的开发者,也适合想搞懂 SMI 时序到底怎么落地的人。

2. 先看清这套驱动的骨架:文件、接口与时序模型

拿到压缩包,别急着编译。先把里面几个文件的关系理顺,否则后面改一个宏,整个时序就崩了。这类 GPIO 模拟 SMI 的驱动,结构通常很朴素:一个头文件放寄存器地址宏和函数声明,一个 C 文件放底层电平操作和帧收发,可能再加一个测试/初始化入口。下面按“这个项目场景下合格开发最可能采用的组织方式”来讲,具体文件名以包内为准,逻辑是对应的。

2.1 目录与文件职责划分

典型的文件构成如下表,读代码前先建立这张地图:

文件职责改动频率
mdcMdioDrv.hPHY 寄存器地址宏、函数原型、GPIO 引脚宏高(换板必改)
mdcMdioDrv.cMDC 时钟产生、MDIO 读写位、完整帧收发低
phyInit.c上电初始化、自协商、链路检测调用示例中
Makefile/vxworks配置片段编译规则、驱动挂载低

头文件里最关键的是引脚定义和延时参数。因为 MDC 是纯输出时钟,MDIO 是双向线,所以引脚配置要区分方向:

/* mdcMdioDrv.h —— 换板子基本只动这一段 */ #define MDC_GPIO_PIN 12 /* MDC 时钟,纯输出 */ #define MDIO_GPIO_PIN 13 /* MDIO 数据,双向 */ /* PHY 地址与寄存器偏移,按实际硬件填 */ #define PHY_ADDR 0x01 #define PHY_REG_BMCR 0x00 /* 基本模式控制寄存器 */ #define PHY_REG_BMSR 0x01 /* 基本模式状态寄存器 */ #define PHY_REG_PHYID1 0x02 #define PHY_REG_PHYID2 0x03 /* 半周期延时,单位 us;MDC 上限一般 2.5MHz,留余量 */ #define MDC_HALF_DELAY_US 1 extern void mdcMdioInit(void); extern STATUS phyRegWrite(UINT16 phyAddr, UINT8 regAddr, UINT16 data); extern STATUS phyRegRead (UINT16 phyAddr, UINT8 regAddr, UINT16 *pData);

这里MDC_HALF_DELAY_US是整个驱动的“心跳”。IEEE 802.3 规定 MDC 最高 2.5MHz,也就是半周期最小 200ns。但 GPIO 翻转本身有开销,加上 VxWorks 下sysUsDelay的精度,实际取值要实测。我一般先给 1us 跑通,再用示波器看波形往小调。PHY_ADDR尤其容易翻车——很多板子 PHY 地址由 strapping 电阻决定,不是想当然的 0。

2.2 SMI 帧格式:32 位里每一位都有讲究

MDIO 读写不是随便发几个脉冲,它有一套严格的帧结构。一次完整的 SMI 帧是 32 个 MDC 周期,前 32 位是前导码(preamble),后面才是操作码、PHY 地址、寄存器地址和数据。写操作和读操作的帧格式不同,读操作还要在“turnaround”阶段把 MDIO 方向从输出切成输入。这是整个驱动最容易写错的地方,先把帧拆开看:

读帧: Preamble(32) | ST(01) | OP(10) | PHYAD(5) | REGAD(5) | TA(Z0) | DATA(16) 写帧: Preamble(32) | ST(01) | OP(01) | PHYAD(5) | REGAD(5) | TA(10) | DATA(16)
  • Preamble:32 个连续的 1,给 PHY 同步用。
  • ST(Start of Frame):固定01。
  • OP(Operation Code):读是10,写是01。
  • PHYAD:5 位 PHY 地址,0~31。
  • REGAD:5 位寄存器地址,0~31。
  • TA(Turnaround):读操作时主机先释放总线(高阻),PHY 拉低一个周期再给数据;写操作时主机发10。
  • DATA:16 位数据,高位在前。

理解这张表,后面看代码就不会迷路。很多新手直接把“读”写成“发完地址就读数据”,漏掉 TA 阶段的方向切换,结果读回来永远是0xFFFF——这是血泪经验里最常见的一条。

2.3 底层电平操作与方向切换

所有上层帧逻辑,最终都落到两个动作:拉高/拉低 MDC,以及设置/读取 MDIO。因为 MDIO 双向,必须能动态切方向。VxWorks 下 GPIO 方向控制一般通过芯片的 GPIO 控制器寄存器或 BSP 提供的封装函数完成,这里用抽象函数表示,实际替换成你板子的gpioSetDir/gpioSetValue即可:

/* 底层电平操作,实际项目替换为 BSP 的 GPIO 接口 */ static void mdcSet(int level) { gpioSetValue(MDC_GPIO_PIN, level ? 1 : 0); } static void mdioSetDir(int isOutput) { /* 1=输出,0=输入;读 PHY 时切输入 */ gpioSetDir(MDIO_GPIO_PIN, isOutput); } static void mdioWriteBit(int bit) { gpioSetValue(MDIO_GPIO_PIN, bit ? 1 : 0); } static int mdioReadBit(void) { return gpioGetValue(MDIO_GPIO_PIN) ? 1 : 0; } /* 一个 MDC 周期:先低后高,数据在上升沿被采样 */ static void mdcClockPulse(void) { mdcSet(0); sysUsDelay(MDC_HALF_DELAY_US); mdcSet(1); sysUsDelay(MDC_HALF_DELAY_US); }

逻辑说明:SMI 协议规定数据在 MDC 上升沿被采样,所以每个 bit 的流程是“先摆好 MDIO 电平 → MDC 拉低 → 延时 → MDC 拉高 → 延时”。mdcClockPulse把“拉低-拉高”封装成一个周期,调用方只需在调用前把 MDIO 摆到目标位。参数上,MDC_HALF_DELAY_US决定时钟频率,mdioSetDir的入参决定当前是主机驱动还是 PHY 驱动总线。注意:切方向前一定要保证 MDC 处于低电平,否则可能在切换瞬间产生毛刺,被 PHY 误采样。

2.4 写寄存器:从帧到位的完整落地

有了底层动作,写寄存器就是把 32 位帧逐位“喂”出去。写帧的 TA 是10,数据段是主机输出,全程 MDIO 保持输出方向:

STATUS phyRegWrite(UINT16 phyAddr, UINT8 regAddr, UINT16 data) { int i; UINT32 frame; mdioSetDir(1); /* 全程输出 */ /* 32 位前导码,全 1 */ for (i = 0; i < 32; i++) { mdioWriteBit(1); mdcClockPulse(); } /* 拼 32 位有效帧:ST=01, OP=01(写), PHYAD, REGAD, TA=10, DATA */ frame = (0x1u << 30); /* ST: 01 */ frame |= (0x1u << 28); /* OP: 01 写 */ frame |= ((UINT32)(phyAddr & 0x1F) << 23); /* PHYAD 5 位 */ frame |= ((UINT32)(regAddr & 0x1F) << 18); /* REGAD 5 位 */ frame |= (0x2u << 16); /* TA: 10 */ frame |= (UINT32)data; /* DATA 16 位 */ for (i = 31; i >= 0; i--) { mdioWriteBit((frame >> i) & 0x1); mdcClockPulse(); } return OK; }

逻辑说明:frame的位布局严格对应 2.2 的写帧格式。ST占 bit30~31,OP占 bit28~29,PHYAD左移 23 位占 bit23~27,REGAD左移 18 位占 bit18~22,TA的10放在 bit16~17,低 16 位是数据。循环从 bit31 往 bit0 发,保证高位在前。参数上,phyAddr和regAddr都做了& 0x1F掩码,防止越界污染相邻字段——这个掩码别省,我见过有人传了0x20的寄存器地址,结果把 OP 位改掉,PHY 直接不响应。

2.5 读寄存器:TA 阶段的方向切换是命门

读操作比写多一个坑:TA 阶段主机要先释放总线,PHY 拉低一个周期表示“我收到了”,然后主机切输入,读 16 位数据。方向切换的时机差一个周期,读出来就是垃圾:

STATUS phyRegRead(UINT16 phyAddr, UINT8 regAddr, UINT16 *pData) { int i; UINT32 frame; UINT16 value = 0; if (pData == NULL) return ERROR; mdioSetDir(1); /* 先输出,发前导码和地址 */ for (i = 0; i < 32; i++) { mdioWriteBit(1); mdcClockPulse(); } /* ST=01, OP=10(读), PHYAD, REGAD */ frame = (0x1u << 30); frame |= (0x2u << 28); /* OP: 10 读 */ frame |= ((UINT32)(phyAddr & 0x1F) << 23); frame |= ((UINT32)(regAddr & 0x1F) << 18); for (i = 31; i >= 18; i--) { /* 只发到 REGAD 结束 */ mdioWriteBit((frame >> i) & 0x1); mdcClockPulse(); } /* TA 第一拍:主机释放总线,PHY 拉低 */ mdioSetDir(0); mdcClockPulse(); /* TA 第二拍:PHY 释放,主机准备读 */ mdcClockPulse(); /* 读 16 位数据,高位在前 */ for (i = 0; i < 16; i++) { value <<= 1; value |= (UINT16)mdioReadBit(); mdcClockPulse(); } mdioSetDir(1); /* 读完恢复输出,避免总线悬空 */ *pData = value; return OK; }

逻辑说明:发完REGAD(bit18)后停止输出,进入 TA。第一个mdcClockPulse期间主机已切输入,PHY 拉低;第二个脉冲 PHY 释放总线。之后 16 个周期逐位读,value <<= 1再或上新位,保证高位在前。读完立刻把方向切回输出,这是个好习惯——总线悬空在高阻态容易受干扰,下次通信前状态不确定。参数pData做了空指针检查,返回ERROR而不是让系统挂掉,VxWorks 下这种防御很必要。

3. 在 VxWorks 里把它跑起来:初始化、编译与验证

代码看懂只是第一步,真正让它在板子上跑通,还有初始化顺序、编译挂载、波形验证几道关。这一章按“上电到读到正确 PHY ID”的顺序走一遍,每一步都给出可操作的命令和判断依据。

3.1 初始化顺序:GPIO 先于一切

mdcMdioInit必须在任何 PHY 读写之前调用,而且顺序不能乱:先配 MDC 为输出并拉高(空闲态),再配 MDIO 为输出并拉高。如果 MDIO 初始方向是输入,前导码根本发不出去:

void mdcMdioInit(void) { /* MDC 空闲态为高,先拉高再配方向,避免上电毛刺 */ gpioSetValue(MDC_GPIO_PIN, 1); gpioSetDir(MDC_GPIO_PIN, 1); /* MDIO 默认输出高(空闲态) */ gpioSetValue(MDIO_GPIO_PIN, 1); gpioSetDir(MDIO_GPIO_PIN, 1); sysUsDelay(10); /* 等 PHY 上电稳定 */ }

逻辑说明:SMI 总线空闲时 MDC 和 MDIO 都应为高。先写值再配方向,是为了避免方向切换瞬间输出一个不确定电平。sysUsDelay(10)给 PHY 内部电路一点稳定时间,具体值看 PHY 手册的上电时序要求,常见是几毫秒,这里 10us 只是保守起步,实际初始化前往往还要等 PHY 复位完成。

3.2 编译与挂载到 VxWorks 镜像

VxWorks 下加驱动,通常是把.c加进Makefile的MACH_OBJS或对应 BSP 的源文件列表,然后重编镜像。如果是可加载模块( downloadable kernel module),也可以单独编译成.o再ld加载:

# 以 BSP 编译为例,把驱动加进对象列表后重编 cd $(WIND_BASE)/target/config/<bspName> # 编辑 Makefile,在 MACH_OBJS 中加入 mdcMdioDrv.o make clean make

逻辑说明:MACH_OBJS是 VxWorks BSP 编译时链接进内核的对象列表,把驱动.o加进去,镜像启动后函数就在符号表里。如果走可加载模块路线,编译命令类似ccppc -c -o mdcMdioDrv.o mdcMdioDrv.c,再在目标机 shell 里ld < mdcMdioDrv.o。参数上,交叉编译器前缀(ccppc)要和你 BSP 的TOOL变量一致,别拿 x86 的编译器去编 PowerPC 的板子。

3.3 用 PHY ID 寄存器做第一次自检

驱动跑起来后,第一个验证动作不是配自协商,而是读 PHY ID 寄存器(PHYID1/PHYID2,地址0x02/0x03)。这两个寄存器是只读的、值固定,读对了说明时序、地址、方向切换全对:

void phySelfTest(void) { UINT16 id1 = 0, id2 = 0; mdcMdioInit(); if (phyRegRead(PHY_ADDR, PHY_REG_PHYID1, &id1) != OK || phyRegRead(PHY_ADDR, PHY_REG_PHYID2, &id2) != OK) { printf("PHY read failed\n"); return; } printf("PHY ID: 0x%04X%04X\n", id1, id2); /* 对照 PHY 手册的 OUI 和型号位,确认读到的不是 0xFFFF/0x0000 */ }

逻辑说明:读回0xFFFF通常是 MDIO 没切输入或 PHY 没响应;读回0x0000多半是总线被拉死或地址错。正常应读到厂商 OUI 加型号,比如某 PHY 的 ID 高 16 位是 OUI 的一部分。这一步过了,再去做BMCR自协商使能、BMSR链路状态轮询才有意义。参数上,PHY_ADDR如果读不到,拿示波器看 MDIO 上有没有 PHY 的 ACK 拉低,比盲改代码快得多。

3.4 自协商与链路检测的调用节奏

PHY 初始化一般流程是:软复位 → 使能自协商 → 等自协商完成 → 读链路状态。这里要注意BMCR的 bit15 是软复位,写 1 后要等它自动清零;BMSR的 bit2 是链路状态,但它有“锁存”特性,读一次会清掉变化位,所以轮询要用两次读或读BMSR后再读一次确认:

STATUS phyAutoNegStart(void) { UINT16 bmcr = 0; int retry; /* 软复位 */ phyRegWrite(PHY_ADDR, PHY_REG_BMCR, 0x8000); for (retry = 0; retry < 100; retry++) { sysUsDelay(1000); phyRegRead(PHY_ADDR, PHY_REG_BMCR, &bmcr); if ((bmcr & 0x8000) == 0) break; /* 复位完成 */ } if (retry >= 100) return ERROR; /* 使能自协商 + 重启自协商 */ phyRegWrite(PHY_ADDR, PHY_REG_BMCR, 0x1200); return OK; }

逻辑说明:0x8000是软复位位,写完轮询直到它清零,最多等 100ms。0x1200是 bit12(自协商使能)加 bit9(重启自协商)。参数上,轮询次数和延时按 PHY 手册的复位时间调整,有的 PHY 要几百毫秒。链路检测则周期性读BMSR,注意锁存特性,别拿单次读的结果当稳定状态。

4. 避坑与排查:那些让波形“看起来对、读出来错”的细节

这套驱动代码量不大,但坑很集中。下面几条都是实际调试中反复出现的,按“现象 → 原因 → 解决”记下来,能省不少示波器前的时间。

4.1 读回恒为 0xFFFF

现象:phyRegRead任何寄存器都返回0xFFFF,写操作似乎也没报错。原因:TA 阶段方向切换时机不对,主机没真正释放总线,或者mdioSetDir(0)之后没有先让 MDC 走一个完整周期就去读。解决:严格按 2.5 的顺序,切输入后先走两个mdcClockPulse(TA 两拍),再进数据循环;用示波器看 MDIO 在 TA 第一拍是否被 PHY 拉低,没有就是 PHY 地址错或 PHY 没上电。

4.2 偶发读写失败,重试就好

现象:大部分时候正常,偶尔一次读写出错,重试又好了。原因:MDC_HALF_DELAY_US太小,GPIO 翻转和sysUsDelay的实际开销叠加后,MDC 高电平时间不够,PHY 采样不稳。解决:先把半周期延时加到 2~3us 跑稳,再逐步往下压;同时确认sysUsDelay在你的 BSP 上是忙等还是基于 tick,基于 tick 的延时精度可能只有毫秒级,那就得改用更细的延时接口。

4.3 换块板子就读不到 PHY

现象:同一份代码,A 板正常,B 板读不到。原因:PHY_ADDR由硬件 strapping 决定,两块板子可能不同;或者 B 板的 MDIO 上拉了但 MDC 没上拉。解决:先扫 0~31 所有 PHY 地址,看哪个能读回非0xFFFF的 ID;再查原理图确认上拉电阻。这个扫描函数很值得写,十行代码省半天。

4.4 多任务并发访问总线冲突

现象:网络任务和调试命令同时读 PHY,数据错乱。原因:SMI 帧是 32 位连续时序,中间被别的任务插入 GPIO 操作就断了。解决:给phyRegRead/phyRegWrite加互斥信号量(semMCreate+semTake/semGive),把整个帧收发包在临界区里。VxWorks 下这是常规操作,别省。

4.5 软复位后立刻读写失败

现象:写完BMCR软复位,马上读BMSR返回异常。原因:PHY 复位期间不响应 SMI,或者复位时间没等够。解决:复位后轮询BMCRbit15 直到清零,再加一点余量;不要在复位窗口内做任何其他寄存器操作。

5. 二次开发:把这份驱动改成你自己的 PHY 管理模块

代码跑通只是起点,真正落地往往要把它嵌进更大的网络驱动框架里。这一章讲几个改造方向和一个验证习惯,都是我在实际项目里踩过之后固定下来的做法。

第一个改造点是抽象 GPIO 层。现在mdcSet/mdioWriteBit直接调 BSP 函数,换芯片就得改驱动。更好的做法是定义一个函数指针结构体,初始化时传入具体实现,驱动本体只依赖接口。这样同一份 SMI 逻辑能复用到不同 SoC 上,测试时还能塞一个模拟实现做单元测试。

第二个是把 PHY 操作封装成对象。单 PHY 场景直接传PHY_ADDR没问题,多 PHY 交换机场景就要维护 PHY 列表、每个 PHY 的链路状态和自协商结果。常见做法是定义一个PHY_DEV结构体,把地址、当前速率、双工模式、链路状态打包,再配一组phyDevRead/phyDevWrite包装函数。改造后调用方不再关心底层是 GPIO 模拟还是硬件 SMI 控制器。

第三个是加一层寄存器访问缓存与超时。PHY 寄存器读写在链路抖动时可能失败,裸调用会让上层逻辑卡死。我一般给读写加超时重试(比如 3 次),并对BMSR这类状态寄存器做短周期缓存,避免高频轮询把总线占满。下面是一个改造后的调用示例:

typedef struct { UINT16 phyAddr; UINT16 lastBmsr; UINT32 lastPollTick; int linkUp; } PHY_DEV; STATUS phyDevPollLink(PHY_DEV *dev) { UINT16 bmsr = 0; int retry; for (retry = 0; retry < 3; retry++) { if (phyRegRead(dev->phyAddr, PHY_REG_BMSR, &bmsr) == OK) break; sysUsDelay(100); } if (retry >= 3) return ERROR; /* BMSR bit2 链路状态,锁存特性:读两次确认 */ dev->linkUp = (bmsr & 0x0004) ? 1 : 0; dev->lastBmsr = bmsr; return OK; }

逻辑说明:重试 3 次、每次间隔 100us,覆盖偶发总线错误;linkUp取BMSRbit2。参数上,lastPollTick可以用来做轮询节流,比如 1 秒内不重复读。这个结构体化的改造,让驱动从“一段能跑的代码”变成“能维护的模块”。

验证方法上,我固定用三步:先读 PHY ID 确认时序,再读BMSR确认链路,最后用BMCR强制指定速率双工做回环测试。三步都过,才认为驱动可靠。表格对比一下改造前后的差异:

维度原始驱动改造后
GPIO 依赖直接调 BSP函数指针注入
多 PHY 支持单地址宏PHY_DEV 列表
错误处理无重试超时重试 + 缓存
可测试性需真实硬件可注入模拟实现

最后说个习惯。早些年我调这类时序驱动,总想着“逻辑对就行”,结果一次板子换 PHY 后读 ID 死活不对,查了两天才发现是 TA 阶段少走了一个时钟周期——代码在旧 PHY 上“恰好”能工作。从那以后我每次改完 SMI 时序,都强制先用示波器抓一帧完整波形,对照 2.2 的帧格式逐位核对,再上电跑自检。这个习惯帮我省下的时间,远比抓波形花的多。希望帮到你。

本文还有配套的精品资源,点击获取

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

扩展MFC DLL开发实战:工程结构、接口设计与状态切换避坑

简介&#xff1a;这是一份面向C与MFC初学者及需要补齐动态链接库基础的开发者的入门示例包&#xff0c;围绕MFC DLL的创建、导出与在客户端程序中调用展开&#xff0c;适合课程实验、自学练手或作为小型项目的参考模板。压缩包共61个文件&#xff0c;约4.79MB&#xff0c;包含h…

作者头像 李华
网站建设 2026/10/26 10:34:22

IM即时通讯私有化部署实战:环境准备到接口联调完整路径

IM 即时通讯私有化部署实战&#xff1a;从环境准备到接口联调的完整落地路径IM 即时通讯私有化部署这两年成了很多技术团队的刚需话题。原因很直接&#xff1a;业务方希望聊天能力长在自己的系统里&#xff0c;账号体系、组织架构、消息记录、文件存储都放在自有服务器上&#…

作者头像 李华
网站建设 2026/10/26 10:34:17

64位OpenGL+GLUT环境配置与链接排错指南

简介&#xff1a;面向计算机图形学课程实验与OpenGL入门开发的64位GLUT库包&#xff0c;主要解决64位工程缺少匹配GLUT头文件与链接库、易出现32/64位不兼容的问题。包内共4个文件&#xff0c;以h头文件、dll动态库、lib导入库及keep占位文件为主&#xff0c;压缩包约127KB&…

作者头像 李华
网站建设 2026/10/26 10:33:57

2022美赛C题F奖复现:黄金比特币日频回测与避坑

简介&#xff1a;这份2022年美赛C题F奖项目资料面向备战美赛、关注金融时间序列建模的学习者&#xff0c;围绕比特币交易策略给出论文、代码与数据的一体化参考。其核心内容包括小波降噪预处理、GRU神经网络价格预测&#xff0c;以及结合动态规划求解买卖时机的策略设计&#x…

作者头像 李华
网站建设 2026/10/26 10:33:57

线性代数 [ 矩阵乘法与线性变换复合的联系 ]

线性代数 两个矩阵相乘&#xff0c;到底把什么乘在了一起&#xff1f; 我们前面已经把矩阵和线性变换联系起来了。一个二维线性变换&#xff0c;只要知道两个标准基向量分别去了哪里&#xff0c;就能够知道整个平面怎样变化。把这两个结果依次放进矩阵的两列&#xff0c;这个…

作者头像 李华
网站建设 2026/10/26 10:33:39

Python进销存系统源码:库存流水结存双写与对账避坑

简介&#xff1a;这套Python进销存系统源码面向具备Python基础、希望完整实践Web业务系统的开发者及小型商贸企业的信息化人员&#xff0c;围绕采购入库、销售出库、库存盘点等流程组织代码&#xff0c;可用于课程设计、毕业项目或二次开发起步。实现上涉及Django、Flask等Web框…

作者头像 李华