01 定位器项目笔记——知识复习回顾
1. 嵌入式中遇到的各种周期
时钟周期: 由CPU主时钟频率决定, 也称振荡周期或节拍。一切操作的基准,可达几MHZ甚至GHZ,是最小,最基本的时间单位。
机器周期:完成一次基本硬件操作的时间, 由N个时钟周期组成, 数值相对固定。
指令周期: 取指并完成一次指令的时间,由N个机器周期组成, 数值因指令而异。
2. 上拉电阻
一个连接在电源(VCC)和信号线(或引脚)之间的的电阻。
作用:
1. 确定默认状态: 当没有主动信号驱动时(例如,按键未按下),上拉电阻确保输入引脚被稳定地拉到高电平
2. 限流保护: 主动驱动时,限制电流大小,防止形成短路,保护电路。
3. 提高噪声容限:为信号提供一个稳定的高电平参考,增强了抗电磁干扰(EMI)的能力。
强上拉:低阻值电阻(如4.7kΩ),驱动能力强,上升时间短,抗干扰好,但功耗大。
弱上拉:高阻值电阻(如50kΩ),静态功耗低,但上升时间慢,易受外界噪声或泄漏电流干扰。
3. 掩码
嵌入式中的掩码,主要是为了修改目标位,不影响其他位
例如: 修改目标位3可以与 (0x01 << 3)进行与或操作。
4. LDO & DCDC
LDO(Low-Dropout Regulator - 低压差线性稳压器): 像一个“精细的电阻”,通过消耗能量来降压,输出干净,但效率低
原理 : 利用运算放大器和滑动变阻器
DCDC (DC-DC Converter - 直流-直流变换器):像一个 “高效的能量搬运工”,通过快速开关、存储和释放能量来降压/升压/升降压,效率高,但输出有噪声。
原理:利用运算放大器 + 高频的开关实现
。应用场景与选型总结
根据上述特性,您的选择策略非常明确:
选择 LDO 当:
压差很小:例如,从3.3V降到3.0V或2.8V,此时效率很高,发热可控。
对噪声极其敏感:为模拟电路(传感器、运放、ADC/DAC的参考电压)、射频电路(RF模块)供电。洁净的电源是性能的保证。
空间和成本受限:电路板空间极其宝贵,或者对成本锱铢必较,且电流不大(< 300mA)。
简单省事:在原型设计或低功耗应用中,快速搭建一个电源轨。
选择 DCDC 当:
压差大或电流大:例如,从12V/24V降到5V/3.3V,或者负载电流超过500mA。这是DCDC的绝对主场,否则散热无法解决。
对效率要求高:电池供电设备(手机、玩具、物联网设备),目标是最大限度延长续航。
需要升压或电压反转:例如,用3.7V锂电池驱动需要5V的器件。
散热是问题:无法通过散热片解决LDO的发热时。
高级玩法:组合使用
在复杂系统中,常常组合使用二者,发挥各自长处:
12V Input -> DCDC Buck -> 5V -> LDO -> 3.3V (For MCU and Analog Sensors)
DCDC 先高效地将12V降到5V,解决了大部分压差和发热问题。
LDO 再将干净的5V转换为极其洁净的3.3V,为对噪声敏感的模拟和数字电路供电。
5. LORA
LoRa 是 Long Range 的缩写,直译为“远距离”。它是一种物理层的无线调制技术,由其母公司Semtech(升特)专有。
核心要义: LoRa技术的设计目标非常明确——用极低的功耗,实现超远的通信距离。它牺牲了数据传输速率和带宽,来换取更好的链路预算和穿透能力。
传输时只能是收或发,不能同时收发。
6.网络知识相关
一些计算机网络中的专业名词解释
一、Web 与访问相关
这类名词与我们日常上网直接相关。
1. URL (统一资源定位符)
是什么:网址。比如
https://www.example.com:443/blog/article?id=123#section2
。作用:就像一封邮件的完整收件地址,它唯一地标识了互联网上每个资源(网页、图片、视频等)的位置。
组成部分:
https://
- 协议 (Protocol)
www.example.com
- 主机名/域名 (Hostname/Domain)
:443
- 端口 (Port)(HTTPS默认端口,通常隐藏)
/blog/article
- 路径 (Path)(资源在服务器上的位置)
?id=123
- 查询参数 (Query)(传递给服务器的额外信息)
#section2
- 片段 (Fragment)(页面内的锚点跳转)2. HTTP (超文本传输协议)
是什么:一种通信规则,是Web浏览器和Web服务器之间“对话”的语言。
作用:规定了客户端如何请求数据,以及服务器如何响应数据。它是明文传输的,就像寄送明信片,路上谁都能看到内容。
特点:无状态(每次请求都是独立的,服务器不记得你)。
3. HTTPS (安全超文本传输协议)
是什么:HTTP的安全版本。
作用:在HTTP和TCP之间增加了一个安全层(SSL/TLS),对传输的数据进行加密和身份验证。就像把明信片换成了上锁的保险箱邮寄,只有拥有钥匙的收件人才能打开查看。
好处:
加密:防止数据在传输过程中被窃听(如密码、信用卡号)。
完整性:防止数据在传输过程中被篡改。
身份认证:确保你访问的就是真正的网站,而不是钓鱼网站。
4. SSL/TLS (安全套接字层/传输层安全协议)
是什么:实现HTTPS加密的具体技术,是那个“保险箱”和“锁”的制造标准。
关系:TLS是SSL的升级版,但现在通常混用。我们常说的“SSL证书”其实大多是TLS证书。
二、核心通信协议
这类名词是互联网的基石,负责数据的传输和寻址。
5. TCP (传输控制协议)
是什么:一种可靠的、面向连接的传输层协议。
作用:确保数据完整、有序地从一端传到另一端。过程如下:
三次握手建立连接(打电话时的“喂,听得到吗?”“听得到,你呢?”“我也听得到,开始说吧!”)。
传输数据,并有确认机制(收到包后回复“收到”)。
如果丢包,会重传。
四次挥手断开连接。
比喻:像打电话,需要先接通,确保对方能听到每一句话,适合重要文件、网页浏览、邮件等。
6. UDP (用户数据报协议)
是什么:一种不可靠的、无连接的传输层协议。
作用:只管把数据包发出去,不建立连接、不保证顺序、不重传。
比喻:像发短信或校园广播,发出去就不管了。速度快,延迟低,但可能丢失。
适用场景:视频通话、在线游戏、直播(丢失少量数据包比延迟等待重传更重要)。
7. IP (网际协议)
是什么:负责逻辑寻址的网络层协议。
作用:给互联网上的每一台设备分配一个唯一的IP地址(如
192.168.1.1
),并定义如何将数据包从源地址路由到目标地址。它负责的是“把包裹送到哪个城市哪个街道”。IP地址:设备的“逻辑地址”,可以变化(比如你换个Wi-Fi,IP就变了)。
8. DNS (域名系统)
是什么:互联网的“电话簿” 或 导航地图。
作用:将人类好记的域名(如
www.google.com
)翻译成机器能识别的IP地址(如142.251.42.206
)。过程:你在浏览器输入网址后,浏览器会先问DNS服务器:“
www.google.com
的地址是多少?”,拿到IP地址后,才去连接对应的服务器。9. ICMP (互联网控制消息协议)
是什么:用于网络设备之间传递控制消息的网络层协议。
作用:检查网络连通性和状态。最著名的工具就是
ping
。比喻:像声纳,发出一个信号,通过回声来判断对方是否存在以及网络延迟。
三、其他重要概念
10. Cookie
是什么:网站为了识别用户身份而存储在用户本地终端上的小型数据。
作用:解决HTTP无状态的问题。服务器给你一个“会员卡”(Cookie),你下次再来时出示这张卡,服务器就知道你是谁了。用于保持登录状态、记录用户偏好等。
11. CDN (内容分发网络)
是什么:一个分布在全球各地的服务器网络。
作用:将网站的内容(图片、视频、脚本)缓存到离用户最近的服务器上。用户访问时,直接从最近的CDN节点获取数据,而不是源站。
好处:极大加快加载速度、减轻源服务器压力、防御网络攻击。
总结与关系
这些名词是如何协同工作的?以访问一个HTTPS网站为例:
你在浏览器输入 URL (
https://www.example.com
)。浏览器询问 DNS 服务器,将域名
www.example.com
解析成 IP 地址。浏览器与服务器通过 SSL/TLS 协议进行安全握手,建立一条加密通道,开始使用 HTTPS 协议通信。
HTTPS 的请求和响应数据被拆分成多个段,通过 TCP 协议可靠地传输(三次握手、确认、重传)。
TCP 段被包装上 IP 包头,通过路由器在网络中一跳一跳地路由到目标IP地址。
在本地网络中,通过 MAC地址 进行设备间的最终投递。
服务器处理请求后,将网页数据(如HTML)返回,这个过程可能用到 CDN 来加速静态资源的传输。
服务器可能会下发一个 Cookie 到你的浏览器,以便下次访问时识别你的身份。
MAC地址 和 IP地址
特性 MAC 地址 (Media Access Control Address) IP 地址 (Internet Protocol Address) 本质 物理地址、硬件地址、烧录地址 逻辑地址、网络地址、软件地址 作用 在同一局域网内,识别唯一的网络设备 在整个互联网上,标识设备的网络位置 OSI层 数据链路层 (第2层) 网络层 (第3层) 分配方式 由设备制造商永久固化在网卡中,全球唯一 由网络管理员或DHCP服务器动态分配,可变 可变性 通常固定不变(除非虚拟MAC或伪造) 可以随时变更(换一个网络就会变) 地址格式 48位(或64位)十六进制数,用冒号分隔
例:00:1A:2B:3C:4D:5E
32位(IPv4)或128位(IPv6)二进制数,用点分十进制表示
例:192.168.1.1
(IPv4)2001:0db8::1
(IPv6)范围 本地。只能在同一个广播域(如一个路由器下的网络)内直接通信 全局。用于在不同网络之间进行路由和寻址 类比 身份证号
(唯一标识你这个人,出生地定)住址
(标识你住在哪,可以搬家改变)依赖性 不依赖IP地址也可以工作(局域网内直接用) 依赖MAC地址才能最终送达 总结:
IP地址用于宏观的、最终目标的寻址。它就像最终收货地址,决定了数据包要去的城市、街道、门牌号。
MAC地址用于微观的、下一跳的寻址。它就像每个快递站点的分拣员,只负责把包裹交给去往下一个站点的卡车司机。
缺一不可:没有IP地址,数据包无法在复杂的互联网中找到最终目的地;没有MAC地址,数据包无法在本地物理网络上传递给下一个设备。
关系:IP地址是目的,MAC地址是手段。数据包依靠IP地址跨网络旅行,但依靠MAC地址完成在每一段本地网络上的“最后一公里”交付。
计算机网络模型
OSI 七层参考模型 (理论标准) TCP/IP 四层模型 (实际实施) 五层混合模型 (教学常用) 功能概述 协议示例 数据单位 (PDU) 典型设备 7. 应用层 应用层 为用户应用程序提供网络服务接口 HTTP, HTTPS, DNS, FTP, SMTP, POP3, Telnet 报文 (Message) 6. 表示层 应用层 (包含在应用层中) 数据转换、加密解密、压缩解压缩(确保应用层数据可读) SSL/TLS, JPEG, MPEG, GIF 5. 会话层 (包含在应用层中) 建立、管理、终止应用程序之间的会话 RPC, NetBIOS 4. 传输层 传输层 传输层 提供端到端的可靠或不可靠传输、流量控制、差错恢复 TCP, UDP 段 (Segment) 网关 3. 网络层 网络互联层 网络层 进行逻辑寻址(IP地址)、路由选择,实现跨网络通信 IP, ICMP, ARP, RIP, OSPF 包 (Packet) 路由器 (Router) 2. 数据链路层 网络接口层 数据链路层 进行物理寻址(MAC地址)、将数据封装成帧、差错控制 Ethernet, PPP, Wi-Fi (802.11) 帧 (Frame) 交换机 (Switch)、网桥 1. 物理层 物理层 在物理媒介上传输原始比特流,定义电气、机械特性 RJ45, 1000BASE-T, 同轴电缆, 光纤 比特 (Bit) 集线器 (Hub)、中继器、网卡
对应关系:
TCP/IP模型的 应用层 对应了OSI模型的 应用层、表示层、会话层 的全部功能。
TCP/IP模型的 网络接口层 对应了OSI模型的 数据链路层和物理层。
传输层 和 网络层 在两个模型中几乎是一一对应的。
五层模型:
为了教学和理解的方便,常将两者结合,形成一个五层模型。它保留了OSI最核心的层次划分,又采纳了TCP/IP的实践名称,是目前最常用的学习模型。
数据流动:
数据发送时,从上到下,每一层对上层数据添加自己的头部信息(封装)。
数据接收时,从下到上,每一层对下层数据剥离相应的头部信息(解封装)。
PDU (Protocol Data Unit) 是指对等层之间交换的数据单位,每层都有自己的名称。
核心思想:
上层(应用、传输)关心的是全局(应用逻辑、端到端连接)。
下层(网络、链路、物理)关心的是局部(如何跳到下一个节点、如何在物理介质上传输)。
、
案例分析(各层如何协同工作)
场景:客户端 (Client) 请求
http://www.example.com/index.html
数据发送过程 (客户端 -> 服务器):封装 (Encapsulation)
应用层 (Application Layer)
客户端做什么:浏览器(应用程序)生成一个 HTTP GET 请求。原始数据是:
GET /index.html HTTP/1.1 Host: www.example.com
。这一层决定了要做什么(获取网页)。服务器对应层做什么:Web服务器程序(如Nginx、Apache)监听在80端口,准备接收这样的请求。它最终会处理这个请求,并准备好要发送的
index.html
文件数据。传输层 (Transport Layer)
客户端做什么:TCP协议接手HTTP请求。它负责端到端的可靠传输。
与服务器的TCP协议三次握手,建立连接。
将HTTP数据分割成段(如果很大)。
为每个段添加一个 TCP 头部。头部中包含关键信息:源端口号(客户端随机端口,如 50000)和目的端口号(Web服务标准端口 80)。
服务器对应层做什么:服务器的TCP模块监听80端口。它会接收这些TCP段,重新排序(如果乱序)、校验错误,并发送确认包。确认无误后,将重组后的原始数据交给应用层的Web服务器程序。
网络层 (Network Layer)
客户端做什么:IP协议接手TCP段。它的任务是逻辑寻址和路由,让数据包能跨越多个网络到达最终目的地。
它给TCP段加上一个 IP 头部。头部中包含关键信息:源IP地址(客户端的公网IP,如
192.168.1.100
)和目的IP地址(通过DNS解析得到的www.example.com
的IP,如93.184.216.34
)。服务器对应层做什么:服务器的IP协议检查收到的IP包的目的IP地址是否是自己的。如果是,就剥去IP头部,将里面的TCP段交给传输层。
数据链路层 (Data Link Layer)
客户端做什么:以太网(Ethernet)协议接手IP数据包。它的任务是在同一个本地网络内(如你的家庭Wi-Fi)传输数据帧。
它给IP数据包加上一个 帧头部和帧尾部。
帧头部中包含关键信息:源MAC地址(你电脑网卡的物理地址)和目标MAC地址(你本地网络中的网关,通常是路由器的MAC地址)。注意:这里的目标不是服务器的MAC地址,而是下一跳设备的地址。
服务器对应层做什么:服务器所在网络的数据链路层(可能是以太网或其它技术)接收帧,检查目标MAC地址是否与自己匹配。如果匹配,就校验数据,剥去帧头和帧尾,将里面的IP数据包交给网络层。
物理层 (Physical Layer)
客户端做什么:网卡将数据链路层准备好的帧(Frame) 转换成一系列 0和1的比特流(Bits),再转换为电信号、光信号或无线电波,通过网线或空气发送出去。
服务器对应层做什么:服务器的网卡侦测到物理线路上传来的信号,将其转换回比特流,并传递给数据链路层。
数据接收与响应过程 (服务器 -> 客户端)
服务器处理请求并返回
index.html
文件的过程,与上述过程完全相反。
服务器应用层:Web服务器程序找到
index.html
文件,准备将其内容作为HTTP响应的主体。服务器传输层:TCP协议将文件数据分割成段,添加TCP头部(源端口:80,目的端口:50000)。
服务器网络层:IP协议添加IP头部(源IP: 93.184.216.34, 目的IP: 192.168.1.100)。
服务器数据链路层:以太网协议添加帧头部(源MAC: 服务器的MAC, 目标MAC: 服务器网关的MAC)。
服务器物理层:将帧转换为物理信号发送出去。
数据包经过互联网路由回到你的客户端。
你的客户端执行解封装过程:
物理层 -> 数据链路层(校验帧,看MAC地址)-> 网络层(校验IP地址)-> 传输层(TCP重组数据,发送确认)-> 应用层(浏览器接收HTTP响应,解析HTML并渲染显示给你)。
关键理解:
对等通信:虽然物理上数据是垂直传递的,但在逻辑上,客户端的应用层觉得自己是在直接和服务器的应用层对话。同样,两端的传输层、网络层也在进行直接对话。这就是分层的威力。
地址的作用:
MAC地址:在同一个局域网内标识一个设备。(“下一站去哪?”)
IP地址:在整个互联网上标识一台设备。(“最终目的地是哪?”)
端口号:在一台设备上标识一个具体的应用程序。(“交给哪个程序?”)
TCP 三次握手 (建立连接)
目的:双方同步初始序列号,建立可靠的双工通信通道。
SYN:客户端发送SYN包(
SYN=1, seq=x
),进入SYN-SENT
状态。SYN-ACK:服务器收到后回复SYN-ACK包(
SYN=1, ACK=1, seq=y, ack=x+1
),进入SYN-RCVD
状态。ACK:客户端收到后回复ACK包(
ACK=1, ack=y+1
),双方进入ESTABLISHED
状态,连接建立。核心:三次握手确保了双方都确认了自己和对方的收发能力正常,防止了已失效的连接请求报文突然传到服务器导致错误。
TCP 四次挥手 (终止连接)
目的:双方各自安全地关闭数据通道,优雅地终止连接。
FIN:主动方发送FIN包(
FIN=1, seq=u
),进入FIN-WAIT-1
状态,表示不再发送数据。ACK:被动方收到后回复ACK包(
ACK=1, ack=u+1
),进入CLOSE-WAIT
状态。主动方收到后进入FIN-WAIT-2
状态。
此时,主动方到被动方的单向连接关闭。
FIN:被动方数据发送完毕后,发送自己的FIN包(
FIN=1, seq=w
),进入LAST-ACK
状态。ACK:主动方收到后回复ACK包(
ACK=1, ack=w+1
),进入TIME-WAIT
状态,等待2MSL时间后关闭。被动方收到ACK后立即关闭。核心:因为TCP是全双工的,两个方向的数据通道必须独立关闭。四次挥手保证了双方都能安全地结束数据发送。
TIME-WAIT
状态是为了确保最后一个ACK能到达被动方,防止其重传FIN,并让本次连接的旧报文在网络中消逝。总结对比
三次握手 四次挥手 目的 建立连接 终止连接 次数 3次 4次 关键原因 防止失效连接请求造成错误 TCP全双工,需独立关闭两个方向
7. 硬件抽象与驱动设计
一、什么是“接口层”?
想象一个现实世界的例子:电源插座。
中国的插座是两脚扁插
英国的插座是三脚方插
你的电器(比如电脑)就是单片机程序,电网就是W5500硬件。你不可能为每个国家重新造一台电脑,怎么办?
答案是使用电源适配器。这个适配器规范,就是“接口层”。
它规定了一边是标准接口(比如USB-C口,连接电脑)。
另一边是具体实现(可以是中式插头、英式插头、甚至是充电宝)。
在单片机编程中,“接口层”就是这个“适配器规范”。 它是一系列的函数声明(标准),规定了“要做什么”(例如:
spi_read()
,spi_write()
,cs_select()
),但并不关心“具体怎么做”。二、为什么不能直接调用官方库,而要重写接口?
官方库(如WIZnet提供的W5500驱动库)为了能在所有人的板子上都能编译通过,它无法知道你的硬件具体是怎么连接的。
你的SPI是SPI1还是SPI2?
你的片选CS引脚接在哪个GPIO?高电平有效还是低电平有效?
你的复位引脚接在哪里?
因此,官方库会把这些硬件相关的、必须由你来实现的函数,单独拿出来,做成一个“接口层”(通常叫
user_defined.c
或wizchip_conf.c
)。这些函数通常是空函数或者只有一句while(1)
,等着你来填满它们。这就是你老师让你重写代码的原因:你需要根据自己板子的硬件连接情况,为这些接口函数提供具体的实现。
三、核心:“注册函数”到底在干什么?
这是最精妙的一步!我们来看代码。
官方库中通常会定义一些函数指针,用于指向真正的硬件操作函数。
// 在官方库(wizchip_conf.h)中,你可能会看到这样的代码:typedef struct _wizchip_callback {void (*spi_read)(uint8_t* pBuf, uint16_t len);void (*spi_write)(uint8_t* pBuf, uint16_t len);void (*cs_select)(void);void (*cs_deselect)(void);void (*reset_assert)(void);void (*reset_deassert)(void); } wizchip_callback_t; // 并声明一个全局的结构体变量 extern wizchip_callback_t my_callback;
然后,它会提供一个注册函数:
// 官方提供的注册函数void wizchip_register_callback(wizchip_callback_t* cb) {my_callback.spi_read = cb->spi_read;my_callback.spi_write = cb->spi_write;my_callback.cs_select = cb->cs_select;// ... 其他函数赋值 }
现在,轮到你了。你需要在你的代码中做两件事:
根据你的硬件,实现具体的函数:
// 在你的 main.c 或者 user_interface.c 中 // 1. 实现具体的SPI读写函数(以STM32 HAL库为例) void my_spi_read(uint8_t *pBuf, uint16_t len) {HAL_SPI_Receive(&hspi2, pBuf, len, HAL_MAX_DELAY); // 使用你的hspi1或hspi2 } void my_spi_write(uint8_t *pBuf, uint16_t len) {HAL_SPI_Transmit(&hspi2, pBuf, len, HAL_MAX_DELAY); } // 2. 实现片选和复位控制 void my_cs_select(void) {HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); // 根据你的实际引脚修改 } void my_cs_deselect(void) {HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); } void my_reset_assert(void) {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET); } void my_reset_deassert(void) {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); }
创建一个结构体,并调用注册函数,把你的函数“告诉”官方库:
// 还是在你的代码中int main(void) {// ... 初始化你的HAL库、GPIO、SPI... // 2. 创建一个回调函数结构体,并用你实现的函数填充它wizchip_callback_t my_cb;my_cb.spi_read = my_spi_read;my_cb.spi_write = my_spi_write;my_cb.cs_select = my_cs_select;my_cb.cs_deselect = my_cs_deselect;my_cb.reset_assert = my_reset_assert;my_cb.reset_deassert = my_reset_deassert; // 3. 【核心】调用官方注册函数,把这个结构体“注册”进去!wizchip_register_callback(&my_cb); // 4. 现在才可以初始化W5500wizchip_init();// ... 后续配置IP等操作 }
四、整个过程的神奇之处(工作流程)
当你调用
wizchip_init()
时,官方库的内部发生了这样的事情:官方库函数 (如: wizchip_read())
通过函数指针调用: my_callback.spi_read()
实际执行: my_spi_read()
(你根据硬件实现的函数)底层硬件驱动
(如: HAL_SPI_Receive())物理硬件
(SPI外设)总结一下这个过程的精髓:
解耦:官方协议栈代码(做什么)和你的硬件驱动代码(怎么做)完全分离。协议栈代码不需要为你的硬件做任何修改。
可移植性:如果你的项目换主板了(比如从STM32换到GD32),你只需要重新实现
my_spi_read
,my_cs_select
这几个函数,然后重新注册一下。所有上层的网络代码(TCP/IP连接、MQTT、HTTP等)完全不需要动。清晰与协作:软件工程师可以专注于上层应用逻辑,硬件工程师可以专注于底层驱动,只要他们共同遵守“接口层”这个契约即可。
核心概念:硬件抽象层与依赖注入
是在典型的嵌入式网络开发中引入一个硬件抽象层 的设计模式。其核心思想是依赖反转,通过回调函数 和注册机制 实现解耦,从而提升代码的可移植性、可测试性和模块化程度。
一、问题所在:紧耦合的原始模式
在没有抽象层的情况下,你的应用代码会直接调用官方驱动库(例如
HAL_ETH_Transmit
)。代码结构通常是这样的:// 你的应用业务逻辑,例如在某个事件中发送数据 void my_app_send_data(void) {uint8_t data[] = "Hello";// 直接依赖并调用ST官方库的发送函数HAL_ETH_Transmit(&heth, data, sizeof(data), 100); }
这种模式的缺点:
紧耦合:
my_app_send_data
函数与ST的HAL库函数HAL_ETH_Transmit
死死绑定在一起。难以移植:如果更换硬件平台(例如换成ESP32或NXP的芯片),所有调用了
HAL_ETH_Transmit
的地方都必须被找到并修改为新平台的发送函数(例如esp_eth_transmit
)。工作量巨大且容易出错。难以测试:在没有真实硬件的情况下,你几乎无法对
my_app_send_data
函数进行单元测试,因为它直接调用了操作硬件的底层函数。二、解决方案:引入抽象层与注册机制
你采用的方法将上述结构彻底改变,分为以下几个步骤:
步骤1:定义抽象接口(契约)
首先,在协议栈和应用之间定义一个稳定的、不依赖于任何硬件的接口。这通常通过函数指针类型来实现。
// net_interface.h // 定义发送函数的类型契约:它必须接受数据和长度,返回一个状态值。 typedef int (*net_send_func_t)(void *data, uint16_t len); // 定义接收回调函数的类型契约:协议栈收到数据后,通过此函数回调给你的应用。 typedef void (*net_recv_callback_t)(void *data, uint16_t len);
步骤2:提供注册接口(注入点)
协议栈提供明确的函数,允许底层驱动和应用层将其实现“注入”到协议栈中。这就是你提到的“注册函数”。
// protocol_stack.h // 提供给底层驱动的注册函数:用于注册“发送数据的实现” void protocol_register_send_function(net_send_func_t send_func); // 提供给应用层的注册函数:用于注册“收到数据后的处理函数” void protocol_register_receive_callback(net_recv_callback_t recv_cb);
步骤3:实现底层驱动并注册
现在,你不再直接让应用调官方库,而是基于官方库实现步骤1中定义的接口,并在初始化时将其注册给协议栈。
// eth_driver.c #include "net_interface.h" #include "stm32h7xx_hal.h" // 仍然包含官方驱动 // 1. 基于官方库实现抽象的发送接口 static int eth_driver_send(void *data, uint16_t len) {// 在这里调用ST的官方驱动,但对外隐藏了这个细节return HAL_ETH_Transmit(&heth, data, len, 100); } // 2. 初始化函数 void eth_driver_init(void) {// 硬件初始化HAL_ETH_Init(&heth); // 最关键的一步:将你的驱动实现注册给协议栈protocol_register_send_function(eth_driver_send); }
步骤4:协议栈使用注册的接口
协议栈内部持有你注册的函数指针,在需要发送数据时,回调你的底层实现。
// protocol_stack.c // 内部保存注册过来的函数指针 static net_send_func_t g_send_func = NULL; void protocol_register_send_function(net_send_func_t send_func) {g_send_func = send_func; // 保存起来 } // 协议栈内部需要发送数据包时(例如TCP模块) int tcp_send_packet(void *packet, uint16_t length) {if (g_send_func != NULL) {// 它不知道也不关心g_send_func指向的是STM32还是ESP32的驱动// 它只是调用了这个接口,实际执行的是你注册的 eth_driver_sendreturn g_send_func(packet, length);}return -1; // Error: no driver registered }
三、专业解读:这种模式的优势
控制反转:传统的依赖关系是“上层调用下层”。现在是“上层定义接口,下层实现并注册给上层”,依赖关系被反转了。协议栈(上层)控制着接口的定义,但不控制具体的实现。
解耦:协议栈模块和硬件驱动模块之间唯一的联系就是那个函数指针。它们可以独立开发、编译和测试。协议栈的代码完全不包含任何特定硬件的头文件或函数。
可移植性:移植到新平台时,你只需要:
根据新平台的官方驱动,重新实现
net_send_func_t
这个接口。在新的
eth_driver_init
中调用protocol_register_send_function
来注册新的实现。协议栈和应用的代码一行都不用改。
可测试性:你可以编写一个“模拟驱动”来测试协议栈。
// test_driver.c int mock_send_func(void *data, uint16_t len) {printf("Would send %d bytes\n", len);return 0; } // 在测试程序中,注册这个模拟驱动 protocol_register_send_function(mock_send_func); // 现在你就可以在没有硬件的情况下运行和测试协议栈了
总结
将“使用硬件”和“控制协议”这两件事彻底分离。
协议栈的任务是处理复杂的网络逻辑(如TCP重传、IP分片),它只需要一个可靠的“发送数据”的底层服务。
你的任务是提供这个底层服务,具体就是用STM32的官方库把数据真正送到网线上。
注册函数就是连接这两个独立世界的唯一桥梁。你写的那个调用注册函数的代码,就是在完成“依赖注入”——将具体的实现(STM32驱动)注入到抽象的框架(协议栈)中。
这是一种非常成熟、专业且强大的软件设计方法,在要求高性能、高可靠性、高可移植性的嵌入式系统中是标准做法。虽然初期会增加一些复杂性,但它为项目带来的长期好处是毋庸置疑的。
8. 控制反转
核心定义
在传统的嵌入式程序结构中,控制流是自上而下的:高层模块(应用/协议栈)直接调用底层模块(硬件驱动)的具体函数。高层主动控制着何时以及如何操作硬件。
控制反转 是一种设计原则,它反转了这种控制流的方向。高层模块不再直接依赖和控制底层模块,而是:
定义抽象接口:高层模块规定它“需要什么功能”(例如,一个
send_data()
函数)。被动等待调用:高层模块的框架(如协议栈)在适当的时机(例如,需要发送数据包时),调用由底层模块提供的具体实现。
简单来说,控制权的归属发生了反转:
传统: “我(应用)要亲自调用
HAL_ETH_Transmit
来发送数据。”(控制权在应用)IoC: “我(协议栈框架)定义了一个发送接口。谁想给我提供实现就注册进来。当需要发送时,我会回调它。”(控制权在框架)
在项目中的具体体现
让我们将这个概念映射到你老师让你写的代码上:
传统模式 (控制权在应用层)
// 应用代码主动控制,直接调用ST的库, tightly coupled void my_app() {// ... 准备数据 ...HAL_ETH_Transmit(&heth, data, len, timeout); // 直接控制硬件驱动 }
控制流:
my_app
→HAL_ETH_Transmit
(应用控制驱动)控制反转模式 (控制权在框架层)
控制流:
protocol_stack_need_to_send
(框架) →g_send_impl
(接口) →my_eth_send_impl
(你的实现) →HAL_ETH_Transmit
(官方驱动)
步骤一:框架定义接口(契约)
// protocol_stack.h (框架头文件) // 框架说:我需要一个发送函数,它必须长成这样 typedef int (*send_func_t)(void *data, uint16_t len);
步骤二:框架提供注册机制(注入点)
// 框架说:谁想提供发送功能的具体实现,就来这里注册 void protocol_stack_register_send(send_func_t impl) {g_send_impl = impl; // 框架保存这个函数指针 }
步骤三:你提供具体实现(遵从契约)
// your_eth_driver.c (你的底层代码) // 你:我来实现这个发送函数(内部用ST库实现) static int my_eth_send_impl(void *data, uint16_t len) {return HAL_ETH_Transmit(&heth, data, len, timeout); }// 在初始化时,你将实现“注入”框架 void driver_init() {protocol_stack_register_send(my_eth_send_impl); // 注册回调函数 }
步骤四:框架在需要时回调(行使控制权)
// 在协议栈框架内部的某个地方 void protocol_stack_need_to_send(void *packet) {// 框架在需要发送数据时,回调之前注册的函数if (g_send_impl != NULL) {g_send_impl(packet, packet_length); // 这里回调了你写的 my_eth_send_impl} }
可以看到,控制权从“你的应用代码”手中转移到了“协议栈框架”手中。 框架决定何时发送数据,它只是“反转”了依赖,让你来提供“如何发送”的具体实现。这就是“控制反转”这个名字的由来。
为什么这么做?(IoC的核心目的)
解耦:协议栈框架 (
protocol_stack.c
) 完全不包含、也不依赖任何STM32的官方头文件(如stm32h7xx_hal.h
)。它只依赖自己定义的抽象接口 (send_func_t
)。这意味着这个协议栈可以完全独立地编译、测试,并且可以轻松移植到任何其他平台。依赖反转原则:这是面向对象设计SOLID原则中的“D”。高层模块(协议栈)不应该依赖低层模块(硬件驱动),二者都应该依赖于抽象(接口)。你的代码完美地遵循了这一原则。
框架化设计:协议栈不再是一个被调用的库,而是一个可以运行你代码的框架。你只是向这个框架“填充”了一些必要的硬件操作实现,然后启动框架,剩下的工作就由框架来调度和控制。
总结
在你项目中的 “控制反转” 是指:
将硬件操作的控制权从应用程序手中移交到一个可复用的协议栈框架手中。应用程序不再主动调用硬件驱动,而是被动地向框架提供硬件驱动的具体实现(回调函数)。框架在运行时反向调用这些实现来完成硬件操作。
你写的那个 注册函数,就是实现控制反转的依赖注入手段。它允许你将具体的、依赖平台的实现(你的代码)注入到抽象的、不依赖平台的框架(协议栈)中。
你老师让你这么做,是在教你如何构建一个高度模块化、可测试、可移植的嵌入式系统架构,这是专业嵌入式开发的基石。
9.控制反转与依赖反转
这是一个非常经典且重要的问题。简短的回答是:它们高度相关,但指代的是不同层次的概念,并不完全等同。
可以这样理解:
控制反转是一种更广泛的设计原则或模式,描述了一种程序控制流转移的现象。
依赖反转原则是一种更具体的设计原则,它是实现控制反转的最常见、最主要的手段。
下面我们进行精确的区分。
1. 控制反转
定义:控制反转 是一种广泛的设计原则,它描述了一个框架如何与应用程序代码交互。其核心是将程序流程的控制权从应用程序代码反转到框架或容器中。
传统控制流:你的应用程序代码负责整个程序的执行流程,它决定何时调用哪些库函数。
图表
控制反转流:你将自己的代码“插入”或“注册”到一个框架中。框架负责整个程序的执行流程,并在适当的时机回调你的代码。
图表
代码
关键:IoC 关注的是 “谁拥有主控权”。是应用程序控制一切,还是一个可复用的框架在驱动整个流程?
在你项目中的体现:
你的主函数main()
初始化后,控制权就交给了协议栈框架。协议栈框架负责处理网络事件、定时器等。当需要发送数据时,框架反过来调用你之前注册的发送函数。这就是控制反转——控制权从你的手中转移到了框架手中。2. 依赖反转原则
定义:依赖反转原则 是面向对象设计 SOLID 原则中的 “D”,它包含了两个具体的高层设计指南:
高层模块不应该依赖低层模块。两者都应该依赖于抽象(接口)。
抽象不应该依赖于细节。细节(具体实现)应该依赖于抽象。
关键:DIP 关注的是 “模块间的依赖方向”。它通过引入抽象(接口)来反转传统的依赖关系。
在项目中的体现:
违反DIP的传统情况:
高层模块:
你的协议栈逻辑
→ 依赖 → 低层模块:STM32 HAL库
这是一种从上到下的直接依赖,高层直接依赖底层细节。
遵循DIP的情况:
高层模块:
你的协议栈逻辑
→ 依赖 →抽象接口(如 send_func_t)
低层模块:
你的驱动实现
→ 依赖 →同一个抽象接口(如 send_func_t)
现在,依赖关系向着抽象进行“反转”,高层和底层都依赖于同一个稳定的抽象,而不是彼此依赖。
两者的关系:依赖反转实现了控制反转
控制反转 是一个更大的概念,它可以通过多种方式实现,例如:
模板方法模式
服务定位器模式
依赖注入
...
而 依赖反转原则 是设计和架构上的指导思想,它强调要面向接口编程。
依赖注入 是遵循 依赖反转原则 最常用的具体实现技术。而你项目中的“注册回调函数”,正是一种依赖注入(具体来说是“接口注入”)。
总结一下这个因果链:
为了实现 → 控制反转
我们需要 → 遵循依赖反转原则
其具体实现技术是 → 依赖注入(注册回调函数)结论与类比
控制反转是目的:就像你想“不自己开车,而是让别人来开车”(反转控制权)。
依赖反转/依赖注入是手段:你通过“定义一个驾驶员的接口,并找到一个符合该接口的司机注入到你的车里”来实现这个目的。
所以,在你的项目中:
你感受到了 控制反转(协议栈框架控制了流程并回调你的函数)。
你实现了 依赖注入(通过注册函数将具体实现注入框架)。
整个代码结构的设计遵守了 依赖反转原则(协议栈和你的驱动都依赖于抽象的
send_func_t
接口,而不是具体实现)。因此,它们不是同一个意思,而是目的、原则和手段的关系。老师的做法同时运用了这三者,是构建高质量、可维护软件的标准方法。