[{"content":"Mihomo远程部署 前言 因为在WSL中需要下载一些东西，而因为电脑又在远程，无法使用GUI，且如果直接调用Windows中的Mihomo，会导致ssh无法正常连接，故在此直接在WSL中部署Mihomo。\n工作环境 WSL2环境 Tailscale SSH 步骤 下载Mihomo 此部分不要直接完全复制，请根据最新版本替换链接\n# 创建目录 mkdir ~/mihomo \u0026amp;\u0026amp; cd ~/mihomo # 下载链接，根据最新链接进行替换，注意应选择mihomo-linux-amd64-vx.xx.x.gz wget https://github.com/MetaCubeX/mihomo/releases/download/v1.18.9/mihomo-linux-amd64-v1.18.9.gz # 解压并重命名 gunzip mihomo-linux-amd64-v1.18.9.gz mv mihomo-linux-amd64-v1.18.9 mihomo chmod +x mihomo 配置核心文件 下载地理文件 这个文件一般Mihomo在初次运行时会自动下载，但是经常失败，所以直接手动下载\nwget -O Country.mmdb https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb 配置设置文件 你应该会有一个订阅链接，通过这个订阅链接可以下载一个config.yaml文件，使用以下语句进行下载：\ncurl -L -o config.yaml \u0026#34;https://api.v1.mk/sub?target=clash\u0026amp;url=你的订阅链接\u0026#34; 初步测试 ./mihomo -d . 如果一切正常，应该会看到Start initial configuration successful之类的内容，并且在shell中它展开了一个服务。\n常驻后台 移动文件 将Mihomo文件移动到/usr/local/bin中，这样就可以直接使用mihomo命令了。\nsudo mv mihomo /usr/local/bin/ sudo mkdir -p /etc/mihomo sudo cp Country.mmdb /etc/mihomo/ sudo cp config.yaml /etc/mihomo/ 使用systemd守护进程 创建服务文件sudo nano /etc/systemd/system/mihomo.service\n然后写下以下内容：\n[Unit] Description=mihomo Daemon After=network.target [Service] Type=simple Restart=always ExecStart=/usr/local/bin/mihomo -d /etc/mihomo [Install] WantedBy=multi-user.target 再Ctrl+O、Enter、Ctrl+X保存文件。\n启动服务 sudo systemctl daemon-reload sudo systemctl enable mihomo sudo systemctl start mihomo 配置启动命令 在~/.bashrc中添加以下内容：\n# 代理开关快捷键 alias proxy=\u0026#34;export https_proxy=\u0026#39;http://127.0.0.1:7890\u0026#39;; export http_proxy=\u0026#39;http://127.0.0.1:7890\u0026#39;; echo \u0026#39;Proxy ON\u0026#39;\u0026#34; alias unproxy=\u0026#34;unset https_proxy; unset http_proxy; echo \u0026#39;Proxy OFF\u0026#39;\u0026#34; 然后source ~/.bashrc使其生效。\n之后输入proxy命令，即可使用代理。输入unproxy命令，即可取消代理。\n界面化配置 我们有时候需要修改连接线路，如果直接在config里面修改很麻烦，通过yacd可以很方便地解决这一问题。\n安装yacd cd /etc/mihomo sudo git clone -b gh-pages https://github.com/haishanh/yacd.git dashboard 修改config.yaml 在config.yaml中添加以下内容：\nexternal-controller: 0.0.0.0:9090 # 允许外部连接控制端口 external-ui: dashboard # 刚才下载的文件夹名称 secret: \u0026#34;你的密码\u0026#34; # 设置一个密码，防止别人乱动 然后重启mihomo：sudo systemctl restart mihomo。\n访问yacd 在浏览器中输入http://tailscale的IP地址:9090/ui，即可访问yacd。\n至此，Mihomo的远程部署就完成了。\n","date":"2026-03-19T11:56:51+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/mihomo%E8%BF%9C%E7%A8%8B%E9%83%A8%E7%BD%B2/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/mihomo%E8%BF%9C%E7%A8%8B%E9%83%A8%E7%BD%B2/","title":"Mihomo远程部署"},{"content":"Qwen3-ASR部署 前言 模型介绍 Qwen3-ASR是阿里的千问团队开发的一个语音识别模型，包括两个版本Qwen3-ASR-1.7B 与 Qwen3-ASR-0.6B，以及一个创新的语音强制对齐模型 Qwen3-ForcedAligner-0.6B。\nQwen3-ASR最具特色的是其支持52个语种和方言的语种识别和语音识别，这在传统只能标准英语和普通话的语音识别模型中有较大突破。\n部署目标 本人有一台装有4070TIS的Windows主机，同时有一台性能普通的笔记本。因为个人研究需求，需要将Qwen3-ASR模型部署到一台服务器上，刚好这个Windows主机就可以成为我的测试服务器，而笔记本作为客户端与服务器进行通信调用。\n部署思路 基本的部署思路是，在Windows主机中通过WSL安装miniconda环境，然后部署Qwen3-ASR环境，通过tailscale将主机与笔记本连接，通过ssh在笔记本控制运行WSL，通过tailscale提供的局域网进行通信。本文主要详述Qwen3-ASR部署流程，关于远程部署部分只会简单提及，有需求可以根据提及的部分进行对应搜索。\n准备工作 一台高性能Windows主机，在本文中其显卡为4070TIS，系统为Windows11，后续我们称为服务器。 另一台电脑，本文中为笔记本，系统为Windows11，后续我们称为客户端。 服务器Windows系统中的tailscale和OpenSSH Server服务。因为我很早之前就已经为这个主机安装了ssh服务器，所以本文默认已经在Windows系统中有ssh服务，并且整个操作均在远端通过ssh访问，而因为服务器没有公网，所以内网穿透由tailscale提供。 部署流程 WSL安装 经过尝试，直接在Windows上部署Qwen3-ASR模型的话，比较容易安装失败，尽管可以通过docker安装，但是在后续开发上仍然可能出现一些类似的踩坑，毕竟我们是为了进行软件开发，所以在WSL上进行安装是更好的选择。\n关于WSL的安装，可以参考官方文档进行，我直接默认安装的Ubuntu系统。\ntailscale安装 安装完WSL后，我们需要在WSL中安装tailscale，这样我们就可以在远端直接用ssh访问WSL的文件系统了，尽管我们也可以先访问Windows，然后在一个Powershell终端里面通过wsl命令访问WSL，但是这种做法无法打开WSL的文件系统，所以直接用tailscale在内部穿透是比较好的方法。关于tailscale的安装，可以参考官方文档。\nssh服务器 在tailscale安装完成后，就已经获得了一个新的内网IP，此时我们可以通过ssh连接WSL了，不过在此之前需要为WSL安装ssh服务器，安装命令如下：\nsudo apt update sudo apt install openssh-server 由于 WSL2 的一些限制（如默认不支持密码登录或端口冲突），我们需要微调一下配置文件：sudo nano /etc/ssh/sshd_config 取消以下内容的注释并根据需要进行修改：\nPort 2222（建议将端口改为 2222，因为 Windows 自身可能已经占用了 22 端口）。 PasswordAuthentication yes（如果你想用密码登录，请确保这里是 yes）。 ListenAddress 0.0.0.0（允许从任何 IP 访问）。 最后保存并退出，按Ctrl+O保存，Enter确认，Ctrl+X退出。\n然后重启服务：sudo service ssh restart。\n通过tailscale ip指令可以获取IP地址，然后通过ssh连接WSL。\n为了方便，我们可以在vscode中安装Remote Development来辅助远程开发。\nMiniconda安装 我们当然不希望只在WSL中安装一个Qwen3-ASR模型后就不玩其它模型了，所以Python环境管理工具是很重要的，因为我们在WSL中且不需要什么界面环境，所以安装miniconda即可，参考官方文档进行安装。\n安装完成后，我们创建一个conda环境：conda create -n qwen-asr-env python=3.10 -y。实际上官方推荐的使用python=3.12，但是因为我个人的需求，部分依赖库没有后续更新，所以我选择了3.10版本。\n之后启动这个环境conda activate qwen-asr-env。\nQwen3-ASR安装 其实到这一步后，部署就已经很简单了：\npip install -U qwen-asr[vllm] MAX_JOBS=4 pip install -U flash-attn --no-build-isolation 两步就可以完成，但是事实上，在安装Flash Attention的时候很容易失败，我之前是在Windows中安装，就反复失败，但是在WSL中就成功了。另外如果觉得下载很慢，可以修改镜像，只需要通过export PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple修改镜像源即可，当然也可以在install命令里面直接添加镜像。\nQwen3-ASR远程调用 在完成部署后，我们就可以在客户端进行远程调用了，因为我们已经通过tailscale分配了IP，现在只需要指定端口即可。下面我写了两个Python脚本，分别运行在客户端和服务器上，实现了包含VAD功能的类流式语音识别，代码没有精修，大概能运行的程度。\n服务器端 import socket import struct import numpy as np from qwen_asr import Qwen3ASRModel import torch.distributed as dist # 配置参数 ASR_MODEL_PATH = \u0026#34;Qwen/Qwen3-ASR-1.7B\u0026#34; HOST = \u0026#34;100.xx.xx.xx\u0026#34; # 换成自己的Tailscale IP PORT = 8823 # 端口号 SAMPLE_RATE = 16000 # 采样率 BYTES_PER_SAMPLE = 4 # float32 = 4 bytes def recv_exact(sock, n): \u0026#34;\u0026#34;\u0026#34;接收精确长度的字节数据，直到收满或连接断开\u0026#34;\u0026#34;\u0026#34; data = b\u0026#39;\u0026#39; while len(data) \u0026lt; n: packet = sock.recv(n - len(data)) if not packet: return None data += packet return data def main(): print(\u0026#34;正在初始化 vLLM 模型...\u0026#34;) asr = Qwen3ASRModel.LLM( model=ASR_MODEL_PATH, gpu_memory_utilization=0.6, max_model_len=16384, max_new_tokens=4096, # 可根据需要调整 ) print(\u0026#34;模型加载完成，启动 TCP 服务器...\u0026#34;) server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_sock.bind((HOST, PORT)) server_sock.listen(5) print(f\u0026#34;服务器监听 {HOST}:{PORT}，等待客户端连接...\u0026#34;) try: while True: client_sock, client_addr = server_sock.accept() print(f\u0026#34;客户端 {client_addr} 已连接\u0026#34;) try: while True: len_data = recv_exact(client_sock, 4) if len_data is None: print(\u0026#34;客户端断开连接\u0026#34;) break data_len = struct.unpack(\u0026#39;!I\u0026#39;, len_data)[0] if data_len == 0: break audio_data = recv_exact(client_sock, data_len) if audio_data is None: print(\u0026#34;客户端断开连接\u0026#34;) break audio_seg = np.frombuffer(audio_data, dtype=np.float32).copy() state = asr.init_streaming_state( unfixed_chunk_num=2, unfixed_token_num=5, chunk_size_sec=2.0, ) asr.streaming_transcribe(audio_seg, state) asr.finish_streaming_transcribe(state) print(f\u0026#34;[{client_addr}] {state.text}\u0026#34;) except Exception as e: print(f\u0026#34;处理客户端 {client_addr} 时出错: {e}\u0026#34;) finally: client_sock.close() print(f\u0026#34;客户端 {client_addr} 连接关闭\u0026#34;) except KeyboardInterrupt: print(\u0026#34;服务器关闭\u0026#34;) finally: server_sock.close() if hasattr(asr, \u0026#39;close\u0026#39;): asr.close() if dist.is_initialized(): dist.destroy_process_group() if __name__ == \u0026#34;__main__\u0026#34;: main() 客户端 import socket import struct import pyaudio import numpy as np import torch from silero_vad import VADIterator, get_speech_timestamps # 导入新库 SERVER_IP = \u0026#39;100.xx.xx.xx\u0026#39; # 服务器的Tailscale IP SERVER_PORT = 8823 SAMPLE_RATE = 16000 CHUNK_SIZE = 512 FORMAT = pyaudio.paInt16 CHANNELS = 1 print(\u0026#34;正在加载 Silero VAD 模型...\u0026#34;) from silero_vad import load_silero_vad model = load_silero_vad() vad_iterator = VADIterator(model, sampling_rate=SAMPLE_RATE) print(\u0026#34;VAD 模型加载完成。\u0026#34;) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((SERVER_IP, SERVER_PORT)) print(f\u0026#34;已连接到服务器 {SERVER_IP}:{SERVER_PORT}\u0026#34;) p = pyaudio.PyAudio() stream = p.open(format=FORMAT, channels=CHANNELS, rate=SAMPLE_RATE, input=True, frames_per_buffer=CHUNK_SIZE) def send_audio(int16_bytes): \u0026#34;\u0026#34;\u0026#34;将 int16 音频转换为 float32 并发送给服务器\u0026#34;\u0026#34;\u0026#34; # int16 -\u0026gt; float32 (归一化到 [-1, 1]) audio_float32 = np.frombuffer(int16_bytes, dtype=np.int16).astype(np.float32) / 32768.0 data = audio_float32.tobytes() # 发送长度前缀 + 音频数据 sock.sendall(struct.pack(\u0026#39;!I\u0026#39;, len(data))) sock.sendall(data) print(f\u0026#34;[客户端] 已发送一句话，长度: {len(int16_bytes)} 字节\u0026#34;) print(\u0026#34;开始监听麦克风...（按 Ctrl+C 退出）\u0026#34;) try: speech_buffer = [] # 用于缓存当前这句话的音频帧 while True: audio_frame = stream.read(CHUNK_SIZE, exception_on_overflow=False) # 转换为 numpy 数组 (int16) audio_int16 = np.frombuffer(audio_frame, dtype=np.int16) # 转换为 float32 torch tensor (Silero 要求的格式) audio_tensor = audio_int16.astype(np.float32) / 32768.0 audio_tensor = torch.from_numpy(audio_tensor) speech_dict = vad_iterator(audio_tensor, return_seconds=False) if speech_dict: # 如果检测到语音开始 (\u0026#39;start\u0026#39; key) if \u0026#39;start\u0026#39; in speech_dict: print(\u0026#34;检测到语音开始\u0026#34;) # 如果检测到语音结束 (\u0026#39;end\u0026#39; key) if \u0026#39;end\u0026#39; in speech_dict: print(\u0026#34;检测到语音结束\u0026#34;) # 将缓存的所有音频帧拼接并发送 if speech_buffer: full_sentence = b\u0026#39;\u0026#39;.join(speech_buffer) send_audio(full_sentence) speech_buffer = [] # 清空缓存，准备下一句 else: print(\u0026#34;警告：检测到语音结束，但缓存为空\u0026#34;) speech_buffer.append(audio_frame) except KeyboardInterrupt: print(\u0026#34;\\n用户中断\u0026#34;) # 处理最后可能残留的音频（如果程序在说话中间被中断） if speech_buffer: print(\u0026#34;处理最后残留的音频...\u0026#34;) if len(speech_buffer) * CHUNK_SIZE / SAMPLE_RATE \u0026gt; 0.5: full_sentence = b\u0026#39;\u0026#39;.join(speech_buffer) send_audio(full_sentence) finally: print(\u0026#34;正在清理资源...\u0026#34;) stream.stop_stream() stream.close() p.terminate() sock.close() print(\u0026#34;客户端已关闭。\u0026#34;) 通过上述步骤，我们成功地在 Tailscale 网络中部署了一个基于 Silero VAD 的 Qwen3-ASR 服务，并实现了客户端与服务器之间的音频传输和语音识别功能。客户端通过麦克风捕获音频，并发送给服务器进行语音识别，服务器将识别结果返回给客户端，通过ssh可以看到结果。\n总结 以上就是关于Qwen3-ASR远程部署的过程，整体流程比较粗狂，不过可以作为一个参考，毕竟安装环境只要安装上就行了。享受这个新AI吧。\n","date":"2026-03-18T16:46:38+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/qwen3-asr%E8%BF%9C%E7%A8%8B%E9%83%A8%E7%BD%B2/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/qwen3-asr%E8%BF%9C%E7%A8%8B%E9%83%A8%E7%BD%B2/","title":"Qwen3-ASR远程部署"},{"content":"关于WPF中xmlns的理解 命名空间映射 以一个通过Visual Studio直接创建的简单WPF项目为例，我们查看其MainWindow.xaml文件，这是一个纯粹的窗口：\n\u0026lt;Window x:Class=\u0026#34;Test.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:local=\u0026#34;clr-namespace:Test\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; Title=\u0026#34;MainWindow\u0026#34; Height=\u0026#34;450\u0026#34; Width=\u0026#34;800\u0026#34;\u0026gt; \u0026lt;Grid\u0026gt; \u0026lt;/Grid\u0026gt; \u0026lt;/Window\u0026gt; 其中这一部分很难理解：\nx:Class=\u0026#34;Test.MainWindow\u0026#34; xmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; xmlns:x=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml\u0026#34; xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; xmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; xmlns:local=\u0026#34;clr-namespace:Test\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; 微软官方文档说，这个部分的内容其实叫XAML命名空间映射。\n命名空间重命名 我们知道，在C#中就存在命名空间，也就是我们的namespace。\n而XAML则是微软基于XML开发的一种声明性语言，WPF是XAML最出名的使用者，而关于XAML的解析则由.NET框架实现，它常被用于定义构造用户界面（UI）。\n显然这是两种不同的语言，但显然两者需要进行交互，那么XAML就需要一种功能来调用命名空间，而这就是xmlns（XML/XAML namespace）的功能。\n我们知道，对于C#中，调用其它命名空间的一种方式就是使用using语句，比如：\nusing System.Collections.Generic; List\u0026lt;string\u0026gt; list = new List\u0026lt;string\u0026gt;(); 而xmlns也是类似这样的功能，我们可以通过类似于以下的方式创建一个我们自定义于命名空间叫Test下的一个TestButton控件：\nxmlns:ts=\u0026#34;clr-namespace:Test\u0026#34; \u0026lt;ts:TestButton /\u0026gt; 当然，如果更加准确一点形容，它在C#中应该更加类似于以下这种形式：\nusing ts = Test; ts.TestButton = new ts.TestButton(); 也就是说，xmlns:（name）这种格式有点类似于引用命名空间并重命名或重映射，这里就是把Test这个命名空间重映射为ts了。\n但是和C#中非常大的不同是，C#可以不重映射，比如直接通过以下形式实现：\nTest.TestButton = new Test.TestButton; 但是XAML中要引用命名空间则必须先使用xmlns语句将其重映射一遍，其中XAML中调用命名空间时使用的clr-namespace:是一个固定关键词，在这个关键词后面可以通过类似C#的方式来写命名空间，比如如果TestButton也是一个命名空间的话，那么就可以通过加.的方式进行连接：\nxmlns:tb=\u0026#34;clr-namespace:Test.TestButton\u0026#34; 这一点用C#的思想去理解就可以了。\n而事实上，我们的xmlns:local就是这个功能，它提供的服务就是将项目的命名空间引用起来了，而这个local可以改成任意其他名称，不过通常我们不用改。\n默认命名空间 现在我们大概理解了xmlns的作用，那么让我们重新回顾语句，会发现其中有一个没加:的语句，即：\nxmlns=\u0026#34;http://schemas.microsoft.com/winfx/2006/xaml/presentation\u0026#34; 这个语句其实代表了一个默认命名空间，而它后面跟的不是clr-namespace:而是一个网址。\n事实上这个网址并不是一个网页，而是统一资源标识符（URI），使用这种格式而非命名空间名字，最大的原因是为了保证全局唯一性，因为microsoft.com这个网址是微软拥有的，所以这个URI只有微软能用，这个URI的信息主要如下：\nhttp://schemas.microsoft.com/winfx/2006/xaml/presentation | | | | | 公司 规范 年份 技术 具体组件 其中schemas代表模式或规范，而WinFx/2006就是WPF的代号和规范的年份，xaml代表用于XAML技术，presentation代表WPF的Presentation层。\n补充一个使用技巧：当你想知道某个命名空间URI对应的实际程序集时，可以查看项目的*.csproj文件，里面会有相应的引用。比如默认WPF命名空间对应多个程序集引用。\n简单理解，这一个自动生成的xmlns代表的就是WPF的整个库，而它没有:则是因为它将作为默认空间使用，在XAML中任何没有加前缀的控件，都会优先在这个命名空间里面寻找，比如：\n\u0026lt;Button Content=\u0026#34;Button\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34;/\u0026gt; 和我们前面的TestButton的调用方法不同，前面并没有加任何name:，所以这种Button就会直接在xmlns指向的默认命名空间中寻找。\n否则，我们在前面加一个ts:：\n\u0026lt;ts:Button Content=\u0026#34;Button\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34;/\u0026gt; 只是加了一个ts:，现在就必须在一个xmlns:ts的命名空间里面寻找了，如果前面没有引用这个空间，或者空间里面没有Button这个类，那么就会报错。\n特殊的x 我们发现在我们开始举例的XAML代码里面，最开始有一个x:Class，而后面才出现了xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml。\n并且通过我们刚才对URI的理解，我们发现这个x的URI和刚刚的xmlns的很相似，只是它的URI少了/presentation，这代表它似乎是直接对XAML本身的规范。\n事实上在XAML中x也确实是一个比较特殊的命名空间，它提供的不是某些控件，而是XAML本身的一些语言特性或特殊操作。\n而x:Class就是这个命名空间中一个比较重要的操作，它提供了XAML和C#后台代码的连接，比如在XAML中我们看到：\nx:Class=\u0026#34;Test.MainWindow\u0026#34; 这其实对应了C#后台代码的：\nnamespace Test { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } } } 可以发现，它们的命名空间和类名完美对应上了，在编译时，两个板块才能够连接在一起。\n而它的原理就是指定了这个XAML编译后生成的局部类(partial class)应该与哪个C#类合并，这也解释了为什么C#中的类是partial的。\n显然x在工作中并不是调用某个C#的类或方法，而是作为一种特殊工具来操作XAML本身。\n而x的常用功能很多，比如我们在XAML中通过语句添加了一个Button控件，就像我们刚才那样：\n\u0026lt;Button Content=\u0026#34;Button\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34;/\u0026gt; 我们或许可以在VS的布局界面看到这个按钮，但是我们该如何在C#中调用这个按钮呢，其实要做的就是添加x:Name，比如：\n\u0026lt;Button x:Name=\u0026#34;button\u0026#34; Content=\u0026#34;Button\u0026#34; HorizontalAlignment=\u0026#34;Center\u0026#34; VerticalAlignment=\u0026#34;Center\u0026#34;/\u0026gt; 现在，我们就可以在C#后台代码中通过变量名button直接访问控制这个控件了，操作变得非常简单。\n关于x，常用的指令有：\nx:Class - 连接XAML和C#后台代码 x:Name - 给控件起名字，用于后台访问 x:Key - 给资源（样式、模板等）起标识符 x:Type - 表示一个类型 x:Static - 引用静态成员 另外x这个名字是可以改的，因为它只是一个约定俗成的名字，其本身仍然遵守XAML的语法规则。但是通常不建议修改，因为一些库里面习惯了这个约定俗成的名字，改了很可能会出现一些意想不到的错误，而且也会降低代码可读性。\n用于设计的d 我们还可以看到一个自动生成的命名空间：\nxmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; d=Design，代表设计时。\n它和x类似，不直接提供控件但是提供一些特殊服务，而这里的服务则是一些供Visual Studio设计器或者Blend中显示，而运行时会消失的特殊属性，比如：\n\u0026lt;Window xmlns:d=\u0026#34;http://schemas.microsoft.com/expression/blend/2008\u0026#34; d:DesignHeight=\u0026#34;450\u0026#34; d:DesignWidth=\u0026#34;800\u0026#34;\u0026gt; 这是在设计器里面提供了一个预览尺寸，方便在设计界面的时候看到效果，但是实际运行时窗口的大小则和这两个属性无关。\n在实际项目开发中，特别是使用MVVM模式时，d:命名空间提供的设计时数据功能非常有用。比如：\nd:DataContext=\u0026#34;{d:DesignInstance Type=local:MainViewModel}\u0026#34; 这可以在设计器里显示绑定数据的效果，避免\u0026quot;设计时一片空白\u0026quot;的问题。所以对于专业WPF开发，d:是很有价值的工具。\n不过你如果担心它会污染你的代码，出现一些意想不到的错误，其实可以放心，因为下面一个命名空间的功能解决了这个问题。\n标记兼容性的mc 继续往下看，我们可以发现两个语句：\nxmlns:mc=\u0026#34;http://schemas.openxmlformats.org/markup-compatibility/2006\u0026#34; mc:Ignorable=\u0026#34;d\u0026#34; 这里面都出现了mc命名空间，mc=Markup Compatibility，即标记兼容性。\n这个命名空间通常与d:配合使用，它会告诉编译器，有些标记只是设计时使用的，运行的时候可以忽略，即mc:Ignorable=\u0026quot;d\u0026quot;。\n如果没有这个声明，编译器在看到d:DesignHeight时可能会产生困惑，这个属性是什么，自己似乎并未见过（因为这个功能只在设计器或Blend中被调用实现了）。\n此外，利用mc的一些特性，可以将某些实验功能或未来版本的功能标记为可忽略，这样旧版本的编译器就不会报错。\n同样，大部分时候我们不需要关注这个标记的用法，需要用的时候再自行查询即可。\n总结：xmlns的层次结构 默认命名空间 (xmlns=\u0026quot;...\u0026quot;) - WPF核心控件库 x命名空间 (xmlns:x=\u0026quot;...\u0026quot;) - XAML语言特性 自定义映射 (xmlns:别名=\u0026quot;clr-namespace:...\u0026quot;) - 引用自己的代码 设计时支持 (xmlns:d=\u0026quot;...\u0026quot;) - 设计器辅助功能 兼容性处理 (xmlns:mc=\u0026quot;...\u0026quot;) - 版本和工具兼容 这种分层设计让XAML既能使用框架控件，又能扩展自定义功能，同时支持设计时和运行时的不同需求。\n","date":"2026-01-14T22:04:53+08:00","image":"/posts/%E6%A1%8C%E9%9D%A2%E7%AB%AF/%E5%85%B3%E4%BA%8Ewpf%E4%B8%ADxmlns%E7%9A%84%E7%90%86%E8%A7%A3/cover.jpg","permalink":"/posts/%E6%A1%8C%E9%9D%A2%E7%AB%AF/%E5%85%B3%E4%BA%8Ewpf%E4%B8%ADxmlns%E7%9A%84%E7%90%86%E8%A7%A3/","title":"关于WPF中xmlns的理解"},{"content":"蓝牙协议HCI抓包实验 实验准备 实验硬件 一台电脑 一台安卓手机（以VIVO手机为例） 一个蓝牙设备（以小米手环为例） 一根手机数据线 实验软件 安卓调试桥（ADB工具） 实验步骤 软件安装 下载ADB工具 访问链接后，根据电脑平台选择软件包\n解压文件 下载得到一个压缩包，将其解压缩到一个文件夹中，并打开该文件夹\n检查下载 在该目录下右键选择在终端中打开，或可以先行打开终端然后跳转到该目录。\n输入.\\adb.exe --version来检查是否可用，如果正常，应该会显示版本号。\n手机设置 打开开发者模式 现在的安卓手机，开发者选项是隐藏的，需要手动打开开发者模式才能看到。\n根据不同手机，可能有不同打开开发者模式的方法，vivo手机的方式是进入设置-关于手机-版本信息，连续点击软件版本号，即可开启开发者模式。大部分手机应该也是类似的方式，如果该操作不行，请自行搜索自己手机的开启方式。\n开启日志功能 进入设置中，选择开发者选项，并将启用蓝牙HCI信息收集日志选项设置为启用。注意：如果此时蓝牙已经启动，应该将其关闭后重启，然后蓝牙日志就将开始记录。\n开启USB调试功能 为了电脑能够读取日志，需要开启USB调试功能。\n完成一次数据传输 正常情况下，手环只要是用过的，在手机蓝牙开启后都会自动连接到手机上，不过为了测试，我们可以手动打开手环软件，比如小米的运动健康，然后刷新手环数据。此时理论上我们的手机和手环会进行如下通信：手机告知手环自己需要的数据，手环将数据传输给手机，当然它们可能还会参杂一些其他相关的认证消息，但是总体内容应该差不多就是这些。\n在完成了这个操作后，为了数据干净，就可以选择关闭蓝牙了，我们的通信日志已经被记录下来了。\n开始调试 连接电脑 我们现在需要的工作就是将通信日志保存到电脑中，此时就需要用到我们的ADB调试工具了。首先将手机通过数据线连接电脑。\n因为开启了USB调试模式，此时会弹窗问是否允许该电脑进行调试，选择允许即可。\n然后选择传输模式为管理文件。\n此时，我们的电脑已经可以对其进行调试了。\nADB读取报告 通过.\\adb.exe bugreport然后加压缩包文件名的方式，可以将日志下载到一个压缩包中，该过程需要一两分钟。\n下载完成后就可以解压缩该压缩包，然后通过访问这个路径，可以找到蓝牙日志。如果是其他手机，不确定是不是一样的，但是可以通过搜索hci和log两个关键字来查找。\n其中btsnoop_hci.log.last就是我们的目标文件了，当然根据实际情况可能没有.last文件，此时只有.log文件大概率也是对的。\n查看抓包内容 找到连接 打开WireShark，将btsnoop_hci.log.last拖拽进去打开。\n现在我们关注Source和Destination，只要其中出现了我们的设备名，就说明抓包成功了。\n可以发现，我们的手机先给手环发送了一个L2CAP发送包，然后手环回了一个L2CAP的接收包。我们知道L2CAP是用于蓝牙协议中建立连接的，故此可以知道我们的通信成功了。\n观察数据 我们继续往下观察，可以发现RFCOMM协议的几个包，我们知道RFCOMM协议是用于构建虚拟串口的，显然这里是在构建通信连接。\n而后我们继续观察，可以发现过了一段时间后，突然出现了大量的SPP包，我们知道这个包适用于规范RFCOMM传输内容的，而且通过数据包长度也可以知晓，这显然就是在传输我们的手环数据给手机。\n让我们点开其中的一个看一下里面的数据：\n这部分数据是data部分的内容，而具体内容则是非常杂乱的，我们在此可以猜测这个数据大概率是加密的数据，需要在手机软件中才会解密，防止被中间截获。\n","date":"2025-11-17T18:31:28+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E8%93%9D%E7%89%99%E5%8D%8F%E8%AE%AEhci%E6%8A%93%E5%8C%85%E5%AE%9E%E9%AA%8C/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E8%93%9D%E7%89%99%E5%8D%8F%E8%AE%AEhci%E6%8A%93%E5%8C%85%E5%AE%9E%E9%AA%8C/","title":"蓝牙协议HCI抓包实验"},{"content":"数独 问题 数独是在一个9*9宫格中，保证每行每列以及九个九宫格中填入的1-9的数字不出现重复。通过穷举法实现求解。\n程序 import tkinter as tk from tkinter import ttk, messagebox import time import threading class SudokuSolver: def __init__(self, root): self.root = root self.root.title(\u0026#34;数独求解器 - 穷举法\u0026#34;) self.root.geometry(\u0026#34;600x700\u0026#34;) # 初始化数独网格 self.board = [[0 for _ in range(9)] for _ in range(9)] self.initial_board = [[0 for _ in range(9)] for _ in range(9)] # 存储初始题目 self.cells = [[None for _ in range(9)] for _ in range(9)] self.selected_cell = None self.solving = False self.speed = 100 # 默认速度（毫秒） self.algorithm_time = 0 # 纯算法时间 self.step_count = 0 # 步骤计数器 # 初始化组件变量 self.speed_label = None self.speed_scale = None self.status_label = None self.stats_label = None self.create_widgets() def create_widgets(self): # 主框架 main_frame = ttk.Frame(self.root, padding=\u0026#34;10\u0026#34;) main_frame.pack(fill=tk.BOTH, expand=True) # 标题 title_label = ttk.Label(main_frame, text=\u0026#34;数独求解器 - 穷举回溯法\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 16, \u0026#34;bold\u0026#34;)) title_label.pack(pady=10) # 数独网格框架 grid_frame = ttk.Frame(main_frame) grid_frame.pack(pady=20) # 创建9x9网格 self.create_grid(grid_frame) # 控制面板 control_frame = ttk.Frame(main_frame) control_frame.pack(pady=20) # 速度控制 speed_frame = ttk.Frame(control_frame) speed_frame.pack(pady=10) ttk.Label(speed_frame, text=\u0026#34;演示速度:\u0026#34;).pack(side=tk.LEFT) self.speed_scale = ttk.Scale(speed_frame, from_=10, to=1000, orient=tk.HORIZONTAL, length=200, command=self.update_speed) self.speed_scale.set(100) self.speed_scale.pack(side=tk.LEFT, padx=10) self.speed_label = ttk.Label(speed_frame, text=\u0026#34;100 ms\u0026#34;) self.speed_label.pack(side=tk.LEFT) # 按钮 button_frame = ttk.Frame(control_frame) button_frame.pack(pady=10) ttk.Button(button_frame, text=\u0026#34;开始求解\u0026#34;, command=self.start_solving).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text=\u0026#34;直接作答\u0026#34;, command=self.direct_solve).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text=\u0026#34;重置\u0026#34;, command=self.reset_board).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text=\u0026#34;清空\u0026#34;, command=self.clear_board).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text=\u0026#34;示例\u0026#34;, command=self.load_example).pack(side=tk.LEFT, padx=5) # 状态显示 self.status_label = ttk.Label(main_frame, text=\u0026#34;请点击格子并输入数字1-9\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 10)) self.status_label.pack(pady=10) # 统计信息 self.stats_label = ttk.Label(main_frame, text=\u0026#34;步骤: 0 | 算法时间: 0.00s\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 10)) self.stats_label.pack(pady=5) # 操作说明 instruction_text = \u0026#34;\u0026#34;\u0026#34;操作说明： 1. 点击格子，然后按数字键1-9填入数字 2. 按0或Delete键清空格子 3. 调整速度滑块控制演示速度 4. 点击\u0026#34;开始求解\u0026#34;观看穷举求解过程 5. 点击\u0026#34;直接作答\u0026#34;直接获得答案 6. 紫色格子表示初始题目，绿色表示尝试的数字，红色表示回溯\u0026#34;\u0026#34;\u0026#34; instruction_label = ttk.Label(main_frame, text=instruction_text, justify=tk.LEFT, font=(\u0026#34;Arial\u0026#34;, 9)) instruction_label.pack(pady=10) def create_grid(self, parent): # 创建9x9网格，每个3x3区域有更粗的边框 for i in range(9): for j in range(9): # 确定边框宽度 - 为3x3区域边界设置更粗的边框 border_width = 1 if i % 3 == 0 and i != 0: border_width = 3 if j % 3 == 0 and j != 0: border_width = 3 # 为外边框设置更粗的边框 if i == 0 or i == 8: border_width = 3 if j == 0 or j == 8: border_width = 3 cell = tk.Entry(parent, width=3, font=(\u0026#34;Arial\u0026#34;, 16), justify=tk.CENTER, relief=\u0026#34;solid\u0026#34;, borderwidth=border_width) cell.grid(row=i, column=j, padx=(1, 1), pady=(1, 1), ipady=5) # 绑定事件 cell.bind(\u0026#34;\u0026lt;Button-1\u0026gt;\u0026#34;, lambda e, row=i, col=j: self.select_cell(row, col)) cell.bind(\u0026#34;\u0026lt;Key\u0026gt;\u0026#34;, lambda e, row=i, col=j: self.on_key_press(e, row, col)) cell.bind(\u0026#34;\u0026lt;FocusIn\u0026gt;\u0026#34;, lambda e, row=i, col=j: self.select_cell(row, col)) self.cells[i][j] = cell def select_cell(self, row, col): # 取消之前选中格子的高亮 if self.selected_cell: old_row, old_col = self.selected_cell self.cells[old_row][old_col].config(bg=self.get_cell_color(old_row, old_col)) # 高亮当前选中的格子 self.selected_cell = (row, col) self.cells[row][col].config(bg=\u0026#34;#e0e0ff\u0026#34;) self.cells[row][col].focus_set() self.status_label.config(text=f\u0026#34;选中格子: ({row+1}, {col+1})\u0026#34;) def get_cell_color(self, row, col): \u0026#34;\u0026#34;\u0026#34;根据格子内容返回对应的背景颜色\u0026#34;\u0026#34;\u0026#34; if self.initial_board[row][col] != 0: return \u0026#34;#e0c0ff\u0026#34; # 初始题目 - 紫色 else: return \u0026#34;white\u0026#34; # 空白格子 - 白色 def on_key_press(self, event, row, col): if event.char in \u0026#39;123456789\u0026#39;: self.set_cell_value(row, col, int(event.char)) elif event.keysym in [\u0026#39;Delete\u0026#39;, \u0026#39;BackSpace\u0026#39;] or event.char == \u0026#39;0\u0026#39;: self.set_cell_value(row, col, 0) # 阻止默认行为 return \u0026#34;break\u0026#34; def set_cell_value(self, row, col, value): self.board[row][col] = value if value == 0: self.cells[row][col].delete(0, tk.END) else: self.cells[row][col].delete(0, tk.END) self.cells[row][col].insert(0, str(value)) # 更新格子颜色 self.cells[row][col].config(bg=self.get_cell_color(row, col)) def update_speed(self, value): self.speed = int(float(value)) # 确保 speed_label 已创建 if hasattr(self, \u0026#39;speed_label\u0026#39;) and self.speed_label is not None: self.speed_label.config(text=f\u0026#34;{self.speed} ms\u0026#34;) def reset_board(self): for i in range(9): for j in range(9): self.set_cell_value(i, j, self.board[i][j]) self.status_label.config(text=\u0026#34;棋盘已重置\u0026#34;) def clear_board(self): for i in range(9): for j in range(9): self.set_cell_value(i, j, 0) self.initial_board[i][j] = 0 # 同时清空初始题目 self.status_label.config(text=\u0026#34;棋盘已清空\u0026#34;) self.step_count = 0 self.algorithm_time = 0 self.update_stats() def load_example(self): # 加载一个示例数独 example = [ [5, 3, 0, 0, 7, 0, 0, 0, 0], [6, 0, 0, 1, 9, 5, 0, 0, 0], [0, 9, 8, 0, 0, 0, 0, 6, 0], [8, 0, 0, 0, 6, 0, 0, 0, 3], [4, 0, 0, 8, 0, 3, 0, 0, 1], [7, 0, 0, 0, 2, 0, 0, 0, 6], [0, 6, 0, 0, 0, 0, 2, 8, 0], [0, 0, 0, 4, 1, 9, 0, 0, 5], [0, 0, 0, 0, 8, 0, 0, 7, 9] ] for i in range(9): for j in range(9): self.set_cell_value(i, j, example[i][j]) self.initial_board[i][j] = example[i][j] # 保存初始题目 self.status_label.config(text=\u0026#34;已加载示例数独\u0026#34;) def start_solving(self): if self.solving: return # 保存当前棋盘状态作为初始题目 for i in range(9): for j in range(9): if self.board[i][j] != 0: self.initial_board[i][j] = self.board[i][j] # 更新格子颜色为初始题目颜色 self.cells[i][j].config(bg=self.get_cell_color(i, j)) # 在新线程中运行求解过程，避免阻塞UI thread = threading.Thread(target=self.solve_thread) thread.daemon = True thread.start() def direct_solve(self): \u0026#34;\u0026#34;\u0026#34;直接求解，不演示中间过程\u0026#34;\u0026#34;\u0026#34; if self.solving: return # 保存当前棋盘状态作为初始题目 for i in range(9): for j in range(9): if self.board[i][j] != 0: self.initial_board[i][j] = self.board[i][j] # 在新线程中运行直接求解过程 thread = threading.Thread(target=self.direct_solve_thread) thread.daemon = True thread.start() def solve_thread(self): \u0026#34;\u0026#34;\u0026#34;演示求解线程\u0026#34;\u0026#34;\u0026#34; self.solving = True self.status_label.config(text=\u0026#34;开始求解...\u0026#34;) self.step_count = 0 self.algorithm_time = 0 # 禁用控制按钮 self.toggle_buttons(False) # 开始求解 start_time = time.time() solved = self.solve_sudoku() end_time = time.time() self.solving = False # 启用控制按钮 self.toggle_buttons(True) total_time = end_time - start_time if solved: self.status_label.config(text=f\u0026#34;求解完成! 总用时: {total_time:.2f}秒\u0026#34;) else: self.status_label.config(text=\u0026#34;无解!\u0026#34;) def direct_solve_thread(self): \u0026#34;\u0026#34;\u0026#34;直接求解线程\u0026#34;\u0026#34;\u0026#34; self.solving = True self.status_label.config(text=\u0026#34;直接求解中...\u0026#34;) # 禁用控制按钮 self.toggle_buttons(False) # 开始直接求解 start_time = time.time() solved = self.solve_sudoku_direct() end_time = time.time() self.solving = False # 启用控制按钮 self.toggle_buttons(True) total_time = end_time - start_time if solved: # 更新UI显示最终结果 self.update_final_board() self.status_label.config(text=f\u0026#34;直接求解完成! 用时: {total_time:.2f}秒\u0026#34;) else: self.status_label.config(text=\u0026#34;无解!\u0026#34;) def solve_sudoku_direct(self): \u0026#34;\u0026#34;\u0026#34;直接求解数独，不更新UI，不延迟\u0026#34;\u0026#34;\u0026#34; # 创建棋盘副本 board_copy = [row[:] for row in self.board] # 直接求解 solved = self.solve_direct(board_copy) # 如果求解成功，更新棋盘 if solved: self.board = board_copy return solved def solve_direct(self, board): \u0026#34;\u0026#34;\u0026#34;直接求解数独的递归函数\u0026#34;\u0026#34;\u0026#34; empty_cell = self.find_empty_cell_direct(board) if not empty_cell: return True # 没有空格子，求解完成 row, col = empty_cell for num in range(1, 10): if self.is_valid_direct(board, row, col, num): # 尝试填入数字 board[row][col] = num # 递归求解 if self.solve_direct(board): return True # 回溯 board[row][col] = 0 return False def find_empty_cell_direct(self, board): \u0026#34;\u0026#34;\u0026#34;在直接求解模式下查找空格子\u0026#34;\u0026#34;\u0026#34; for i in range(9): for j in range(9): if board[i][j] == 0: return (i, j) return None def is_valid_direct(self, board, row, col, num): \u0026#34;\u0026#34;\u0026#34;在直接求解模式下检查数字是否有效\u0026#34;\u0026#34;\u0026#34; # 检查行 for i in range(9): if board[row][i] == num: return False # 检查列 for i in range(9): if board[i][col] == num: return False # 检查3x3宫格 start_row, start_col = 3 * (row // 3), 3 * (col // 3) for i in range(3): for j in range(3): if board[start_row + i][start_col + j] == num: return False return True def update_final_board(self): \u0026#34;\u0026#34;\u0026#34;更新UI显示最终结果\u0026#34;\u0026#34;\u0026#34; for i in range(9): for j in range(9): value = self.board[i][j] if value != 0: self.cells[i][j].delete(0, tk.END) self.cells[i][j].insert(0, str(value)) # 如果是算法填入的数字，显示为浅蓝色 if self.initial_board[i][j] == 0: self.cells[i][j].config(bg=\u0026#34;#c0e0ff\u0026#34;) def toggle_buttons(self, state): state_str = \u0026#34;normal\u0026#34; if state else \u0026#34;disabled\u0026#34; for widget in self.root.winfo_children(): for child in widget.winfo_children(): if isinstance(child, ttk.Frame): for button in child.winfo_children(): if isinstance(button, ttk.Button): button.config(state=state_str) def is_valid(self, board, row, col, num): # 检查行 for i in range(9): if board[row][i] == num: return False # 检查列 for i in range(9): if board[i][col] == num: return False # 检查3x3宫格 start_row, start_col = 3 * (row // 3), 3 * (col // 3) for i in range(3): for j in range(3): if board[start_row + i][start_col + j] == num: return False return True def solve_sudoku(self): # 使用回溯法求解数独 empty_cell = self.find_empty_cell() if not empty_cell: return True # 没有空格子，求解完成 row, col = empty_cell for num in range(1, 10): # 记录算法开始时间 algo_start = time.time() if self.is_valid(self.board, row, col, num): # 尝试填入数字 self.board[row][col] = num self.step_count += 1 # 更新UI - 确保格子显示为绿色 self.root.after(0, lambda r=row, c=col, v=num: self.update_cell_ui(r, c, v, \u0026#34;try\u0026#34;)) # 记录算法结束时间 algo_end = time.time() self.algorithm_time += (algo_end - algo_start) self.update_stats() # 延迟用于演示 time.sleep(self.speed / 1000.0) # 递归求解 if self.solve_sudoku(): return True # 回溯 algo_start = time.time() self.board[row][col] = 0 self.step_count += 1 # 更新UI self.root.after(0, lambda r=row, c=col, v=0: self.update_cell_ui(r, c, v, \u0026#34;backtrack\u0026#34;)) # 记录算法结束时间 algo_end = time.time() self.algorithm_time += (algo_end - algo_start) self.update_stats() # 延迟用于演示 time.sleep(self.speed / 1000.0) else: # 即使数字无效，也记录算法时间 algo_end = time.time() self.algorithm_time += (algo_end - algo_start) self.update_stats() return False def find_empty_cell(self): for i in range(9): for j in range(9): if self.board[i][j] == 0: return (i, j) return None def update_cell_ui(self, row, col, value, action_type): # 如果是初始题目格子，保持紫色，不改变 if self.initial_board[row][col] != 0: return if value == 0: self.cells[row][col].delete(0, tk.END) if action_type == \u0026#34;backtrack\u0026#34;: self.cells[row][col].config(bg=\u0026#34;#ffcccc\u0026#34;) # 回溯时显示红色 # 立即更新UI self.root.update_idletasks() self.root.update() else: self.cells[row][col].delete(0, tk.END) self.cells[row][col].insert(0, str(value)) if action_type == \u0026#34;try\u0026#34;: self.cells[row][col].config(bg=\u0026#34;#ccffcc\u0026#34;) # 尝试时显示绿色 # 立即更新UI self.root.update_idletasks() self.root.update() def update_stats(self): if hasattr(self, \u0026#39;stats_label\u0026#39;) and self.stats_label is not None: self.root.after(0, lambda: self.stats_label.config( text=f\u0026#34;步骤: {self.step_count} | 算法时间: {self.algorithm_time:.3f}s\u0026#34;)) def main(): root = tk.Tk() app = SudokuSolver(root) root.mainloop() if __name__ == \u0026#34;__main__\u0026#34;: main() 执行结果 ","date":"2025-11-02T16:47:32+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E6%95%B0%E7%8B%AC/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E6%95%B0%E7%8B%AC/","title":"数独"},{"content":"井字棋 问题 井字棋是在一个九宫格中依次填入O或X，当横、竖、斜出现三个相同棋子时即获胜。以O先下为例，统计各自胜场和平局数。\n完全穷举 讨论所有情况的胜率。\n程序 class TicTacToeCompleteExhaustive: def __init__(self): self.total_states = 0 self.o_wins = 0 self.x_wins = 0 self.draws = 0 self.unfinished = 0 self.total_moves = 0 # 胜利条件 self.win_conditions = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], # 横线 [0, 3, 6], [1, 4, 7], [2, 5, 8], # 竖线 [0, 4, 8], [2, 4, 6] # 对角线 ] def is_win(self, state, player): \u0026#34;\u0026#34;\u0026#34;判断某玩家是否获胜\u0026#34;\u0026#34;\u0026#34; for condition in self.win_conditions: if all(state[i] == player for i in condition): return True return False def is_draw(self, state): \u0026#34;\u0026#34;\u0026#34;判断是否平局\u0026#34;\u0026#34;\u0026#34; return 0 not in state and not self.is_win(state, 1) and not self.is_win(state, 2) def is_unfinished(self, state): \u0026#34;\u0026#34;\u0026#34;判断是否未结束\u0026#34;\u0026#34;\u0026#34; return 0 in state and not self.is_win(state, 1) and not self.is_win(state, 2) def analyze_state(self, state): \u0026#34;\u0026#34;\u0026#34;分析一个状态并更新统计\u0026#34;\u0026#34;\u0026#34; self.total_states += 1 # 计算当前状态的步数（已下的棋子数） moves_in_state = state.count(1) + state.count(2) self.total_moves += moves_in_state if self.is_win(state, 1): # O赢 self.o_wins += 1 elif self.is_win(state, 2): # X赢 self.x_wins += 1 elif self.is_draw(state): # 平局 self.draws += 1 elif self.is_unfinished(state): # 未结束 self.unfinished += 1 def dfs(self, state, is_o_turn): \u0026#34;\u0026#34;\u0026#34;深度优先搜索所有可能的局面（完全穷举）\u0026#34;\u0026#34;\u0026#34; # 分析当前状态 self.analyze_state(state) # 如果游戏结束，不再继续搜索 if self.is_win(state, 1) or self.is_win(state, 2) or self.is_draw(state): return # 继续搜索下一步 for i in range(9): if state[i] == 0: state[i] = 1 if is_o_turn else 2 self.dfs(state, not is_o_turn) state[i] = 0 def analyze_all_states(self): \u0026#34;\u0026#34;\u0026#34;分析所有可能的游戏局面（完全穷举）\u0026#34;\u0026#34;\u0026#34; print(\u0026#34;开始分析井字棋所有可能局面（完全穷举）...\u0026#34;) initial_state = [0] * 9 # 0表示空，1表示O，2表示X self.dfs(initial_state, True) # O先手 # 输出结果 print(\u0026#34;\\n=== 井字棋穷举分析结果（完全穷举）===\u0026#34;) print(f\u0026#34;总局面数: {self.total_states:,}\u0026#34;) print(f\u0026#34;O 获胜局面数: {self.o_wins:,}\u0026#34;) print(f\u0026#34;X 获胜局面数: {self.x_wins:,}\u0026#34;) print(f\u0026#34;平局局面数: {self.draws:,}\u0026#34;) print(f\u0026#34;未结束局面数: {self.unfinished:,}\u0026#34;) # 验证总数 total_calculated = self.o_wins + self.x_wins + self.draws + self.unfinished print(f\u0026#34;验证: {self.o_wins:,} + {self.x_wins:,} + {self.draws:,} + {self.unfinished:,} = {total_calculated:,}\u0026#34;) print(f\u0026#34;与实际总数对比: {total_calculated} = {self.total_states}\u0026#34;) print(\u0026#34;\\n=== 比例分析 ===\u0026#34;) print(f\u0026#34;O 胜率: {self.o_wins/self.total_states*100:.4f}%\u0026#34;) print(f\u0026#34;X 胜率: {self.x_wins/self.total_states*100:.4f}%\u0026#34;) print(f\u0026#34;平局率: {self.draws/self.total_states*100:.4f}%\u0026#34;) print(f\u0026#34;未结束率: {self.unfinished/self.total_states*100:.4f}%\u0026#34;) # 计算平均深度 if self.total_states \u0026gt; 0: avg_moves = self.total_moves / self.total_states print(f\u0026#34;\\n平均步数: {avg_moves:.2f}\u0026#34;) return self.total_states # 运行分析 if __name__ == \u0026#34;__main__\u0026#34;: analyzer = TicTacToeCompleteExhaustive() total = analyzer.analyze_all_states() 执行结果 完全去重 去除因为顺序和对称性导致的重复情况\n程序 import tkinter as tk from tkinter import messagebox, ttk import itertools class TicTacToeAnalyzer: def __init__(self): self.states = {} # 存储所有局面 self.unique_states = set() # 去重后的局面 self.state_list = [] # 状态列表，用于浏览 self.current_state_index = 0 # 当前浏览的状态索引 self.o_wins = 0 self.x_wins = 0 self.draws = 0 # 对称变换策略 self.symmetry_strategies = [ [0, 1, 2, 3, 4, 5, 6, 7, 8], # 原始 [2, 1, 0, 5, 4, 3, 8, 7, 6], # 垂直翻转 [6, 7, 8, 3, 4, 5, 0, 1, 2], # 水平翻转 [8, 5, 2, 7, 4, 1, 6, 3, 0], # 主对角线翻转 [0, 3, 6, 1, 4, 7, 2, 5, 8], # 副对角线翻转 [6, 3, 0, 7, 4, 1, 8, 5, 2], # 旋转90度 [8, 7, 6, 5, 4, 3, 2, 1, 0], # 旋转180度 [2, 5, 8, 1, 4, 7, 0, 3, 6], # 旋转270度 ] # 胜利条件 self.win_conditions = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], # 横线 [0, 3, 6], [1, 4, 7], [2, 5, 8], # 竖线 [0, 4, 8], [2, 4, 6] # 对角线 ] self.setup_gui() def setup_gui(self): self.root = tk.Tk() self.root.title(\u0026#34;井字棋穷举分析器\u0026#34;) self.root.geometry(\u0026#34;900x700\u0026#34;) # 控制面板 control_frame = tk.Frame(self.root) control_frame.pack(pady=10) tk.Button(control_frame, text=\u0026#34;开始分析\u0026#34;, command=self.start_analysis, bg=\u0026#34;lightblue\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 12)).pack(side=tk.LEFT, padx=5) tk.Button(control_frame, text=\u0026#34;重置\u0026#34;, command=self.reset, bg=\u0026#34;lightcoral\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 12)).pack(side=tk.LEFT, padx=5) # 统计信息 stats_frame = tk.Frame(self.root) stats_frame.pack(pady=10) self.stats_text = tk.Text(stats_frame, height=6, width=80, font=(\u0026#34;Courier\u0026#34;, 10)) self.stats_text.pack() # 进度条 self.progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=600, mode=\u0026#39;determinate\u0026#39;) self.progress.pack(pady=10) # 棋盘显示区域 board_frame = tk.Frame(self.root) board_frame.pack(pady=10) # 创建棋盘 self.canvas = tk.Canvas(board_frame, width=300, height=300, bg=\u0026#34;white\u0026#34;) self.canvas.pack() # 状态信息和导航 nav_frame = tk.Frame(self.root) nav_frame.pack(pady=10) # 状态信息 self.state_info = tk.Label(nav_frame, text=\u0026#34;请点击\u0026#39;开始分析\u0026#39;进行穷举分析\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 12), fg=\u0026#34;blue\u0026#34;) self.state_info.pack(pady=5) # 导航按钮 nav_buttons = tk.Frame(nav_frame) nav_buttons.pack(pady=5) tk.Button(nav_buttons, text=\u0026#34;第一个\u0026#34;, command=self.first_state, font=(\u0026#34;Arial\u0026#34;, 10)).pack(side=tk.LEFT, padx=5) tk.Button(nav_buttons, text=\u0026#34;上一个\u0026#34;, command=self.prev_state, font=(\u0026#34;Arial\u0026#34;, 10)).pack(side=tk.LEFT, padx=5) tk.Button(nav_buttons, text=\u0026#34;下一个\u0026#34;, command=self.next_state, font=(\u0026#34;Arial\u0026#34;, 10)).pack(side=tk.LEFT, padx=5) tk.Button(nav_buttons, text=\u0026#34;最后一个\u0026#34;, command=self.last_state, font=(\u0026#34;Arial\u0026#34;, 10)).pack(side=tk.LEFT, padx=5) # 结果显示区域 result_frame = tk.Frame(self.root) result_frame.pack(pady=10, fill=tk.BOTH, expand=True) # 创建滚动条 scrollbar = tk.Scrollbar(result_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.result_text = tk.Text(result_frame, height=10, width=80, font=(\u0026#34;Courier\u0026#34;, 9), yscrollcommand=scrollbar.set) self.result_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.result_text.yview) def draw_board(self, state): \u0026#34;\u0026#34;\u0026#34;绘制棋盘和棋子\u0026#34;\u0026#34;\u0026#34; self.canvas.delete(\u0026#34;all\u0026#34;) # 绘制棋盘网格 for i in range(1, 3): # 垂直线 self.canvas.create_line(i * 100, 0, i * 100, 300, width=2) # 水平线 self.canvas.create_line(0, i * 100, 300, i * 100, width=2) # 绘制棋子 for i in range(9): row = i // 3 col = i % 3 x = col * 100 + 50 y = row * 100 + 50 if state[i] == 1: # O self.canvas.create_oval(x-30, y-30, x+30, y+30, width=3, outline=\u0026#34;blue\u0026#34;) elif state[i] == 2: # X self.canvas.create_line(x-30, y-30, x+30, y+30, width=3, fill=\u0026#34;red\u0026#34;) self.canvas.create_line(x-30, y+30, x+30, y-30, width=3, fill=\u0026#34;red\u0026#34;) def rearrange(self, state, strategy): \u0026#34;\u0026#34;\u0026#34;根据对称策略重新排列局面\u0026#34;\u0026#34;\u0026#34; return [state[i] for i in strategy] def get_symmetric_states(self, state): \u0026#34;\u0026#34;\u0026#34;获取所有对称局面\u0026#34;\u0026#34;\u0026#34; symmetric_states = [] for strategy in self.symmetry_strategies: symmetric_states.append(self.rearrange(state, strategy)) return symmetric_states def is_win(self, state, player): \u0026#34;\u0026#34;\u0026#34;判断某玩家是否获胜\u0026#34;\u0026#34;\u0026#34; for condition in self.win_conditions: if all(state[i] == player for i in condition): return True return False def is_draw(self, state): \u0026#34;\u0026#34;\u0026#34;判断是否平局\u0026#34;\u0026#34;\u0026#34; return 0 not in state and not self.is_win(state, 1) and not self.is_win(state, 2) def get_state_key(self, state): \u0026#34;\u0026#34;\u0026#34;获取状态的规范表示（用于去重）\u0026#34;\u0026#34;\u0026#34; # 找到所有对称状态中最小的元组表示 symmetric_states = self.get_symmetric_states(state) min_state = min(tuple(s) for s in symmetric_states) return min_state def dfs(self, state, is_o_turn): \u0026#34;\u0026#34;\u0026#34;深度优先搜索所有可能的局面\u0026#34;\u0026#34;\u0026#34; state_key = self.get_state_key(state) # 如果已经访问过这个状态，直接返回 if state_key in self.states: return # 记录这个状态 self.states[state_key] = { \u0026#39;state\u0026#39;: state.copy(), \u0026#39;o_win\u0026#39;: self.is_win(state, 1), \u0026#39;x_win\u0026#39;: self.is_win(state, 2), \u0026#39;draw\u0026#39;: self.is_draw(state) } # 如果游戏结束，不再继续搜索 if self.is_win(state, 1) or self.is_win(state, 2) or self.is_draw(state): return # 继续搜索下一步 for i in range(9): if state[i] == 0: state[i] = 1 if is_o_turn else 2 self.dfs(state, not is_o_turn) state[i] = 0 def analyze_game(self): \u0026#34;\u0026#34;\u0026#34;分析所有可能的游戏局面\u0026#34;\u0026#34;\u0026#34; self.states = {} initial_state = [0] * 9 # 0表示空，1表示O，2表示X self.dfs(initial_state, True) # O先手 # 将状态转换为列表，用于浏览 self.state_list = list(self.states.keys()) # 统计结果 self.o_wins = sum(1 for data in self.states.values() if data[\u0026#39;o_win\u0026#39;]) self.x_wins = sum(1 for data in self.states.values() if data[\u0026#39;x_win\u0026#39;]) self.draws = sum(1 for data in self.states.values() if data[\u0026#39;draw\u0026#39;]) return len(self.states) def format_state(self, state): \u0026#34;\u0026#34;\u0026#34;格式化显示局面\u0026#34;\u0026#34;\u0026#34; symbols = {0: \u0026#39; \u0026#39;, 1: \u0026#39;O\u0026#39;, 2: \u0026#39;X\u0026#39;} lines = [] for i in range(0, 9, 3): line = \u0026#39; | \u0026#39;.join(symbols[state[i+j]] for j in range(3)) lines.append(line) return \u0026#39;\\n---------\\n\u0026#39;.join(lines) def display_current_state(self): \u0026#34;\u0026#34;\u0026#34;显示当前状态\u0026#34;\u0026#34;\u0026#34; if not self.state_list: return state_key = self.state_list[self.current_state_index] state_data = self.states[state_key] state = state_data[\u0026#39;state\u0026#39;] # 绘制棋盘 self.draw_board(state) # 更新状态信息 status = \u0026#34;O胜\u0026#34; if state_data[\u0026#39;o_win\u0026#39;] else \u0026#34;X胜\u0026#34; if state_data[\u0026#39;x_win\u0026#39;] else \u0026#34;平局\u0026#34; if state_data[\u0026#39;draw\u0026#39;] else \u0026#34;未结束\u0026#34; self.state_info.config(text=f\u0026#34;状态 {self.current_state_index+1}/{len(self.state_list)} - {status}\u0026#34;) # 在结果区域显示详细信息 self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, f\u0026#34;局面 #{self.current_state_index+1}:\\n\u0026#34;) self.result_text.insert(tk.END, self.format_state(state)) self.result_text.insert(tk.END, f\u0026#34;\\n\\n状态: {status}\u0026#34;) self.result_text.insert(tk.END, f\u0026#34;\\n局面代码: {state}\u0026#34;) def first_state(self): \u0026#34;\u0026#34;\u0026#34;跳转到第一个状态\u0026#34;\u0026#34;\u0026#34; if self.state_list: self.current_state_index = 0 self.display_current_state() def prev_state(self): \u0026#34;\u0026#34;\u0026#34;跳转到上一个状态\u0026#34;\u0026#34;\u0026#34; if self.state_list and self.current_state_index \u0026gt; 0: self.current_state_index -= 1 self.display_current_state() def next_state(self): \u0026#34;\u0026#34;\u0026#34;跳转到下一个状态\u0026#34;\u0026#34;\u0026#34; if self.state_list and self.current_state_index \u0026lt; len(self.state_list) - 1: self.current_state_index += 1 self.display_current_state() def last_state(self): \u0026#34;\u0026#34;\u0026#34;跳转到最后一个状态\u0026#34;\u0026#34;\u0026#34; if self.state_list: self.current_state_index = len(self.state_list) - 1 self.display_current_state() def display_results(self): \u0026#34;\u0026#34;\u0026#34;显示分析结果\u0026#34;\u0026#34;\u0026#34; total_states = len(self.states) stats_info = f\u0026#34;\u0026#34;\u0026#34; === 井字棋穷举分析结果 === 总局面数（去重后）: {total_states} O 获胜局面数: {self.o_wins} X 获胜局面数: {self.x_wins} 平局局面数: {self.draws} 未结束局面数: {total_states - self.o_wins - self.x_wins - self.draws} O 胜率: {self.o_wins/total_states*100:.2f}% X 胜率: {self.x_wins/total_states*100:.2f}% 平局率: {self.draws/total_states*100:.2f}% \u0026#34;\u0026#34;\u0026#34; self.stats_text.delete(1.0, tk.END) self.stats_text.insert(1.0, stats_info) # 显示第一个状态 if self.state_list: self.current_state_index = 0 self.display_current_state() def update_progress(self, value): \u0026#34;\u0026#34;\u0026#34;更新进度条\u0026#34;\u0026#34;\u0026#34; self.progress[\u0026#39;value\u0026#39;] = value self.root.update_idletasks() def start_analysis(self): \u0026#34;\u0026#34;\u0026#34;开始分析\u0026#34;\u0026#34;\u0026#34; self.stats_text.delete(1.0, tk.END) self.stats_text.insert(1.0, \u0026#34;正在分析井字棋所有可能局面...\\n这可能需要几秒钟...\u0026#34;) # 模拟进度更新 for i in range(101): self.update_progress(i) self.root.after(10) # 小延迟让界面更新 # 执行分析 total_states = self.analyze_game() # 显示结果 self.display_results() messagebox.showinfo(\u0026#34;分析完成\u0026#34;, f\u0026#34;分析完成！共找到 {total_states} 个不同的局面。\u0026#34;) def reset(self): \u0026#34;\u0026#34;\u0026#34;重置分析\u0026#34;\u0026#34;\u0026#34; self.states = {} self.state_list = [] self.current_state_index = 0 self.o_wins = 0 self.x_wins = 0 self.draws = 0 self.progress[\u0026#39;value\u0026#39;] = 0 self.stats_text.delete(1.0, tk.END) self.result_text.delete(1.0, tk.END) self.state_info.config(text=\u0026#34;请点击\u0026#39;开始分析\u0026#39;进行穷举分析\u0026#34;) self.canvas.delete(\u0026#34;all\u0026#34;) def run(self): \u0026#34;\u0026#34;\u0026#34;运行应用程序\u0026#34;\u0026#34;\u0026#34; self.root.mainloop() # 创建并运行分析器 if __name__ == \u0026#34;__main__\u0026#34;: analyzer = TicTacToeAnalyzer() analyzer.run() 执行结果 ","date":"2025-11-02T16:47:09+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E4%BA%95%E5%AD%97%E6%A3%8B/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E4%BA%95%E5%AD%97%E6%A3%8B/","title":"井字棋"},{"content":"密码破解 问题 使用暴力破解包含大小写字母和数字的四位密码。\n程序 import itertools import time import hashlib def generate_hashes(passwords): \u0026#34;\u0026#34;\u0026#34;生成密码的MD5哈希值\u0026#34;\u0026#34;\u0026#34; hashes = {} for pwd in passwords: hashes[pwd] = hashlib.md5(pwd.encode()).hexdigest() return hashes def crack_by_hash(target_hashes): \u0026#34;\u0026#34;\u0026#34;通过哈希值破解密码\u0026#34;\u0026#34;\u0026#34; characters = \u0026#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\u0026#39; print(\u0026#34;开始破解密码...\u0026#34;) start_time = time.time() attempts = 0 found_passwords = {} # 记录每个密码的破解信息 crack_info = {} # 生成所有1-3位字符组合 for length in range(1, 5): for candidate in itertools.product(characters, repeat=length): password_attempt = \u0026#39;\u0026#39;.join(candidate) attempts += 1 # 计算当前尝试密码的MD5哈希值 hash_attempt = hashlib.md5(password_attempt.encode()).hexdigest() # 检查哈希值是否匹配任何目标 for original_pwd, target_hash in target_hashes.items(): if hash_attempt == target_hash and original_pwd not in found_passwords: found_passwords[original_pwd] = password_attempt crack_time = time.time() - start_time crack_info[original_pwd] = { \u0026#39;attempts\u0026#39;: attempts, \u0026#39;time\u0026#39;: crack_time, \u0026#39;found_password\u0026#39;: password_attempt } print(f\u0026#34;✅ 破解成功: \u0026#39;{original_pwd}\u0026#39; -\u0026gt; \u0026#39;{password_attempt}\u0026#39;\u0026#34;) print(f\u0026#34; 尝试次数: {attempts}, 耗时: {crack_time:.4f}秒\u0026#34;) # 如果所有密码都已找到，提前结束 if len(found_passwords) == len(target_hashes): total_time = time.time() - start_time print(f\u0026#34;\\n所有密码破解完成!\u0026#34;) print(f\u0026#34;总尝试次数: {attempts}, 总耗时: {total_time:.4f}秒\u0026#34;) return crack_info total_time = time.time() - start_time print(f\u0026#34;\\n破解结束，找到 {len(found_passwords)}/{len(target_hashes)} 个密码\u0026#34;) print(f\u0026#34;总尝试次数: {attempts}, 总耗时: {total_time:.4f}秒\u0026#34;) return crack_info def main(): # 预存密码 - 代表最快和最后被遍历出来的密码 passwords = [\u0026#39;a\u0026#39;, \u0026#39;9\u0026#39;, \u0026#39;aa\u0026#39;, \u0026#39;99\u0026#39;, \u0026#39;aaa\u0026#39;, \u0026#39;999\u0026#39;, \u0026#39;aaaa\u0026#39;, \u0026#39;9999\u0026#39;] print(\u0026#34;=\u0026#34; * 60) print(\u0026#34; 密码哈希破解演示 (使用MD5)\u0026#34;) print(\u0026#34;=\u0026#34; * 60) # 生成并显示预存密码的哈希值 print(\u0026#34;\\n预存密码及其MD5哈希值:\u0026#34;) print(\u0026#34;-\u0026#34; * 80) hashes = generate_hashes(passwords) for pwd, h in hashes.items(): print(f\u0026#34;密码: \u0026#39;{pwd}\u0026#39; -\u0026gt; 哈希: {h}\u0026#34;) print(\u0026#34;-\u0026#34; * 80) # 显示字符集信息 characters = \u0026#39;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\u0026#39; print(f\u0026#34;\\n字符集: {characters}\u0026#34;) print(f\u0026#34;字符集大小: {len(characters)}\u0026#34;) print(f\u0026#34;1位密码组合数: {len(characters)}\u0026#34;) print(f\u0026#34;2位密码组合数: {len(characters)**2}\u0026#34;) print(f\u0026#34;3位密码组合数: {len(characters)**3}\u0026#34;) print(f\u0026#34;4位密码组合数: {len(characters)**4}\u0026#34;) print(f\u0026#34;总组合数: {len(characters) + len(characters)**2 + len(characters)**3 + len(characters)**4}\u0026#34;) # 开始破解 input(\u0026#34;\\n按回车键开始破解...\u0026#34;) # 破解密码（只通过哈希值比较） results = crack_by_hash(hashes) # 显示破解结果总结 print(\u0026#34;\\n\u0026#34; + \u0026#34;=\u0026#34; * 60) print(\u0026#34;破解结果总结:\u0026#34;) print(\u0026#34;-\u0026#34; * 60) # 按破解顺序显示 for pwd in passwords: if pwd in results: info = results[pwd] print(f\u0026#34;密码 \u0026#39;{pwd}\u0026#39;: {info[\u0026#39;attempts\u0026#39;]:6d} 次尝试, {info[\u0026#39;time\u0026#39;]:.4f} 秒\u0026#34;) else: print(f\u0026#34;密码 \u0026#39;{pwd}\u0026#39;: 未找到\u0026#34;) print(\u0026#34;=\u0026#34; * 60) if __name__ == \u0026#34;__main__\u0026#34;: main() 执行效果 ============================================================ 密码哈希破解演示 (使用MD5) ============================================================ 预存密码及其MD5哈希值: -------------------------------------------------------------------------------- 密码: \u0026#39;a\u0026#39; -\u0026gt; 哈希: 0cc175b9c0f1b6a831c399e269772661 密码: \u0026#39;9\u0026#39; -\u0026gt; 哈希: 45c48cce2e2d7fbdea1afc51c7c6ad26 密码: \u0026#39;aa\u0026#39; -\u0026gt; 哈希: 4124bc0a9335c27f086f24ba207a4912 密码: \u0026#39;99\u0026#39; -\u0026gt; 哈希: ac627ab1ccbdb62ec96e702f07f6425b 密码: \u0026#39;aaa\u0026#39; -\u0026gt; 哈希: 47bce5c74f589f4867dbd57e9ca9f808 密码: \u0026#39;999\u0026#39; -\u0026gt; 哈希: b706835de79a2b4e80506f582af3676a 密码: \u0026#39;aaaa\u0026#39; -\u0026gt; 哈希: 74b87337454200d4d33f80c4663dc5e5 密码: \u0026#39;9999\u0026#39; -\u0026gt; 哈希: fa246d0262c3925617b0c72bb20eeb1d -------------------------------------------------------------------------------- 字符集: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 字符集大小: 62 1位密码组合数: 62 2位密码组合数: 3844 3位密码组合数: 238328 4位密码组合数: 14776336 总组合数: 15018570 按回车键开始破解... 开始破解密码... ✅ 破解成功: \u0026#39;a\u0026#39; -\u0026gt; \u0026#39;a\u0026#39; 尝试次数: 1, 耗时: 0.0000秒 ✅ 破解成功: \u0026#39;9\u0026#39; -\u0026gt; \u0026#39;9\u0026#39; 尝试次数: 62, 耗时: 0.0006秒 ✅ 破解成功: \u0026#39;aa\u0026#39; -\u0026gt; \u0026#39;aa\u0026#39; 尝试次数: 63, 耗时: 0.0010秒 ✅ 破解成功: \u0026#39;99\u0026#39; -\u0026gt; \u0026#39;99\u0026#39; 尝试次数: 3906, 耗时: 0.0053秒 ✅ 破解成功: \u0026#39;aaa\u0026#39; -\u0026gt; \u0026#39;aaa\u0026#39; 尝试次数: 3907, 耗时: 0.0055秒 ✅ 破解成功: \u0026#39;999\u0026#39; -\u0026gt; \u0026#39;999\u0026#39; 尝试次数: 242234, 耗时: 0.2391秒 ✅ 破解成功: \u0026#39;aaaa\u0026#39; -\u0026gt; \u0026#39;aaaa\u0026#39; 尝试次数: 242235, 耗时: 0.2394秒 ✅ 破解成功: \u0026#39;9999\u0026#39; -\u0026gt; \u0026#39;9999\u0026#39; 尝试次数: 15018570, 耗时: 14.6867秒 所有密码破解完成! 总尝试次数: 15018570, 总耗时: 14.6869秒 ============================================================ 破解结果总结: ------------------------------------------------------------ 密码 \u0026#39;a\u0026#39;: 1 次尝试, 0.0000 秒 密码 \u0026#39;9\u0026#39;: 62 次尝试, 0.0006 秒 密码 \u0026#39;aa\u0026#39;: 63 次尝试, 0.0010 秒 密码 \u0026#39;99\u0026#39;: 3906 次尝试, 0.0053 秒 密码 \u0026#39;aaa\u0026#39;: 3907 次尝试, 0.0055 秒 密码 \u0026#39;999\u0026#39;: 242234 次尝试, 0.2391 秒 密码 \u0026#39;aaaa\u0026#39;: 242235 次尝试, 0.2394 秒 密码 \u0026#39;9999\u0026#39;: 15018570 次尝试, 14.6867 秒 ============================================================ ","date":"2025-11-02T16:41:22+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E5%AF%86%E7%A0%81%E7%A0%B4%E8%A7%A3/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E5%AF%86%E7%A0%81%E7%A0%B4%E8%A7%A3/","title":"密码破解"},{"content":"水仙花数 问题 三位数的水仙花数是指一个三位数的每个数字的立方和等于该数本身。求所有三位水仙花数。\n程序 for num in range(100, 1000): a = num // 100 # 百位 b = (num // 10) % 10 # 十位 c = num % 10 # 个位 if a**3 + b**3 + c**3 == num: print(num) 思想 穷举所有可能并打印合适的数。\n执行效果 ","date":"2025-11-02T16:37:50+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E6%B0%B4%E4%BB%99%E8%8A%B1%E6%95%B0/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E6%B0%B4%E4%BB%99%E8%8A%B1%E6%95%B0/","title":"水仙花数"},{"content":"百钱买百鸡 问题 鸡翁一，值钱五；鸡母一，值钱三；鸡雏三，值钱一；百钱买百鸡，则翁、母、雏各几何？\n程序 # 公鸡5文钱一只，母鸡3文钱一只，小鸡1文钱三只。用100文钱买100只鸡，问公鸡、母鸡、小鸡各多少只？ # 穷举法：遍历所有可能的公鸡、母鸡、小鸡数量，判断是否满足条件 for cock in range(0, 21): # 公鸡从0到20只 for hen in range(0, 34): # 母鸡从0到33只 chick = 100 - cock - hen # 小鸡数量 if chick % 3 == 0 and cock * 5 + hen * 3 + chick // 3 == 100: print(f\u0026#34;公鸡：{cock}只，母鸡：{hen}只，小鸡：{chick}只\u0026#34;) 思想 穷举所有可能，记录所有可能的情况。\n执行效果 ","date":"2025-11-02T16:31:42+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%99%BE%E9%92%B1%E4%B9%B0%E7%99%BE%E9%B8%A1/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%99%BE%E9%92%B1%E4%B9%B0%E7%99%BE%E9%B8%A1/","title":"百钱买百鸡"},{"content":"八皇后问题 问题 八皇后问题是一个以国际象棋为背景的问题：如何能够在8×8的国际象棋棋盘上放置八个皇后，使得任何一个皇后都无法直接吃掉其他的皇后？为了达到此目的，任两个皇后都不能处于同一条横行、纵列、正斜线或反斜线。使用穷举法列举所有可能。\n程序 import tkinter as tk from tkinter import messagebox import time import threading class EightQueensGUI: def __init__(self, root): self.root = root self.root.title(\u0026#34;八皇后问题演示\u0026#34;) self.root.geometry(\u0026#34;650x750\u0026#34;) self.board_size = 8 self.cell_size = 50 self.delay = 100 self.setup_ui() self.solutions = [] self.current_solution = 0 self.solving = False self.solve_thread = None def setup_ui(self): # 创建主框架 main_frame = tk.Frame(self.root) main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 创建画布 self.canvas = tk.Canvas(main_frame, width=500, height=500, bg=\u0026#39;white\u0026#39;) self.canvas.pack(pady=10) # 创建控制按钮框架 control_frame = tk.Frame(main_frame) control_frame.pack(pady=10, fill=tk.X) # 第一行按钮 btn_frame1 = tk.Frame(control_frame) btn_frame1.pack(pady=5) self.solve_btn = tk.Button(btn_frame1, text=\u0026#34;开始求解\u0026#34;, command=self.solve, width=10) self.solve_btn.pack(side=tk.LEFT, padx=5) self.next_btn = tk.Button(btn_frame1, text=\u0026#34;下一个解\u0026#34;, command=self.next_solution, width=10) self.next_btn.pack(side=tk.LEFT, padx=5) self.prev_btn = tk.Button(btn_frame1, text=\u0026#34;上一个解\u0026#34;, command=self.prev_solution, width=10) self.prev_btn.pack(side=tk.LEFT, padx=5) self.stop_btn = tk.Button(btn_frame1, text=\u0026#34;停止求解\u0026#34;, command=self.stop_solving, width=10) self.stop_btn.pack(side=tk.LEFT, padx=5) # 第二行控件 btn_frame2 = tk.Frame(control_frame) btn_frame2.pack(pady=5, fill=tk.X) # 速度控制 speed_frame = tk.Frame(btn_frame2) speed_frame.pack(side=tk.LEFT, padx=10) tk.Label(speed_frame, text=\u0026#34;速度:\u0026#34;).pack(side=tk.LEFT) self.speed_scale = tk.Scale(speed_frame, from_=10, to=500, orient=tk.HORIZONTAL, length=200, showvalue=True, command=self.update_delay) self.speed_scale.set(self.delay) self.speed_scale.pack(side=tk.LEFT, padx=5) # 解决方案选择 sol_frame = tk.Frame(btn_frame2) sol_frame.pack(side=tk.RIGHT, padx=10) tk.Label(sol_frame, text=\u0026#34;直接查看解:\u0026#34;).pack(side=tk.LEFT) self.solution_var = tk.StringVar(value=\u0026#34;1\u0026#34;) self.solution_spinbox = tk.Spinbox(sol_frame, from_=1, to=92, width=5, textvariable=self.solution_var, command=self.jump_to_solution) self.solution_spinbox.pack(side=tk.LEFT, padx=5) # 信息标签 self.info_label = tk.Label(main_frame, text=\u0026#34;点击\u0026#39;开始求解\u0026#39;来演示八皇后问题的解法\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 12), wraplength=600) self.info_label.pack(pady=5) # 状态标签 self.status_label = tk.Label(main_frame, text=\u0026#34;\u0026#34;, font=(\u0026#34;Arial\u0026#34;, 10), fg=\u0026#34;blue\u0026#34;) self.status_label.pack(pady=5) # 绘制初始棋盘 self.draw_board() def draw_board(self): self.canvas.delete(\u0026#34;all\u0026#34;) # 绘制棋盘 for row in range(self.board_size): for col in range(self.board_size): x1 = col * self.cell_size + 50 y1 = row * self.cell_size + 50 x2 = x1 + self.cell_size y2 = y1 + self.cell_size color = \u0026#34;#DEB887\u0026#34; if (row + col) % 2 == 0 else \u0026#34;#8B4513\u0026#34; self.canvas.create_rectangle(x1, y1, x2, y2, fill=color, outline=\u0026#34;black\u0026#34;) # 绘制行列标记 for i in range(self.board_size): # 行标记 (A-H) self.canvas.create_text(25, i * self.cell_size + 50 + self.cell_size // 2, text=chr(65 + i), font=(\u0026#34;Arial\u0026#34;, 12, \u0026#34;bold\u0026#34;)) # 列标记 (1-8) self.canvas.create_text(i * self.cell_size + 50 + self.cell_size // 2, 25, text=str(i + 1), font=(\u0026#34;Arial\u0026#34;, 12, \u0026#34;bold\u0026#34;)) def draw_queens(self, board): self.draw_board() for row in range(self.board_size): for col in range(self.board_size): if board[row][col] == 1: self.draw_queen(row, col) self.root.update() def draw_queen(self, row, col): x = col * self.cell_size + 50 + self.cell_size // 2 y = row * self.cell_size + 50 + self.cell_size // 2 radius = self.cell_size // 3 # 绘制皇后 self.canvas.create_oval(x - radius, y - radius, x + radius, y + radius, fill=\u0026#34;red\u0026#34;, outline=\u0026#34;darkred\u0026#34;, width=2) # 绘制皇冠 crown_points = [ x - radius//2, y - radius, x - radius//3, y - radius//2, x, y - radius, x + radius//3, y - radius//2, x + radius//2, y - radius ] self.canvas.create_polygon(crown_points, fill=\u0026#34;gold\u0026#34;, outline=\u0026#34;darkgoldenrod\u0026#34;, width=2) def is_safe(self, board, row, col): # 检查同一列 for i in range(row): if board[i][col] == 1: return False # 检查左上对角线 for i, j in zip(range(row-1, -1, -1), range(col-1, -1, -1)): if board[i][j] == 1: return False # 检查右上对角线 for i, j in zip(range(row-1, -1, -1), range(col+1, self.board_size)): if board[i][j] == 1: return False return True def solve_n_queens(self, board, row=0): if not self.solving: return False if row \u0026gt;= self.board_size: # 找到解，复制当前棋盘状态 solution = [row[:] for row in board] self.solutions.append(solution) self.update_status(f\u0026#34;找到第 {len(self.solutions)} 个解\u0026#34;) # 更新显示 self.draw_queens(board) time.sleep(self.delay / 1000) # 继续寻找更多解 return False for col in range(self.board_size): if not self.solving: return False if self.is_safe(board, row, col): board[row][col] = 1 # 更新显示 self.draw_queens(board) self.update_info(f\u0026#34;尝试在位置 ({chr(65+row)}{col+1}) 放置皇后\u0026#34;) time.sleep(self.delay / 1000) # 递归调用 self.solve_n_queens(board, row + 1) # 回溯 board[row][col] = 0 # 更新显示 self.draw_queens(board) self.update_info(f\u0026#34;回溯: 移除位置 ({chr(65+row)}{col+1}) 的皇后\u0026#34;) time.sleep(self.delay / 1000) return False def solve(self): self.solve_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.solutions = [] self.solving = True board = [[0 for _ in range(self.board_size)] for _ in range(self.board_size)] # 在新线程中运行求解过程 self.solve_thread = threading.Thread(target=self.solve_thread_func, args=(board,)) self.solve_thread.daemon = True self.solve_thread.start() def solve_thread_func(self, board): # 使用改进的算法找到所有解 self.find_all_solutions(board) if self.solving: if self.solutions: self.current_solution = 0 self.show_solution_info() self.update_status(f\u0026#34;求解完成! 共找到 {len(self.solutions)} 个解\u0026#34;) self.solution_spinbox.config(to=len(self.solutions)) self.solution_var.set(\u0026#34;1\u0026#34;) else: self.update_status(\u0026#34;未找到解!\u0026#34;) self.solve_btn.config(state=tk.NORMAL) self.stop_btn.config(state=tk.DISABLED) self.solving = False def find_all_solutions(self, board, row=0): \u0026#34;\u0026#34;\u0026#34;找到所有解的递归函数\u0026#34;\u0026#34;\u0026#34; if not self.solving: return if row \u0026gt;= self.board_size: # 找到一个解 solution = [row[:] for row in board] self.solutions.append(solution) self.update_status(f\u0026#34;找到第 {len(self.solutions)} 个解\u0026#34;) # 显示当前解 self.draw_queens(board) time.sleep(self.delay / 1000) return for col in range(self.board_size): if not self.solving: return if self.is_safe(board, row, col): board[row][col] = 1 # 更新显示 self.draw_queens(board) self.update_info(f\u0026#34;尝试在位置 ({chr(65+row)}{col+1}) 放置皇后\u0026#34;) time.sleep(self.delay / 1000) # 递归调用 self.find_all_solutions(board, row + 1) # 回溯 board[row][col] = 0 # 更新显示 self.draw_queens(board) self.update_info(f\u0026#34;回溯: 移除位置 ({chr(65+row)}{col+1}) 的皇后\u0026#34;) time.sleep(self.delay / 1000) def stop_solving(self): self.solving = False self.update_status(\u0026#34;求解已停止\u0026#34;) def show_solution_info(self): if self.solutions and 0 \u0026lt;= self.current_solution \u0026lt; len(self.solutions): solution = self.solutions[self.current_solution] self.draw_queens(solution) self.update_info(f\u0026#34;解 {self.current_solution + 1}/{len(self.solutions)}\u0026#34;) def next_solution(self): if self.solutions: self.current_solution = (self.current_solution + 1) % len(self.solutions) self.solution_var.set(str(self.current_solution + 1)) self.show_solution_info() def prev_solution(self): if self.solutions: self.current_solution = (self.current_solution - 1) % len(self.solutions) self.solution_var.set(str(self.current_solution + 1)) self.show_solution_info() def jump_to_solution(self): if self.solutions: try: sol_num = int(self.solution_var.get()) - 1 if 0 \u0026lt;= sol_num \u0026lt; len(self.solutions): self.current_solution = sol_num self.show_solution_info() except ValueError: pass def update_delay(self, value): self.delay = int(value) def update_info(self, text): self.info_label.config(text=text) self.root.update_idletasks() def update_status(self, text): self.status_label.config(text=text) self.root.update_idletasks() def main(): root = tk.Tk() app = EightQueensGUI(root) root.mainloop() if __name__ == \u0026#34;__main__\u0026#34;: main() 思想 该程序使用穷举法，并通过UI展示了完整的穷举过程，可以访问穷举结果。穷举中如果出现错误的情况就回溯。\n执行效果 ","date":"2025-11-02T16:02:36+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E5%85%AB%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E5%85%AB%E7%9A%87%E5%90%8E%E9%97%AE%E9%A2%98/","title":"八皇后问题"},{"content":"如何在hugo博客中添加mermaid支持 前言 对于很多hugo博客的主题，他们并没有直接填完关于mermaid语法的支持，这也就导致了我们制作的一些mermaid图表无法被渲染，可读性急剧下降。为此，为hugo添加mermaid支持就非常有必要了。\n在很多博客里面，我们可以看到可以通过添加短代码的方式来实现添加mermaid支持，但是通过这种方式支持的mermaid需要在markdown文件中添加短代码语法，类似于这样：\n{{\u0026lt; mermaid \u0026gt;}} graph LR A[开始] --\u0026gt; B{思考} B --\u0026gt;|是| C[行动] B --\u0026gt;|否| D[放弃] {{\u0026lt; /mermaid \u0026gt;}} 而不是：\n```markdown graph LR A[开始] --\u0026gt; B{思考} B --\u0026gt;|是| C[行动] B --\u0026gt;|否| D[放弃] ``` 这让页面组织会显得有点怪，而且这篇文章如果需要发到某些支持mermaid的markdown的平台上时就会产生混乱。\n为此我们可以选择通过hugo的partial功能来实现这个效果。\n创建局部模板文件 在layouts/partials目录下创建一个局部模板，我们将其命名为mermaid.html。\n然后将以下代码填入其中：\n{{ if .Params.mermaid }} \u0026lt;script type=\u0026#34;module\u0026#34;\u0026gt; import(\u0026#39;https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs\u0026#39;) .then((mermaidModule) =\u0026gt; { const mermaid = mermaidModule.default; mermaid.initialize({ startOnLoad: true, theme: \u0026#39;neutral\u0026#39;\t// 可以修改主题 }); Array.from(document.getElementsByClassName(\u0026#34;language-mermaid\u0026#34;)).forEach((el) =\u0026gt; { el.parentElement.outerHTML = `\u0026lt;div class=\u0026#34;mermaid\u0026#34;\u0026gt;${el.innerHTML}\u0026lt;/div\u0026gt;`; }); mermaid.init(undefined, \u0026#39;.mermaid\u0026#39;); }) .catch((error) =\u0026gt; { console.warn(\u0026#39;Mermaid 加载失败，图表将显示为代码块:\u0026#39;, error); }); \u0026lt;/script\u0026gt; {{ end }} 现在，我们就拥有了mermaid模块了，可以将其添加在一个界面模板中，以供加载。\n修改文章模板 将个人主题的文章模板single.html复制到根目录的layouts/_default文件夹中，然后在其中的合适位置，例如{{ .Content}}的下方，插入一行代码：\n{{- partial \u0026#34;mermaid.html\u0026#34; . -}} 这种先将文章模板复制再进行修改的方法，可以防止后来主题更新将我们的修改覆盖掉（相较于在主题文件里修改），同时也保证了原本主题的完整性（相较于不复制直接创建一个空白的single.html）。\n按需启用 如果你需要在某一个文章中启用mermaid，只需要在这个文章的Front Matter中添加mermaid: true，以此来声明需要加载mermaid即可。\n总结 通过这种方式，我们就实现了hugo博客对于mermaid的支持，本片博客就是通过这个方法实现的对mermaid的支持，以下是实现效果。\nflowchart TD A[开始集成Mermaid] --\u003e B[创建mermaid.html局部模板] B --\u003e C[在single.html中插入模板调用] C --\u003e D[在文章Front Matter中设置mermaid: true] D --\u003e E[在文章中使用mermaid代码块] E --\u003e F[完成集成]其实理论上，我们还可以用hooks来实现，但是我没有试验成功，所以就不在此赘述了。\n","date":"2025-10-31T20:52:57+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/%E5%A6%82%E4%BD%95%E5%9C%A8hugo%E5%8D%9A%E5%AE%A2%E4%B8%AD%E6%B7%BB%E5%8A%A0mermaid%E6%94%AF%E6%8C%81/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/%E5%A6%82%E4%BD%95%E5%9C%A8hugo%E5%8D%9A%E5%AE%A2%E4%B8%AD%E6%B7%BB%E5%8A%A0mermaid%E6%94%AF%E6%8C%81/","title":"如何在hugo博客中添加mermaid支持"},{"content":"WireShark抓包实验 目标 掌握TCP/IP网络体系结构的网络接口层、网络层、传输层和应用层典型协议（Ethernet II、IP、TCP、ICMP、HTTP）的分析。 分析常用即时通讯软件的通信过程。 实验内容 抓包PING Ping命令是一种常用的网络诊断工具，用于测试两台计算机之间的连通性。本次实验中我们通过Ping命令访问百度的网址，并对整个过程进行抓包分析。\n我们打开WireShark然后进入抓包界面，并开始抓包。\n我们打开命令行，然后输入指令ping www.baidu.com。\n分析DNS DNS查询报文：因为我们不知道百度的IP地址，所以我们使用域名进行访问，而这个过程中，Ping会向DNS服务器寻求百度的IP地址，然后DNS服务器会返回它的IP地址。我们可以在WireShark中可以发现这两个包。\n首先让我们分析一下请求包，首先我们可以从统计表中得知数据是从172.20.*.*这个网址发出去的，这个就是我们的IP地址，根据网段不难得知这是一个私网网址。根据Protocol可以得知这是一个DNS包，而通过Info可以得知本次DNS查询是一次标准查询，通讯ID号是0x1c2d，而其中的A表示查询记录类型是IPv4地址（因为我这里的网络没有通IPv6，如果是IPv6，则应该为AAAA)，而后面跟的就是我们想要查询IP的网址www.baidu.com。让我们看看报文内容。\n首先让我们看看Ethernet II，即以太网帧（其中不重要的内容还有隐私内容已被我打码，下同）：\n我们可以在其中找到我们的以太网帧的帧头，这其中包含了目标MAC地址和源MAC地址，以及帧里面的数据包类型。在WireShark中，我们可以看到右边是实际发送的数据，左边是其解析的含义。\n接着我们观察IPv4报头，这是属于网络层的内容：\n其中重要的就是我们的源IP地址和目标IP地址，这里我的主机也就是源IP是172.20.*.*，目标IP是DNS服务器61.139.2.69。不过我们也可以关注一下其它部分，首先开头的45中4代表IPv4，5代表IP首部长度为5x4=20字节。*Differentiated Services Field（区分服务字段）*则是用于改善流量转发效果的，此处DSCP=00(CS0)让路由器默认处理该数据包，不改变其优先级，而ECN=00(Not-ECT)则代表不支持ECN显式拥塞通知功能。而后的3b代表IP数据包的总长度为59字节。而后4c f8代表其标识符ID。再后面的0000则依次代表Flag(3bit)=0(可以分片但是没有分)，Fragment Offset(13bit)=0(片偏移量为0)。而后80代表该报文生存时间为128跳。后面的11代表报文所用协议为UDP。然后又是个0000，代表首部校验和为0，用于校验IP头部的完整性。再然后就是源IP地址和目标IP地址了，前面已经说过。\n再往下则是UDP的报文内容。\nUDP报文的内容包括源端口号（2byte）、目的端口号（2byte）、UDP报文长度（2byte）、校验和（2byte）。\n然后后面就是我们的DNS报文了，这一部分根本目的是为了顺利获取到域名对应的IP地址：\n其事务ID（Transaction ID）为0x1c2d，代表该查询的唯一标识，这个查询报文对应的响应报文其事务ID也会是这个。Flags的值为0x0100，则代表了这是一个标准（Opcode=0)查询（Response=0）且授权应答位（Authoritative，在左侧未标注，但是可以观察到Opcode和Truncated中间有一个0，这一位只有在响应报文中有用）和截断位（Truncated）均未设置（=0），期望递归位已设置（Recursion desire=1，这个可以递归解析器，而不需要客户端自己去遍历根、TLD等服务器）、可递归位未设置（Recursion available，在Recursion desire和Z中间有一个0，这一位只有在响应报文中有用），保留位为0（Z=0），应答认证位（Answer authenticated，在Z和Non-authenticated data中间有一个0，表示服务器已经验证了DNSSEC签名，并确认数据是经过认证的，即来自权威源且未被篡改，这个只在响应报文中有用），禁用DNSSEC验证位（CD，即是否需要禁用前面的AD验证，0表示没有禁用，不过因为这里是查询报文，所以WireShark中显示的是Non-Authenticated data）。之后是连续4个2字节的计数字段分析，Questions=1代表报文中包含一个查询问题，后续三个都是在响应报文中才会出现的数据。至于Queries中的数据其实就是查询部分了，域名占用了13个字节（包括点号），不过我们仔细查看报文其实可以发现，这里的点号是用计数的方式来表示实现的，例如最开始的03 77 77 77其实代表了www，而03其实代表的接下来的一个标签（也就是www）长度为3，后续的baidu是05，com 是 03也是同理，而最后面还需要跟一个00作为结束符表示后面没有标签了。再后面的00 01代表希望查询的IP类型是A，即IPv4。最后的00 01则代表Class是Internet类，即互联网，这是最常用的类别。至此我们对于DNS查询报文的分析完成。\nDNS响应报文：关于响应报文，前面的以太网帧头、IP报头、UDP协议包头都没有什么继续分析的必要了，因为本质上和查询报文没有区别，只是方向反过来而已。我们重点关注最后的DNS部分：\n首先事务ID和查询报文是一样的。其次，它的Flags中Response变为了1，代表是响应报文。Opcode为0000，则仍代表该查询是标准查询。授权应答位Authoritative=0，代表该服务器不是该域名的权威服务器。Truncated为0代表报文没有截断，完整返回。Recursion Desire为1代表客户端希望递归查询。Recursion Desire为1代表服务器支持递归查询，提供递归服务。Z是保留位为0。Answer Authenticated为0表示该答案部分没自由经过DNSSEC认证，Checking Disabled为0代表没有禁用DNSSEC认证。Questions为1表示查询部分包含1个问问题，Answer RRs为3表示有三个资源记录，剩下两个都是0代表没有授权服务器信息和没有附加信息，全是字面意思。Queries和查询报文一样。我们重点还是看Answers部分，这是这个响应报文的核心部分。我们从Answer RRs中可以知道，我们有三个答案记录，而第一个记录的Name是www.baidu.com，Type是CNAME，Class是Internet。我们应当注意的是，这里的Name在报文中显示的数据是c0 0c，这里其实是代表的一个类似于指针的设置，用于指示重复的域名以缩减报文传输量，c的二进制是1100，其中11其实代表的就是这是个指针，而后的00以及后面的12bit，共同组成了偏移量，这样它就指示到了从DNS报文的0字节开始，往后第12字节开始的那个域名，正是wwww.baidu.com，后面出现类似情况也是同理。然后我们注意这里的Type是CNAME，也就是说它其实是检测到一个别名了，而这个别名的值是www.a.shifen.com，这个是百度以前的一个广告系统的网址，看来百度是直接把它的www.baidu.com域名连接到这个域名上了，它还可以将其它域名的别名也设置成这个，这样未来如果需要迁移地址，只需要迁移这个域名的地址就可以了。然后我们可以查看下面这个域名，检测到两个IP，分别是183.2.172.177和183.2.172.17，看样子百度用了多IP轮询机制来实现负载均衡或者容灾备份。最终从我们的Ping命令效果来看，我们最终应该是选用了第一个，或者说当时第一个就可以联通，不需要使用第二个备用IP了。\n分析ICMP ICMP报文：在获取到DNS响应报文后，程序即获得了IP地址，此时即可以通过ICMP报文与服务器通信。PING命令会连续发送四条ICMP报文，并根据返回报文计算连通性性和通信时间，我们只需要分析其中一对即可。\nICMP报文由三部分组成：以太网帧头、IP报头和ICMP协议数据部分。其中前两者和DNS相比差不多，只是内容存在部分差异，我们就不重复详细分析了。重点针对ICMP协议数据部分进行讨论：\n我们首先观察Type=8，其代表了这是一个回显请求，即ping的发送端。而Code=0则代表了无特殊错误码，正常请求。Checksum=0x4d2c是一个校验字段，用于验证报文完整性，已校验通过。Identifier(BE)=1表示发送大端格式的进程标识符，通常是进程的ID或者PID，此处为1。Identifier(LE)=256表示小端格式的进程标识符，对应了BE的值，本质上其实就是同一个值。Sequence Number(BE)=47表示大端格式的序列号，LE=12032表示小端格式的序列号，本质上就是同一个值。而后的Data部分有32个字节，根据ASCII码不难发现，其实发送的内容本质就是abcdefghijklmnopqrstuvwabcdefghi，其目的其实就是为了填充数据，没有实际含义。接下来让我们查看响应报文：\n不难发现，其实内容也没有什么区别，只有Type变为了0，代表这是回显应答，即ping的响应端。校验和在值上发生了改变但是结果仍然正确没有问题。并且我们可以看到Response time=35.251ms，与我们在终端看到的35ms是对应的，这其实是抓包工具计算出的时间，并不在报文之中。\n分析HTTP HTTP报文：现在让我们分析一下方法百度的三次握手包。因为只是初次尝试，我们选择访问HTTP的百度。通过在命令行中运行nslookup baidu.com这个代码获取百度网页的ip地址。\n应答中出现了两个地址，我们选择访问第一个试试，在此之前记得打开Wireshark：\n看来没问题，然后我们在Wireshark中添加过滤来找到这个访问，输入ip.host == 39.156.70.37然后回车即可：\n然后我们就可以查看显示结果了：\n可以发现，不知道为何，我们这里出现了两个三次握手，不过下面有一个HTTP报文，可以通过它来分析哪一个才是我们需要的。其中关于以太网帧和IP数据包的部分仍然可以忽略，让我们观察TCP部分：\nTCP部分可以注意到其Source Port是59049，Destination Port是80，说明我们只需要找到上方同样通信Port是59049和80的报文就可以了，刚好有三个，可以组成三次握手。其它的都是一些传输信息，我们不需要太关注。HTTP报文中在TCP后面还有一个HTTP部分，这一部分其实就是该网页的一些基础信息，不同网页内容也不一样，这里我们也同样不关注。\n分析三次握手 三次握手报文：在三次握手中，我们主要需要关注的还是TCP部分。\n这是第一次握手的内容：\n我们可以注意到其Flags的SYN标志被置为1了，这是而ACK仍为0，所以这显然是第一次握手的内容。而Sequence Number初始为0，因为这是当前客户端的第一个报文。Acknowledgment Number被设置为0，因为这是第一次握手。Options中则设置了包括分块等一系列设置，这会在前两次握手中出现。\n现在我们观察第二次握手的报文：\n这一次我们可以看到Flags中的ACK被置为1了，而Syn也仍然为1，可以判断这是第二次握手。Sequence Number也仍然为0，因为这是服务器的第一个报文。而Acknowledgment Number则为1，因为已经接收到一次报文了。Options仍然有一些反馈设置。\n我们观察第三次握手报文：\n在这一次中，Flags中只有ACK被置为1，同时Sequence Number变成了1，Acknowledgment Number也变成了1，符合我们的发送接收情况。同时我们可以注意这一次没有Options了。\n至此，我们的三次握手分析完毕。\n分析微信 接下来我们简单分析一下微信的数据传输，首先我们需要确定微信通信的IP地址，这个过程非常简单，我们只需要打开资源管理器并选中网络部分，接着给文件传输助手发送一个比较大的文件，然后观察资源管理器中的变化就可以了。\n显然，我们获知了微信的IP地址，现在让我们在筛选器中填入ip.addr == 219.153.153.110就好了，接着我们会发现一大堆GQUIC报文：\n关于QUIC的介绍可以参考《QUIC协议详解》，而gQUIC则是谷歌的版本，与其相对的是IETF版本的iQUIC。\n我们通过前面的介绍还有名字不难发现微信使用的QUIC协议本质上其实就是一种UDP的传输层协议，在协议中通过QUIC将实际需要发送的UDP数据加密打包发送。\n总结 通过本次实验，我们完成了包括针对PING、三次握手、微信传输三个大流程中使用到的各个报文的抓包和协议解析，基本掌握了WireShark的操作方法和筛选技巧，以及针对具体应用（如微信）如何通过资源管理器进行限位，以快速找到通信IP地址实现抓包。\n","date":"2025-10-15T22:49:58+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/wireshark%E6%8A%93%E5%8C%85%E5%AE%9E%E9%AA%8C/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/wireshark%E6%8A%93%E5%8C%85%E5%AE%9E%E9%AA%8C/","title":"WireShark抓包实验"},{"content":"使用IIS组件搭建服务器（HTTPs） 目标 根据《使用IIS组件搭建服务器（Web）》，将原本HTTP网站改为HTTPS网站，注意你应该已经完成了该文章所述的所有步骤。\n步骤 创建签名 因为HTTPS需要安全签名，所以我们要创建一个，如果你需要将其应用于公网，也可以自行申请一个。不过我们只是实验，创建一个本地用就够了。\n在IIS管理器中，选择计算机名称（不是某个网站），然后在中间的功能视图中，双击服务器证书。\n在右侧选择“创建自签名证书”。\n输入证书名称，然后确定。\n绑定HTTPS 右键点击你原本的HTTP网站，然后选择编辑绑定。\n点击添加。\n类型选择https，SSL证书选择刚刚创建的证书，其它全部保持默认即可。然后点击确定。你网站的https就配置好了。\n测试 直接访问https://localhost，你可能收到浏览器的警告，这是因为我们的SSL是自己创建的，并不被公共认可，浏览器认为其可能不安全。此时强制访问即可。\n","date":"2025-10-05T19:28:38+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E4%BD%BF%E7%94%A8iis%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8https/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E4%BD%BF%E7%94%A8iis%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8https/","title":"使用IIS组件搭建服务器（HTTPs）"},{"content":"使用IIS组件搭建服务器（FTP） 目标 使用IIS组件搭建一个Web服务器。\n步骤 启用IIS功能 IIS在Windows中是默认关闭的，需要手动启用。\n打开控制面板，选择“程序”\n进入程序界面，选择“程序和功能”。\n进入程序和功能界面，选择“启用或关闭Windows功能”\n展开“Internet Information Services”，将“FTP服务器”中的所有选项勾上。\n点击确定后等待系统自动安装所需文件，完成后点击关闭即可。\n添加FTP站点 按Win+R，输入inetmgr，然后打开“IIS管理器”。在左侧连接框中展开计算机，然后右键点击站点，选择“添加FTP站点”。\n在信息填写界面，根据你的喜好输入一个网站名称，物理路径你可以根据需要在一个地址创建一个文件夹并选中，这会是你的FTP根目录，例如我的C:\\Workspace\\Test\\FTPService。你可以在这个路径下创建一些文件，你可以随意在文件中写一些东西，以便于等会测试用。之后点击下一页。\nIP地址选择“全部未分配”（这样我们可以直接使用环回地址127.0.0.1来进行测试），端口使用默认的21端口，SSL选择“无SSL”（因为我们没有SSL，而且只是测试用，没必要设置）。点击下一页。\n身份验证我们选择基本，这样需要输入用户名和密码，然后再授权中选择所有用户，并给他们权限。这里其实无所谓如何设置，因为只是我们自己测试使用。设置完成后点击完成即可。\n如无意外，你可以看到你左侧网站界面多了一个你刚才创建的FTP网站，点击它，中间的界面就会变成FTP的管理界面。\n使用命令行访问FTP 现在你已经可以访问FTP了，打开一个命令行，例如Powershell，然后导航到一个你希望下载文件的路径。例如C:\\Workspace\\Test\\FTPDownload。使用指令ftp 127.0.0.1访问FTP服务器。然后输入用户名和密码，这个用户名和密码就是你当前登录的Windows账户的用户名和登录密码。此时你就可以登录到FTP服务器中了，此时输入ls即可查看当前FTP服务器中的文件，使用get \u0026quot;文件名\u0026quot; [\u0026quot;重命名\u0026quot;]即可将获取文件。使用完毕后，输入quit即可退出访问。\n此时我们的路径下就获得了FTP中的文件。\n","date":"2025-10-05T19:28:30+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E4%BD%BF%E7%94%A8iis%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8ftp/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E4%BD%BF%E7%94%A8iis%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8ftp/","title":"使用IIS组件搭建服务器（FTP）"},{"content":"使用IIS组件搭建服务器（Web） 目标 使用IIS组件搭建一个Web服务器。\n步骤 启用IIS功能 IIS在Windows中是默认关闭的，需要手动启用。\n打开控制面板，选择“程序”\n进入程序界面，选择“程序和功能”。\n进入程序和功能界面，选择“启用或关闭Windows功能”\n勾选“Internet Information Services”旁边的复选框，这会默认勾选大部分子项，我们只需要这些默认的子项即可，不过你也可以检查一下是否和我的一样。注意下面的“可承载的Web核心”不需要勾选，这部分更加类似于一个可被编程调用的库，并非我们本次实验的目的。\n点击确定后等待系统自动安装所需文件，完成后点击关闭即可。\n创建网站文件夹和内容 在合适的地方创建一个文件夹用于管理你的网站，例如我的C:\\Workspace\\Test\\WebService。\n创建首页文件：在你创建的文件夹中创建一个名为index.html的文件，使用记事本或你的文本编辑器打开它并修改内容。你可以在其中添加一些简单的HTML内容，例如：\n\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;IIS Web服务测试\u0026lt;/title\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;meta lang=\u0026#34;zh-CN\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;这是IIS Web服务测试\u0026lt;/h1\u0026gt; \u0026lt;p\u0026gt;欢迎使用IIS Web服务\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 在IIS管理器中创建网站 现在使用IIS管理器来配置网站。按住Win+R，输入inetmgr，然后回车。也可以在开始菜单搜索“IIS”或“Internet Information Services(IIS)管理器”。\n展开左侧“连接”面板中计算机名称，然后右键点击站点，选择“添加网站”。\n接下来在打开的网站信息填写界面，根据你的喜好输入一个网站名称，物理路径选择你刚才创建的文件夹。其他的暂时可以默认不变。不过需要注意你的端口可能会冲突，此时可以修改你的端口为8080或其它未被占用的端口。点击确定。你可以参考我的填写内容。\n如无意外，你可以看到你的左侧网站界面多了一个你刚才创建的网站。点击它，中间的界面就会变成该网站的管理界面。\n测试网站 现在我们可以测试这个网站了，打开一个浏览器，输入地址并在其后添加你刚刚选择的端口号，例如http://localhost:8080，你就可以打开刚才你的index.html网站。实验成功。\n","date":"2025-10-05T19:28:13+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E4%BD%BF%E7%94%A8iis%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8web/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E4%BD%BF%E7%94%A8iis%E7%BB%84%E4%BB%B6%E6%90%AD%E5%BB%BA%E6%9C%8D%E5%8A%A1%E5%99%A8web/","title":"使用IIS组件搭建服务器（Web）"},{"content":"IPv4和IPv6比较 前言 IPv4和IPv6是当前互联网协议的两个主要版本，并基于它们的不同特点进行对比。\n特点 兼容性 IPv4：IPv4是互联网的基础，已经应用数十年，几乎所有现有网络设备、操作系统、应用程序都完美支持IPv4，兼容性极佳。 IPv6：尽管IPv6是未来趋势，但其普及率仍不及IPv4，部分老旧设备、操作系统或应用程序可能不完全支持IPv6，存在兼容性问题。 此外，兼容性也带来了过度成本问题，包括设备升级改造成本和技术学习成本。\n易用性 IPv4：地址采用点分十进制表示，格式简单，便于人类记忆、输入和配置。 IPv6：地址采用冒号分隔的十六进制表示，长度较长且复杂，不便于人类记忆和手动输入。 地址空间 IPv4：采用32位地址，理论地址总数约43亿个，但由于互联网高速发展，这点地址空间根本不够，甚至不足以全世界每人一个IP地址。这是IPv4最大的缺陷。尽管通过NAT技术可以缓解这一问题，但是这本质上破坏了端到端的通信原则，增加了网络复杂性，可能导致连接延迟，并使得网络故障排查和安全溯源更加困难。 IPv6：采用128位地址，理论上地址数量高达2的128次方个，足以满足未来物联网、智能设备等海量终端接入的需求，从根本上解决了地址枯竭问题，且无需NAT转换，保证了互联网端到端通信的特性，简化了网络结构，提高了通信效率和可靠性。 路由效率 IPv4：地址分配不均，导致路由表庞大且难以聚合，路由器查找路由的负担较重，影响数据转发效率。 IPv6： 地址层次结构清晰，具有良好的可聚合性，能有效压缩路由表规模，减轻路由器负担，加快路由查找和数据包转发速度。 安全性 IPv4：协议本身设计时未充分考虑安全性，其安全机制（如IPsec）是可选的附加功能，而非强制内置，使得网络更容易受到攻击。 将IPsec（IP安全协议）作为强制性标准集成在协议中，为数据传输提供端到端的加密和认证，大大增强了网络通信的安全性。 自动配置 IPv4：依赖于DHCP服务器，需要单独进行配置DHCP服务器。 IPv6：支持无状态地址自动配置（SLAAC），设备接入网络后可以自动获取IP地址，无需依赖DHCP服务器，简化了网络管理，尤其适合大规模设备部署。 总结 IPv4凭借其成熟和兼容性仍是当前的主流，但其地址枯竭和安全性的根本缺陷使其难以适应未来发展。IPv6则通过巨大的地址空间、内置安全、高效路由和自动配置等优势，成为解决当前互联网瓶颈的必然选择，是未来互联网发展的方向。目前，两者正处于长期共存的过渡阶段，最终IPv6将逐步取代IPv4成为主流。\n","date":"2025-10-05T19:07:56+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/ipv4%E5%92%8Cipv6%E6%AF%94%E8%BE%83/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/ipv4%E5%92%8Cipv6%E6%AF%94%E8%BE%83/","title":"IPv4和IPv6比较"},{"content":"ARP协议分析 ARP协议简介 ARP全称为“Address Resolution Protocol”（地址解析协议）。其功能是将IP地址解析转换为MAC地址。因为IP地址工作在网络层，MAC地址工作在数据链路层，通常IP地址用于局域网之外的数据转发，而MAC地址用于局域网内的数据通信。当一台主机需要向同一局域网内的另一台主机发送数据时，它虽然知道对方的IP地址，但是最终数据需要以帧的形式在物理网络上传输，这个传输过程依赖于MAC地址，故需要一种机制将目标IP地址翻译成的对应的MAC地址。\n为什么不能只用MAC或IP MAC地址工作在数据链路层，IP地址工作在网络层，它们其实负责的是不同板块的功能。MAC地址负责同一局域网内的数据定位，而IP则负责不同局域网之间的数据定位。如果只用MAC地址，因为MAC地址本身没有分层的结构，路由表将会变得极其庞大而难以管理。\n而如果只用IP地址，因为IP地址已经包含了过多功能，在对IP数据包进行处理的时候会相对复杂，且路由器本身成本也更高，相比于使用一台路由器来管理，远不如用一台有硬件级MAC转发电路的交换机来得更为高效和划算，而且这也会导致路由器的角色混乱和性能下降。\nARP工作流程 ARP的工作流程简单讲就是先广播询问，后单播应答。\n假设两台设备在同一局域网内，A设备知道B设备的IP地址，但不知道它的MAC地址，此时就可以启动ARP协议：\n检查缓存表：主机A先检查自己的ARP缓存表（该缓存表可以避免重复广播），查看是否已有主机B的MAC地址。如果存在，则直接使用该地址构造数据帧。如果不存在，则进入ARP请求过程。 广播ARP请求：主机A在局域网内广播一个ARP请求报文，该报文包含主机A的IP和MAC，以及主机B的IP，而该报文的目标MAC地址设置为FF:FF:FF:FF:FF:FF用于广播，本地广播域内所有设备将会接收到这个请求。 单播ARP响应：只有主机B接收到广播并通过IP地址确定了该广播询问的是自己的MAC地址后，向主机A发送一个单播的ARP应答报文，里面包含了自己的MAC地址。 更新ARP缓存：主机B在接收到ARP请求后，就将主机A的MAC地址存入自己的ARP缓存。主机A在接受到主机B的应答报文后，也将主机B的MAC地址存入自己的ARP缓存。 正常通信：现在双方都知道了对方的MAC地址，就可以开始正常通信了，而在一段时间内，一因为双方ARP缓存都有对方的MAC地址，也无需要再次广播。 ARP报文格式 ARP时一个独立的三层协议，所以ARP报文在数据链路层不需要经过IP协议的封装，而是直接生成自己的报文。ARP报文可以分为ARP请求报文和ARP应答报文两种，但是它们的格式可以统一为同一种格式：\n名称 大小（字节） 功能 硬件类型 2 表示ARP报文可以在哪种网络上传输，值为1时表示以太网地址。 上层协议类型 2 表示硬件地址要映射的协议地址类型，映射IP地址时的值为0x0800。 MAC地址长度 1 标识MAC地址长度，以字节为单位，此处为6。 IP地址长度 1 标识IP得知长度，以字节为单位，此处为4。 操作类型 2 指定本次ARP报文类型。1标识ARP请求报文，2标识ARP应答报文。 源MAC地址 6 发送方的MAC地址。 源IP地址 4 发送方的IP地址。 目的MAC地址 6 接收方的MAC地址。若为请求报文，因为当前不知道该地址，则此时全为0，即00:00:00:00:00:00，表示任意地址。 目的IP地址 4 接受方的IP地址。 ARP帧格式 ARP报文并非直接在网络层发送和使用的，还需要向下传输到数据链路层，此时需要再次封装。以以太网为例，ARP报文传输到以太网数据链路层后会形成ARP帧，ARP帧是在ARP报文前面添加了一个以太网帧头，以太网帧头如下表所示：\n名称 大小（字节） 功能 目标MAC地址 6 接收方的MAC地址。若为请求报文，则为广播帧，设置为广播地址FF:FF:FF:FF:FF:FF。 源MAC地址 6 发送方的MAC地址。 帧类型 2 表示帧封装的上层协议，ARP帧封装的ARP报文，所以此处填ARP的协议号0x0806。 ","date":"2025-09-27T15:07:00+08:00","image":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/arp%E5%8D%8F%E8%AE%AE%E5%88%86%E6%9E%90/cover.jpg","permalink":"/posts/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/arp%E5%8D%8F%E8%AE%AE%E5%88%86%E6%9E%90/","title":"ARP协议分析"},{"content":"USB基础概念 📝 备注 在学习USB开发的时候，因为涉及到很多未接触过的概念以及本人存在的一些思维定势，导致在学习过程中遇到了很多问题。为了防止以后遗忘，也为后人学习提供一些参考思路，现将USB的一些基础概念整理成文。本文的核心目标是能够帮助初学者辅助理解USB的编程。\n🚨 注意 因个人实际学习需要，本文内容主要关注USB 2.0，且对于OTG内容探讨较少，另外虽然会涉及物理层面的内容，但是在本文中不会过于详细探讨。此外因本人并非专业USB开发人员，个人水平有限，内容可能存在遗漏或错误，欢迎指正。\nUSB基础概念 USB组织架构 系统物理架构 系统逻辑组织 单设备逻辑组织 USB数据传输 设计概念 包 事务 传输 总结 参考资料 USB组织架构 系统物理架构 一个拥有USB功能的芯片中通常至少有一个USB控制器（USB Controller）。若该USB控制器可以运行在主机模式（HOST MODE），则其内部拥有一个根集线器（ROOT HUB），通过根集线器可以延伸出一个或多个下行端口（Downstream Port）。若该USB控制器只能运行在设备模式（DEVICE MODE），则其不存在根集线器，并仅提供一个上行端口（Upstream Port）。\n每个端口（Port）至少有两根数据传输线D+/D-（以USB 2.0为例）。与I2C这类单端信号总线不同，**D+/D-**为差分数据线，它们传输大小相等、相位相反的信号，并且每个端口采用一对一连接，进行半双工通信。\n部分芯片的USB控制器支持OTG（On-The-Go）功能，则该设备同时支持主机模式和设备模式，并可以在使用过程中进行切换。OTG接口通常还需要多一根ID引脚（ID Pin）来决定默认角色：当ID引脚接地时，设备默认作为主机（A-device）；当ID引脚悬空时，设备默认作为从设备（B-device）。对于一个实际的USB物理端口，还应有VCC和GND两根供电线。\n为了实现设备检测和传输速度识别，通过对数据传输线D+/D-的上下拉电阻来实现。USB主机的D+/D-两根线均使用15KΩ电阻下拉至GND。对于全速（Full Speed）或高速（High Speed）的USB设备，则使用1.5KΩ电阻将D+上拉至3.3V。对于低速（Low Speed）的USB设备，则使用1.5KΩ电阻将**D-上拉至3.3V。主机通过检测D+/D-**引脚的电平来判断是否有设备接入以及接入设备的初始工作速度。高速设备在连接初期会表现为全速设备，在与主机完成握手协商后，再切换到高速模式。\n系统逻辑组织 USB（通用串行总线）的逻辑结构为一种分层星型结构，也称树形结构。\n一个USB通信系统中有且仅有一个USB主机（USB HOST），系统中所有的通信均由主机发起和控制，它是这个树形结构的根节点。主机通过其内部的根集线器，在物理上会提供一个或多个下行端口（Downstream Port）。下行端口可用于连接USB 设备或USB 集线器。\n一个USB通信系统中可以有多个USB设备（USB DEVICE）。设备不能主动与主机或其他设备通信，必须等待主机轮询，它们是这个树形结构的叶节点。\n一个USB通信系统中可以有多个USB集线器（USB HUB）。一个USB集线器通常有一个上行端口（Upstream Port）和多个下行端口（Downstream Port）。其上行端口连接USB主机或上层集线器的下行端口，其下行端口可以连接USB设备或下层集线器。在通信过程中，集线器扮演着双重角色：对于上层而言，它是一个USB设备；对于下层而言，它扮演着简化版USB主机的角色（负责端口管理和信号中继）。尽管USB集线器可以层层级联，但USB协议规定这种拓扑的最大深度不能超过7层。USB集线器是树形结构的分支节点。\n不过对于USB主机而言，在逻辑拓扑上所有的USB设备都是和USB主机直接相连的，通过访问不同的地址即可实现访问任意设备。\n对于一个USB通信系统，包括USB集线器在内的所有设备不能超过127个，因为USB的寻址系统使用7位地址，最多可提供127个设备地址（地址0保留用于初始配置）。\n单设备逻辑组织 对于某一个具体的USB设备而言，其在逻辑组织上存在一个四层组织，从上到下分别是：设备、配置、接口、端点，每个层次都有对应的描述符可用于发送给USB主机，在设备插入后进行发送用于设备的枚举。\n🚨 注意 在下方的介绍中我会提供描述符的结构示例，但是并不会详细介绍每项具体是做什么的，事实上如果只是为了目前的初步学习，你也没必要认真看每一项，通常情况如果需要配置描述符的时候再去查就可以了。\n设备（设备描述符）：用于描述这是一个怎样的设备。里面包含了厂商ID（VID）、产品ID（PID），用于唯一标识设备型号，操作系统可以用它来寻找合适的驱动程序。除此之外，还可以在里面设置该设备的设备类 (Class)、子类 (SubClass)、协议 (Protocol)，用于粗略指出设备的类型（如果是复合设备，即拥有多种功能的设备，则不建议在设备描述符里面明确这一部分）。以下是设备描述符的结构：\nstruct _DEVICE_DESCRIPTOR_STRUCT { BYTE bLength; //设备描述符的字节数大小，为0x12 BYTE bDescriptorType; //描述符类型编号，为0x01 WORD bcdUSB; //USB版本号,表示了本设备能适用于那种协议，例如 0x0200 代表 USB 2.0 BYTE bDeviceClass; //USB分配的设备类代码，0x01~0xfe为标准设备类，0xff为厂商自定义类型 BYTE bDeviceSubClass; //usb分配的子类代码，同上，值由USB规定和分配的 BYTE bDeviceProtocol; //USB分配的设备协议代码，同上 //上面三个字段的配置，如果设备类定义在接口描述符中，则设备描述符中的这些字段应设置为 0x00 BYTE bMaxPacketSize0; //端点0的最大包长 WORD idVendor; //厂商编号 WORD idProduct; //产品编号 WORD bcdDevice; //设备出厂编号 BYTE iManufacturer; //描述厂商字符串的索引 BYTE iProduct; //描述产品字符串的索引 BYTE iSerialNumber; //描述设备序列号字符串的索引 BYTE bNumConfigurations;//可能的配置数量 } 配置（配置描述符）：代表设备的一种工作模式，一个设备可以有一个或多个配置，但是同一时间只能有一个配置是激活的。配置描述符会指定设备的供电方式、最大功耗、接口数量。切换配置会改变整个设备的功耗、接口等特性。通常情况，一个设备有一个配置就够了，不过在部分需要实现设备高低功耗切换或使用不同传输模式的情况下，就可以设置多个配置。以下是配置描述符的结构：\nstruct _CONFIGURATION_DESCRIPTOR_STRUCT { BYTE bLength; //配置描述符的字节数大小，固定为9字节 BYTE bDescriptorType; //描述符类型编号，为0x02 WORD wTotalLength; //此配置下所有描述符（配置、接口、端点等）的总长度 BYTE bNumInterface; //此配置所支持的接口数量 BYTE bConfigurationVale; //Set_Configuration命令需要的参数值 BYTE iConfiguration; //描述该配置的字符串的索引值 BYTE bmAttribute; //供电模式的选择,bit 6 表示自供电（1）还是总线供电（0），bit 5 表示是否支持远程唤醒。这两个是最关键的位。 BYTE MaxPower; //设备从总线提取的最大电流,单位是 2mA。例如，100 代表最大电流为 200mA。 }CONFIGURATION_DESCRIPTOR_STRUCT 接口（接口描述符）：用于将设备在逻辑上划分为多个独立的功能部分，每个接口通常对应主机上的一个驱动程序。通常如果需要设置复合设备，就可以通过设置多个不同接口来实现。例如一个设备同时拥有键盘功能和SD卡读卡器功能，那么它就可以定义两个接口，并修改两个接口的接口描述符，并将这两个接口分别设置为HID和MSC。以下是接口描述符的结构：\nstruct _INTERFACE_DESCRIPTOR_STRUCT { BYTE bLength; //设备描述符的字节数大小，为0x09 BYTE bDescriptorType; //描述符类型编号，为0x04 BYTE bInterfaceNumber; //接口的编号 BYTE bAlternateSetting;\t//备用的接口描述符编号，一个接口可以有多个“备用设置”（例如，一个音频接口可以有不同带宽的设置），主机可以通过 SetInterface 请求在它们之间动态切换。 BYTE bNumEndpoints; //该接口使用端点数，不包括端点0 BYTE bInterfaceClass; //接口类型 BYTE bInterfaceSubClass;//接口子类型 BYTE bInterfaceProtocol;//接口所遵循的协议 BYTE iInterface; //描述该接口的字符串索引值 }INTERFACE_DESCRIPTOR_STRUCT 不过随着复合设备越来越复杂，有时候需要将多个接口组合起来共同实现一个单一功能。比如一个视频会议摄像头，它拥有一个视频接口、一个音频接口、一个控制接口，但是它们共同实现的是“视频会议”这个单一的功能，需要加载一个共同的驱动，为此USB协议还引入了接口关联描述符（IAD）。可以在每个类要合并的接口描述符之前加一个接口关联描述符，将多个接口定义为一个类设备，从而让主机为这几个接口加载一个共同的驱动。以下是接口关联描述符的结构：\ntypedef struct _USB_INTERFACE_ASSOCIATION_DESCRIPTOR { UCHAR bLength; //长度为8 UCHAR bDescriptorType; //USB_INTERFACE_ASSOCIATION_DESCRIPTOR_TYPE，值为0x0b UCHAR bFirstInterface; //第一个接口编号 UCHAR bInterfaceCount; //接口总数量 UCHAR bFunctionClass; //视频接口类代码CC_VIDEO，值0x0E UCHAR bFunctionSubClass; //视频子类接口代码 SC_VIDEO_INTERFACE_COLLECTION,值为0x03 UCHAR bFunctionProtocol; //未用，必须为PC_PROTOCOL_UNDEFINED，值为0x00 UCHAR iFunction; //字符串描述符索引 } USB_INTERFACE_ASSOCIATION_DESCRIPTOR, *PUSB_INTERFACE_ASSOCIATION_DESCRIPTOR; 端点（端点描述符）：负责数据传输的硬件单元，它是主机与设备通信的最终目的地或源。端点可以被配置为USB规范的四种传输类型（控制传输、中断传输、同步传输、批量传输），每个端点只能有一个传输方向，一个设备最多可以有32个端点（16个IN端点和16个OUT端点），它们各自以0至15进行编号，其中端点0是一个特殊的双向端点，它可以是IN也可以是OUT，但共享一个端点号0。端点0通常仅用于枚举过程，且传输类型只能是控制传输。端点类似于一个缓冲区，因为程序并不能直接读写USB总线，且USB读写流程总是受主机控制，故主机发送给设备的数据，实际上是直接发送给一个OUT端点的，同理，当USB主机还未轮询到设备前，USB设备如果须发送数据，就应该把数据暂存在一个IN端点里面，当主机轮询时由设备的USB控制器将其发送至主机。需要注意的是IN和OUT是针对主机视角的。以下是端点描述符的结构：\nstruct _ENDPOIN_DESCRIPTOR_STRUCT { BYTE bLength; //设备描述符的字节数大小，为0x07 BYTE bDescriptorType; //描述符类型编号，为0x05 BYTE bEndpointAddress; //端点地址及输入输出属性，bit 7 表示方向（1=IN, 0=OUT），bit 3~0 表示端点号。 BYTE bmAttribute; //端点的传输类型属性， bits 1..0 表示传输类型（00=控制，01=同步，10=批量，11=中断）。对于同步端点，其他位有额外含义。 WORD wMaxPacketSize; //端点收、发的最大包的大小 BYTE bInterval; //主机查询端点的时间间隔 } ENDPOIN_DESCRIPTOR_STRUCT ； 通常情况，一个设备中可以有一个或多个配置（但每次只有一个配置是活动的），一个配置中可以有多个接口（当前配置下所有接口以及接口下的端点都是活动的），一个接口下可以有一个或多个端点。不过需要注意的是，在USB连接之后，只有端点0是可以通信的，只有当使用端点0枚举完成后，其它端点才可以开始通信。\nUSB数据传输 设计概念 USB在传输设计上和USART、SPI这类简单串行协议不同。为了保证传输速度、稳定性和准确性，USB采用了类似网络协议的分层设计，将所需传输的数据进行封装而非简单透传。为了更好理解这种设计，我们引用TCP/IP协议作为参考。\n注意：USB协议规范本身并没有明确划分这些层级，以下的类比是为了理解方便而采用的逻辑模型。\n应用层 (Application Layer)\nTCP/IP：如HTTP、FTP、SMTP等，负责处理特定应用程序的数据语义。\nUSB：如HID（人机接口设备）、MSC（大容量存储设备）、CDC（通信设备类）等设备类协议。它们定义了设备的功能以及数据格式的含义。\n传输层 (Transport Layer)\nTCP/IP：由TCP和UDP组成。TCP提供可靠、面向连接的传输服务；UDP提供无连接的、不可靠的传输服务。\nUSB：对应USB架构中的传输（Transfer）。它由控制传输、中断传输、同步传输和批量传输四种传输类型组成。它们负责在主机和设备的某个端点之间提供可靠、有时序保证的端到端数据传递。\n事务层 (Transaction Layer)\nTCP/IP：对应网络层（如IP），负责寻址和路由数据包到目标主机。\nUSB：由事务（Transaction） 组成，例如IN、OUT、SETUP等。一次传输（Transfer） 通常需要分解为多个事务。事务是主机与设备特定端点之间单次数据交换（例如：主机询问、设备发送数据、设备回复状态）的基本可靠单元。全局的“寻址”通过包含在每个事务中的设备地址和端点号来实现。\n数据链路层 (Data Link Layer)\nTCP/IP：如以太网协议，将数据包封装成帧，处理帧同步、错误校验等。\nUSB：由包（Packet） 组成，例如令牌包、数据包、握手包。协议将事务信息封装成包，并添加CRC校验等，为在物理线上传输做好准备。\n简单来讲，USB协议栈在处理数据时可以看作“应用 — 传输 — 事务 — 包”这样的逻辑结构。每次应用需要交换数据时，协议栈首先会根据类型（如批量）建立一次传输，然后将这次传输根据数据长度拆分成多个事务，而每个事务又由三个（有时是两个）包（令牌包、数据包、握手包）组成，以确保通信的时序和可靠性。\n这种分层是有意义的，现在让我们从设计USB协议的视角去思考为什么要这样设计？\n在明确我们只有D+/D-这样两根差分总线进行数据传输的物理设计，假设我们直接将所有数据进行透传，那么我们的一个USB就只能连接一个设备。但这与我们实际需求不同，我们需要更多的USB接口，但不可能为每个接口都添加一个USB控制器（这样成本太高了），为此我们需要对数据进行封，从而区分当前总线上传输的数据是属于哪一个设备的。一个简单的方法就是在数据的开头添加一个指向某一具体设备的地址信息，同时在结尾添加一个结束信号用于表示解除占用总线。\n但是如果只是这样简单地在首位添加数据，就存在这样一个问题，假设一个设备是实时传输设备，比如摄像头，我们无法得知它会在什么时候停止使用，此时为了保证实时性，就无法添加结束信号，这个设备就会长时间占用整个总线，而其它设备就无法使用了。很显然，在串行总线上解决多设备并行运行问题最简单的思想就是时分复用，为此我们可以将传输数据进行拆分，并在拆分的每个部分的前后添加上我们的地址信息和结束信号，此外还可以添加一些校验信息、时钟信息等，来保证每次传输的稳定性，而这样一个新的微型数据就是包。\n尽管我们有了包之后，已经拥有时分复用传输数据的基础了，但是整个总线仍然需要一个管理机制来确认每次总线该有哪个设备占用。因为USB上总是有一个主机和多个设备，并且这些设备都是服务于主机的，那么自然由主机来确定由谁来占用总线是最合理的。很显然最简单的方法就是让主机来决定，并且通过总线将这个决定告诉所有设备，即在最开始发送一个“现在我需要发送/接受设备X的数据”（也就是令牌包），通过这样的方式来提醒需要发送或接受数据的设备做好准备，而其他的设备应当停止发送数据。\n尽管我们在物理设计的时候已经尽量保证总线的稳定性了，但是现实中难免会出现一些意外产生丢包的情况，为了防止这样的情况出现，我们需要添加一个保证包传输可靠性的功能。一个比较好的方法就是接收方在收到“所需要的数据所在的包”（也就是数据包）后，向发送方发送一个表示“我已经收到了的包”（也就是握手包），否则就不发。这样发送方就可以知晓数据是否成功传输到目标，从而决定是否需要重新再发送一次。\n现在通过令牌包——数据包——握手包这样的过程，就可以保证一次通信的完整可靠性，这也就是事务的作用。\n不过对于嵌入式、上位机、驱动开发者而言，这些底层的内容其实并不是那么重要，因为整个过程其实都是在USB协议栈和硬件中完成了设计和定义，尽管在部分情况下为了性能优化可能需要进行一些额外的配置，但是这个配置的过程对于需要开发的程序逻辑而言是不重要的，应用程序关心的是完整的数据，而不关心这个程序是通过多少次事务完成的。应用需要直接关注的是通过什么样的方式来管理这次通信，是需要保证即时通信（例如摄像头），还是需要保证定期检测的通信（例如键盘），或者需要收发大量数据（例如U盘），也可能是配置设备的某些功能（例如设置鼠标宏），因为这些功能对于总线带宽的占用频率和大小存在非常大的差异。为此我们设计了四种传输模式，来分配传输的频率和占用带宽大小。控制传输用于突发、小量数据的传输。批量传输用于突发、大量数据的传输。中断传输用于周期性、小量数据的传输。等时传输用于周期性、大量数据的传输。而主机会根据设备不同的传输模式，来控制传输的频率和带宽。\n通过这样的控制设计之后，在应用层面，程序员只需专注于对数据的处理逻辑和算法即可，整个USB的功能也就完成了。\n包 包是USB总线上实际传输的二进制数据序列，是物理信号层面上最基本的单元。一个包的传输分为四个部分，从开始到结束依次为：\nSOP：用于告知包的开始。在物理层面是主机将差分信号线从空闲状态（J状态，对于全速/高速设备为D+高、D-低）跳变到K状态（D-高、D+低），通过这个跳变沿告知总线上所有接收器一个包即将开始。\nSYNC：用于完成时钟同步。在物理层面是发送了一串8位二进制序列00000001。因为USB在差分线上使用NRZI编码，传输这个序列会产生频繁的跳变，从而实现时钟同步，并让接收方确认之前的跳变是有效的SOP而非噪声干扰。\nPacket Content：这是一个包中实际需要传输的内容，根据包类型的不同，其数据内容也会有所差异，但核心内容分为以下部分：\nPID：包标识符，用于区分不同类型的包，USB。在数据层面是八个比特的数据，高四位的包类型标识符用于指明包的类型，低四位是高四位的补码，用于错误检测。\n令牌包（xx01）：总是由主机发出，用于定义一个事务的开始，并设定该事务的地址和端点。\nPID 含义 说明 0001 令牌OUT 主机发送数据到USB设备 1001 令牌IN 主机接收从USB设备发出的数据 0101 令牌SOF 此时作为一个帧或者微帧的开始信息 1101 令牌SETUP 主机向USB设备发送配置信息 数据包（xx11）：用于携带实际的数据载荷。可以由主机或设备发出，其中DATA0和DATA1应交替发送，这个机制被称为数据切换同步（Data Toggle），是USB保证事务级可靠性的关键机制之一，用于检测和丢弃重复的数据包。\nPID 含义 说明 0011 数据DATA0 数据包偶数包 1011 数据DATA1 数据为奇数据包 0111 数据DATA2 此为作为一个高速同步事务的专用数据包 1111 数据MDATA 此时作为一个SPLIT事务的专用数据包。 握手包（xx10）：用于确认事务的状态，是最简单的包类型（通常没有数据载荷）。\nPID 含义 说明 0010 握手ACK 数据正确接收 1010 握手NAK 数据未正确接收 1110 握手STALL 使用的端点被挂起 0110 握手NYET 接收方没有响应 特殊包（xx00）：用于一些特殊用途，例如高速传输中的拆分事务（Split Transaction）等。\nPID 含义 说明 1000 特殊用途SPLIT 高速主使用事该SPLIT事务解决从高速模式到低速和全速模式的转换 0100 特殊用途PING 仅用于高速模式下主机使用该事务判断设备是否可以接收数据 1100 特殊用途ERR SPLIT事务中表示出现错误 1100 PRE令牌包 低速数据的先导包 地址：地址包含设备地址和端点地址，用于明确这个包是发到哪的。其中设备地址占1字节，端点地址占4位。\n帧号：总共11位，主机每发出一个帧，帧号都会自动加1，帧号到达到7FFH时将归零重新开始计数。\n数据域：USB传输的数据，对于不同的USB传输类型，数据域长度不同。\n控制传输：高速和全速都是64字节。 批量传输：高速512字节，全速64字节。 中断传输：高速1024字节，全速64字节。 同步传输：高速1024字节，全速1023字节。 CRC校验域：分为令牌校验域和数据校验域。\n令牌Token CRC校验域 计算地址域和帧号域的CRC：G(X) = X^5^+ X^2^ + 1 Data CRC 计算数据域数据的CRC： G(X) = X^16^ + X^15^ + X^2^ + 1 EOP：用于告知包的结束。在物理层面是产生一个持续2位时间的SE0（Single-Ended Zero） 状态（D+和D-均被驱动为低），然后恢复1位时间的J状态，之后总线驱动被释放，进入空闲状态。\n事务 尽管包是传输物理信号层面上的最小单元，但是单个包（如令牌包、握手包）并没有传输用户的实际数据，而是协议层面的控制信息。事务才是一次完整且有意义的通信流程的最小单元，它确保了定量数据的可靠交付。\n一次事务通常由三个阶段的包组成：\n令牌包 (Token Packet)：由主机发起，指定目标设备地址、端点号和事务类型（IN, OUT, SETUP）。 数据包 (Data Packet) (可选)：由数据发送方发出，承载实际的有效数据载荷。数据的长度不能超过对应端点的最大包长（wMaxPacketSize）。 握手包 (Handshake Packet)：由数据的接收方发出，用于向发送方反馈接收状态（如ACK, NAK, STALL），以保证传输的可靠性。某些事务可能没有数据过程，因此直接以握手包结束。 通常，一个大的数据传输会被拆分成多个事务进行。只有数据包中的有效载荷才会被缓存在端点的FIFO中，协议开销（包标识PID、地址、CRC等）由硬件处理，不占用软件可见的缓冲区空间。\n对于USB 2.0，基本事务类型包括：\nSETUP事务：用于控制传输的建立阶段，传输主机发送给设备的请求命令。其数据包必须为DATA0，且会重置端点的数据触发位。 OUT事务：主机向设备发送数据。 IN事务：主机从设备读取数据。 PING事务（仅高速模式）：主机探询设备是否有空间接收OUT数据，以提升总线效率。 SPLIT事务（仅高速枢纽使用）：用于在高速总线上传输全速/低速流量。 IN和OUT事务使用DATA0和DATA1数据包交替发送的机制（数据切换同步）。每次事务成功完成，双方都会翻转数据触发位，用于检测数据包是否丢失或重复。事务失败则重试相同的数据包PID。\n传输 因为单个事务传输数据大小有限，但是对于应用而言，只需要关注完整的数据即可，故将一次完整数据的通信过程称之为传输，并根据传输的频率和带宽占用划分为四种传输模式。\n控制传输(Control Transfer) ：用于突发的少量数据传输。例如USB设备在接入总线后进行的枚举过程，就是通过控制传输发送USB设备的描述符。 批量传输(Bulk Transfer)：用于突发的大量数据传输。例如U盘、打印机等设备。 中断传输(Interrupt Transfer)：用于周期的少量数据传输。例如鼠标、键盘、游戏手柄等。 等时传输(Isochronous Transfer)：用于周期的大量数据传输。例如摄像头、麦克风、音箱等。 需要注意的是，控制传输是最可靠的传输方式，通常用于设备的配置控制。控制传输被分为三个阶段，分别为建立阶段——数据阶段——状态阶段：\n在建立阶段，固定有1个SETUP事务，目的是为了传递一个8字节的标准化请求，该请求数据放在数据包DATA0中，且握手包必须回复ACK，否则主机会重试。 在数据阶段，根据请求类型，传输数据（如发送描述符），可能会有0个或多个IN或OUT事务，使用DATA0/DATA1交替传输。 在状态阶段，固定有1个事务，由数据阶段的接收方发送一个IN或OUT事务，数据包存在但载荷为0字节。 USB定义了8个字节的标准请求，通过这些请求，可以对设备的状态进行更改或对设备进行枚举，请求必须通过控制传输进行，关于USB的标准请求可以详见《USB 标准请求》。\n其余三种传输都没有分阶段，而是直接由IN或OUT事务组成的单事务循环队列，且在一次传输中是单向通信的。\n不过等时传输更特殊一些，它是可靠性最低但实时性最高的传输方式，它的IN或OUT事务中没有握手包，也就是说如果出现丢包的情况，主机和设备会直接无视这次错误而不会采取重传，以此来保证下一次事务的准时通信。\n总结 我们目前已经基本了解了USB的基础概念，脑海中已经有了一定的USB通信过程的框架。通过学习，我们不难知道，一个USB设备的工作流程可以细化为以下步骤：\n接入与上电 (Attachment and Power)： 设备插入USB端口，从总线（VBus）获取电源。 设备通过其上拉电阻将D+或D-信号线拉高，主机检测到这一电平变化，确认有设备接入。 复位 (Reset)： 主机向设备发送一个复位信号（持续10ms以上的SE0状态）。 设备收到复位信号后，进入默认状态，使用默认地址0进行通信，并准备好响应控制传输。 枚举与配置 (Enumeration and Configuration)： 获取描述符：主机通过默认地址0，向设备的端点0发送各种标准请求（如GET_DESCRIPTOR），逐步获取设备的设备描述符、配置描述符、接口描述符、端点描述符等。 设置地址：主机为设备分配一个唯一的地址（1-127），并发送SET_ADDRESS请求。设备之后的所有通信都使用这个新地址。 获取配置信息：主机使用新地址，再次获取设备的详细配置描述符。 选择配置：主机根据设备的配置信息，发送SET_CONFIGURATION请求，激活设备的某一个配置。设备至此进入配置状态（Configured State），其所有端点都准备就绪，可以正常工作。 驱动加载与通信 (Driver Loading and Communication)： 主机根据枚举到的信息（如设备类、厂商ID、产品ID），加载对应的设备驱动程序。 驱动程序开始工作，通过主机控制器与设备进行数据通信。所有通信均由主机发起，通过之前配置好的各个端点（如批量传输端点、中断传输端点等），按照设备支持的传输类型进行数据交换。 移除与下电 (Removal and Power Down)： 设备被拔出，主机检测到D+/D-信号线进入SE0状态持续一段时间，判断设备已移除。 主机通知操作系统和驱动程序，释放所有为该设备分配的资源（地址、带宽等）。 参考资料 USB中文网\n《STM32 USB设备开发指南-基于TeenyUSB协议栈》\nUSB原理：从零基础入门到放弃\n","date":"2025-09-04T16:33:00+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/usb/usb%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/usb/usb%E5%9F%BA%E7%A1%80%E6%A6%82%E5%BF%B5/","title":"USB基础概念"},{"content":" 前言 编译和链接 声明和定义 为什么要声明 为什么要定义 总结 前言 很多编程语言的语法中都有关于声明和定义的概念，这种概念一般会应用于函数或变量的创建和使用中，但是为什么要这么做？\n以C语言为例，一些书籍或教程会要求读者在程序文件开头写上函数和变量的声明，然后再在后面对其进行定义（对于变量也可以叫初始化）。这不免让人感到有一丝疑惑，为什么要这样做？我能不能先定义再声明？或者我能否不声明直接定义？\n事实上这样做很多时候也是可以的，但是为什么呢？\n为此我们需要明确一些基本的概念。\n编译和链接 以C和C++为例，如果你正在使用某个IDE进行编程，那么你应该可以发现，在你写好一个程序后需要将它运行之前，往往需要先进行构建（有的IDE里面写的是生成或build），然后经过一段时间的等待后，你就会获得一个可执行程序，之后你就可以运行这个程序文件得到想要的效果。\n这个过程中发生了什么呢？一般情况下这个过程分为了三步——预处理、编译、链接。预处理过程一般是C和C++的特色，你所见到的那些带#的语句一般被称为宏，而这些宏将会被预处理器进行预处理。通常而言，你只需要知道#define（将被定义的内容用定义的内容进行替换）和#include（将被引用的文件全部原样复制到此）两个宏就够了。\n不过预处理并不是我们讨论的焦点。\n编译一般发生于预处理之后，一般而言，当预处理结束后，所有有用的.h文件都已经通过#include被原样复制到了.c文件中了，所以整个工程现在就只剩下一堆.c文件。而编译器会检测每个.c文件然后将它们编译称为对应的汇编文件，再然后将汇编文件翻译成二进制文件。不过一般而言，翻译成为汇编文件这个过程都是被隐藏的，至少如果不单独设置你是无法看到生成汇编文件这个过程。不过没关系，这样也可以简化我们的思路，所以直接将编译过程理解为从.c文件到二进制文件（这个文件后缀根据编译器和系统的不同可能有所差异，一般是.o或.obj，当然具体是什么后缀都不重要）。\n编译结束后，显然我们会得到一堆二进制文件，它们和之前写的.c文件一般是一一对应的。但是很显然，这些二进制文件还不是可执行程序，而且它们太分散了。这个时候就需要链接器进行工作，链接器会根据链接表将所有二进制文件进行重组和拼装，最后形成一个完整的可执行文件。\n声明和定义 那么，声明和定义在这里有什么用呢？\n为什么要声明 现在我们假设一个工程中有两个.c文件，一个是main.c，里面放了一个空的main函数，此外什么也没有。另一个是hello.c，里面放了一个void SayHello()函数并引用了stdio.h文件，这个函数的功能是输出一个Hello World。\n显然，hello.c里面实现了对SayHello函数的定义，也就是说我们的工程中已经实现了SayHello这个函数，此时我们是否能够在main函数中调用它呢？\n如果你尝试直接在main函数中调用SayHello，然后分别对main.c和hello.c进行编译（一般IDE会有单独的编译选项，右键单击某个c文件就可以选择，而且这个编译事实上还是包括了预处理过程的），你会发现对main.c进行编译的时候会出现报错。\n编译器会提示你，没有对SayHello函数进行定义。\n但是我们明明在hello.c中进行定义了啊？难道是我需要先编译一下hello.c然后再编译main.c？事实上你换一下顺序结果也是一样。\n因为编译器是没有记忆的，当他编译完一个文件，就会马上忘记它在这个文件里都发现了什么东西，所以哪怕它刚刚才在hello.c里面编译了SayHello的定义，但是现在它在编译main.c，它就已经忘记了这回事了。\n那么我们有什么办法可以解决这种情况呢？\n第一种方法就是，在main.c的前面加上对SayHello的定义，注意尤其一定要在main函数的前面。有人会问，为什么一定要在前面，后面不行吗？事实上确实不行，因为当编译器发现一个它从来没见过的函数调用时，就会马上报错，不会那么智能地再在后面找一遍你是不是定义在后面了，所以唯有在调用之前就定义好，他才知道你有好好定义这个函数，然后它就不会闹了。\n但是显然，如果我希望在多个文件里面调用SayHello函数，那么这个方法就不起效了。\n所以我们就有了第二种方法，在main.c文件前面加上一句对SayHello的声明。声明只需要写上函数返回值、函数名和函数参数表就可以了，不需要写完整的实现。声明其实是程序员对编译器的保证，意思就是“我保证我在其它某个地方肯定会写这个函数的定义的”，然后编译器会相信程序员，虽然你并没有实际在这个文件中定义这个函数，但是当你调用这个函数的时候，编译器不会报错。注意，声明也需要写在调用的前面，逻辑和第一种方法一样。\n现在你大概理解了声明的意思了，而一般情况下，声明会被写在一个.h文件中，要使用某个函数之前只要#include这个文件并且将它对应的.c文件包含在工程内即可。\n注意，除了#include，你一定也要将它对应的.c文件包含在工程内，因为.c文件里包含了对这些函数的定义。\n为什么要定义 没有定义显然是不行的，否则这个函数就是一个空有名字和输入输出的不明物，很显然这是程序员的疏忽（或像我们故意）造成的错误，是需要被发现检测出来的。\n现在，我们做个简单的小实验，还是刚才的工程，但是我们将hello.c里的SayHello注释掉（如果你不怕麻烦想要更好的体验，可以直接删掉hello.c或将它移出工程），此时我们再次编译hello.c和main.c，你会发现，编译器仍然没有报错。\n是不是有点疑惑，现在明显没有SayHello的定义，但是为什么编译器不报错呢？\n很显然，是因为编译器非常相信你，你在main.c中声明了，你一定会定义SayHello的，所以编译器就理所当然地相信了你，而没有记忆的它显然是无法发现自己被骗了。而hello.c文件里什么都没有，那么自然也不会有什么错（如果你直接将其移除了工程就更加不可能报错了）。\n但是这并不代表你能一直骗它，我们知道编译结束后还得链接才能得到可执行程序，不过IDE一般没有单独的链接按钮，所以我们得使用构建，当按下这个按钮后整个工程都将完整经历完前面说的三个过程——预处理、编译和链接。其中预处理和编译仍然是刚才我们的编译器进行的，它和刚才一样，没有发现任何问题。\n但是你骗不了链接器，因为它会对程序进行重新组装，但是在组装的过程中它会发现，有一个本来应该出现的函数定义却在此刻不见了，然后它会发出一个L字母开头的报错，唯有你在工程中重新创建了这个函数的定义并重新构建才可以消除。\n总结 这里用C语言的函数声明与定义解释了为什么要声明和定义以及它们有什么用，用相同的逻辑它也可以扩展到变量以及其它语言的声明与定义中。\n","date":"2023-02-11T15:02:59+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/%E5%A3%B0%E6%98%8E%E5%92%8C%E5%AE%9A%E4%B9%89/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/%E5%A3%B0%E6%98%8E%E5%92%8C%E5%AE%9A%E4%B9%89/","title":"C/C++ 声明和定义"},{"content":"记录一下，实测有效\n《 QT找不到python27.dll解决办法》\n《打开Qt Creator软件时报警告：xxx.exe应用程序错误》\n","date":"2022-05-05T23:00:50+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/qt-creator%E6%89%93%E5%BC%80%E6%97%B6%E6%8A%A5%E9%94%99python27.dll%E5%92%8C%E6%8A%A5%E9%94%99%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E9%94%99%E8%AF%AF%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95%E7%BD%91%E5%9D%80/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/qt-creator%E6%89%93%E5%BC%80%E6%97%B6%E6%8A%A5%E9%94%99python27.dll%E5%92%8C%E6%8A%A5%E9%94%99%E5%BA%94%E7%94%A8%E7%A8%8B%E5%BA%8F%E9%94%99%E8%AF%AF%E8%A7%A3%E5%86%B3%E5%8A%9E%E6%B3%95%E7%BD%91%E5%9D%80/","title":"Qt Creator打开时报错python27.dll和报错应用程序错误解决办法"},{"content":" 题目来源 分析 我的程序 运行结果 题目来源 LeetCode：https://leetcode-cn.com/problems/two-sum\n分析 具体思想可以参考这个题解的演示：https://leetcode-cn.com/problems/two-sum/solution/tu-jie-guan-fang-tui-jian-ti-jie-liang-s-02xs/，LeetCode中使用的哈希算法是通过uthash.h这个文件实现的，该文件本来是个开源项目，目前已整合到gcc中，关于其使用请参考uthash的文档：https://troydhanson.github.io/uthash/userguide.html。\n我的程序 struct numsHash{ int key;\t// 键值 int val;\t// 数据 UT_hash_handle hh; }; struct numsHash* pNumsHashHead = NULL; struct numsHash* Find_NumsHash(int targetKey)\t// 定义查询函数 { struct numsHash *pReturn; HASH_FIND_INT(pNumsHashHead, \u0026amp;targetKey, pReturn); return pReturn; } void Add_NumsHash(int addKey, int addVal) { struct numsHash* pTarget = Find_NumsHash(addKey); if(pTarget == NULL){ struct numsHash* pTempHash = malloc(sizeof(struct numsHash)); pTempHash-\u0026gt;key = addKey; pTempHash-\u0026gt;val = addVal; HASH_ADD_INT(pNumsHashHead, key, pTempHash); } } int* twoSum(int* nums, int numsSize, int target, int* returnSize) { pNumsHashHead = NULL; for(int i = 0; i \u0026lt; numsSize; i++){ struct numsHash* pTempHash = Find_NumsHash(target - nums[i]); if(pTempHash != NULL){ int* ret = malloc(sizeof(int) * 2); ret[0] = pTempHash-\u0026gt;val; ret[1] = i; *returnSize = 2; return ret; } Add_NumsHash(nums[i], i); } *returnSize = 0; return NULL; } 运行结果 执行用时 内存消耗 4ms 7.6MB ","date":"2022-04-05T20:16:44+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C%E5%93%88%E5%B8%8C%E7%AE%97%E6%B3%95/","title":"算法：两数之和（哈希算法）"},{"content":" 题目来源 我的程序 结果 官方答案 结果 分析 题目来源 LeetCode：https://leetcode-cn.com/problems/two-sum\n我的程序 /** * Note: The returned array must be malloced, assume caller calls free(). */ int* twoSum(int* nums, int numsSize, int target, int* returnSize){ int* ret = NULL; *returnSize = 0; int temp = 0; for(int i = 0; i \u0026lt; numsSize; ++i) { for(int j = i + 1; j \u0026lt; numsSize; ++j) { if(nums[i] + nums[j] == target) { *returnSize += 2; ret = realloc(ret, sizeof(int) * (*returnSize)); ret[temp++] = i; ret[temp++] = j; } } } return ret; } 结果 执行用时 内存消耗 152ms 6.1MB 官方答案 int* twoSum(int* nums, int numsSize, int target, int* returnSize) { for (int i = 0; i \u0026lt; numsSize; ++i) { for (int j = i + 1; j \u0026lt; numsSize; ++j) { if (nums[i] + nums[j] == target) { int* ret = malloc(sizeof(int) * 2); ret[0] = i, ret[1] = j; *returnSize = 2; return ret; } } } *returnSize = 0; return NULL; } 结果 执行用时 内存消耗 92ms 6.3MB 分析 我的程序是整个完全遍历完了找出所有解，而官方是找到后直接跳出。相对于这道题来说官方的答案很显然更加贴近题意，但是我的程序更加具有普遍性（因为适合满足多组解的情况）。\n","date":"2022-04-05T20:16:15+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C%E6%9A%B4%E5%8A%9B%E6%9E%9A%E4%B8%BE/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E4%B8%A4%E6%95%B0%E4%B9%8B%E5%92%8C%E6%9A%B4%E5%8A%9B%E6%9E%9A%E4%B8%BE/","title":"算法：两数之和（暴力枚举）"},{"content":" 题目来源 题目内容 分析 我的程序 运行结果 题目来源 浙大数据结构MOOC-PTA的课后题\n题目内容 分析 就是个普通的二分查找，没啥好分析的。\n我的程序 Position BinarySearch( List L, ElementType X ) { Position left, right; left = 1; right = L-\u0026gt;Last; while(left \u0026lt;= right) { Position middle = (left + right) / 2; // 记录中间位置 if(L-\u0026gt;Data[middle] == X)return middle; else if(L-\u0026gt;Data[middle] \u0026lt; X)left = middle +1; else if(L-\u0026gt;Data[middle] \u0026gt; X)right = middle - 1; } return NotFound; } 运行结果 ","date":"2022-04-02T19:15:43+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E4%BA%8C%E5%88%86%E6%9F%A5%E6%89%BE/","title":"二分查找"},{"content":" 题目来源 题目内容 分析 我的程序 运行结果 题目来源 浙大数据结构MOOC-PTA的课后题\n题目内容 分析 这一题时最大子列和问题的变形，需要输出最大子列和的子列首项与末项，且在数列全为负的情况下，最大子列和为0，且需要输出首尾。而且如果出现了前后有相同都是最大子列的情况，选择前面的那一个。\n我继续沿用了最大子列和问题中的在线处理算法，但是加上了对子列首尾的处理。\n说实话这题有点难，我的做法也稍微有点取巧，使用专门的标志来处理非正有0的情况，不过总体来说结果是过了。\n我的程序 #include \u0026lt;stdio.h\u0026gt; int main() { // 获取数据 int size; scanf(\u0026#34;%d\u0026#34;, \u0026amp;size); int num[size]; int max = 0, this = 0; // 记录首尾位置 int start = 0, end = 0, temp = 0; // 记录负数和正数的个数 int negFlag = 0; int posFlag = 0; // 在线处理，寻找最大子列和同时记录该子列和首尾位置 for(int i = 0; i \u0026lt; size; i++) { scanf(\u0026#34;%d\u0026#34;, num + i); // 记录正数和负数的个数，等会有用 if(num[i] \u0026lt; 0)negFlag++; else if(num[i] \u0026gt; 0)posFlag++; // 计算子列和 this += num[i]; if(this \u0026gt; max) { max = this; // 当最大子列和发生了变化，记录其末尾，并将之前缓存的首部放入start end = i; start = temp; } else if(this \u0026lt; 0) { this = 0; // 当在线处理发现了子列和为负数的情况，需要丢弃前面的子列和，此时使用temp记录此次更新，因为它可能成为最大子列和的首项 temp = i + 1; } } if(negFlag == size)printf(\u0026#34;%d %d %d\u0026#34;, 0, num[0], num[size - 1]);\t// 全是负数，输出最大值0和首尾项 else if(posFlag \u0026gt; 0)printf(\u0026#34;%d %d %d\u0026#34;, max, num[start], num[end]);\t// 如果有正数，则必然存在非零最大子列 else if(posFlag == 0)printf(\u0026#34;0 0 0\u0026#34;);\t// 如果没有正数，又不全是负数，则最大子列和是0，且子列内容也全是0，直接输出0 return 0; } 运行结果 ","date":"2022-04-02T19:09:57+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95maximum-subsequence-sum/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95maximum-subsequence-sum/","title":"算法：Maximum Subsequence Sum"},{"content":" 题目来源 题目内容 分析 我的程序 运行结果 题目来源 浙大数据结构MOOC-PTA的课后题\n题目内容 分析 这个程序在课程里讲过，一共有四种方法，其中两种是同一种方法，只是进行了优化，这道题没有什么好分析的，直接写就行了。\n我的程序 暴力穷举：\n#include \u0026lt;stdio.h\u0026gt; int main() { // 获取数据 int size; scanf(\u0026#34;%d\u0026#34;, \u0026amp;size); int num[size]; int max = 0, this = 0; for(int i = 0; i \u0026lt; size; i++) { scanf(\u0026#34;%d\u0026#34;, num + i); } // 穷举所有子列并找出最大和 for(int i = 0; i \u0026lt; size; i++)\t// 分别以每个项作为子列起点，然后计算这个起点所有子列和 { this = 0;\t// 每次计算完子列，都需要清空 for(int j = i; j \u0026lt; size; j++)\t// 计算当前所在起点的所有子列和 { this += num[j]; if(this \u0026gt; max)max = this;\t// 当出现了更大的子列和时，将其存入max } } printf(\u0026#34;%d\u0026#34;, max);\t// 打印出来 return 0; } 这种解法的时间复杂度为O(N^2^)，虽然能用但是还差点意思。\n分而治之：\n#include \u0026lt;stdio.h\u0026gt; int getMaxSequenceSum(int* num, int size); int main() { int size; scanf(\u0026#34;%d\u0026#34;, \u0026amp;size); int num[size]; int max = 0; for(int i = 0; i \u0026lt; size; i++) { scanf(\u0026#34;%d\u0026#34;, num + i); } max = getMaxSequenceSum(num, size); printf(\u0026#34;%d\u0026#34;, max); return 0; } int getMaxSequenceSum(int* num, int size) // 分而治之，左右分并分别计算最大子列和，然后从中间向两边延申计算最大子列和，比较三者，最终得到最大子列和 { if(size == 1) { return num[0]\u0026gt;0?num[0]:0; // 不可再分时，最大子列和就是本身或0（小于0的情况） } int left = getMaxSequenceSum(num, size / 2); int right = getMaxSequenceSum(num + size / 2, size - size / 2); int mid_l = 0, mid_r = 0; int max = 0, this = 0; for(int i = 1; i \u0026lt;= size / 2; i++) { this += num[size / 2 - i]; if(mid_l \u0026lt; this)mid_l = this; } this = 0; for(int i = 0; i \u0026lt; size / 2; i++) { this += num[size / 2 + i]; if(mid_r \u0026lt; this)mid_r = this; } max = mid_l + mid_r \u0026gt; left?mid_l + mid_r:left; return max \u0026gt; right?max:right; } 这种解法的时间复杂度为O(NlogN)，已经是一种非常优秀的解法了。\n在线处理：\n#include \u0026lt;stdio.h\u0026gt; int main() { // 获取数据 int size; scanf(\u0026#34;%d\u0026#34;, \u0026amp;size); int num[size]; int max = 0, this = 0; for(int i = 0; i \u0026lt; size; i++) { scanf(\u0026#34;%d\u0026#34;, num + i); this += num[i]; if(this \u0026gt; max)max = this; else if(this \u0026lt; 0)this = 0; } printf(\u0026#34;%d\u0026#34;, max);\t// 打印出来 return 0; } 在线处理的时间复杂度是O(N)，时间复杂度是线性的算法，是该问题的最优解。\n运行结果 下面的配图是我在MOOC中提交的图，里面代码没有优化，所以结果可能和预期有一点差别，不过整体大差没差。\n1.暴力穷举\n2.分而治之\n3.在线处理\n","date":"2022-04-02T18:45:44+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E6%9C%80%E5%A4%A7%E5%AD%90%E5%88%97%E5%92%8C%E9%97%AE%E9%A2%98/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E9%A2%98%E7%9B%AE/%E7%AE%97%E6%B3%95%E6%9C%80%E5%A4%A7%E5%AD%90%E5%88%97%E5%92%8C%E9%97%AE%E9%A2%98/","title":"算法：最大子列和问题"},{"content":"STM32CubeMX GCC工程Makefile内容详解 STM32CubeMX GCC工程Makefile内容详解 基础介绍 注意 Makefile TARGET DEBUG OPT BUILD_DIR C_SOURCES ASM_SOURCES PREFIX CC,AS,CP,SZ HEX,BIN CPU FPU FLOAT-ABI MCU AD_DEFS,C_DEFS AS_INCLUDES,C_INCLUDES ASFLAGS,CFLAGS ifeq ($(DEBUG), 1) CFLAGS LDSCRIPTS LIBS,LIBDIR LDFLAGS all OBJECTS BUILD clean flash dependencies 结语 基础介绍 因为项目原因，需要对编译系统进行一些比较复杂的使用，但是我对于编译系统这一块并不是非常精通了解，所以需要进行一下学习。正巧，众所周知STM32推出过非常实用的工具——STM32CubeMX，这个工具可以对STM32进行一些基础配置并设置一些配置代码，基本可以靠图形界面完成对STM32的所有初始化配置，然后它就会生成一个基础工程，编程者在这个基础上进行设置即可。\n当然，本文主要关注点是在编译系统，所以该工具的配置什么的就不详细说了，基本上就是简单随便配置一下时钟就可以了（事实上应该时钟都不需要配置），重点在于我们生成工程的时候需要选择生成Makefile工程，这样才可以使用我们的开源工具链进行配置。\n之后我们可以得到如下的工程目录（当然，众所周知CubeMX的一些设置会影响到工程根目录的具体结构，不过这都不是重点）：\n其中build目录是编译后生成的，先不管，另外Core文件夹下的Test文件夹及其中内容是我进行测试的时候添加的，暂时也先不管。\n我们先来看看其它的东西，首先是Core中的内容，这部分的内容基本上就完全是STM32CubeMX生成的，Inc中放h文件而Src中放c文件，里面的文件都是在CubeMX中配置过的东西，这些东西是每次CubeMX在修改后都会重新生成的东西（也有可能是你修改了哪一部分，就会重新生成哪一部分的文件，这个是CubeMX自己的机制，不重要）。\n其次是Drivers中的内容，这些内容实际上都是从 CubeMX的安装目录里的sdk中复制过来的，如果我们在配置的时候没有选择要将这些东西复制过来，那么CubeMX也不会生成这个目录下的内容（或者不能说生成，而就是直接复制过来），但是取而代之的就是你使用的所有sdk的库函数，都是直接调用的安装目录里的东西，那么为了安全性着想，就不要在编程的时候修改他们了，否则你的库函数就被你的代码污染了，当然这样做的好处就是你的代码复用性会提高而且工程体积会稍微小点。\n然后是下面的.mxproject文件，CubeMX的工程文件，不管。\n接着就是我们等会最重要的Makefile，这个是Make工具的脚本文件，如果你不知道Make工具是啥，建议先了解一下再看本文会好一点。\n之后是MakeTest.ioc文件，CubeMX的配置文件，不管。\n然后是后缀为s的start文件，这个文件是芯片的项目启动文件，一般不需要管，里面放了中断向量表之类的东西。\n最后是后缀为ld的链接脚本文件，这个是用于链接的，等会会说。\n注意 本文的解析结构并没有按照makefile的解析顺序来，而是采用从上到下的顺序来解说的（当然会根据实际情况进行跳跃），主要原因在于我学习这个东西的目的主要是直接把这个Makefile当成一个模板，然后未来直接在这个模板上进行修改就可以了，而到目前为止我暂时还不打算自己从头来写个Makefile（毕竟能用就行，平时哪需要那么麻烦）。\n考虑到篇幅，就不单独把全部makefile列出来了，需要时你直接用CubeMX生成一个工程然后看它Makefile即可，只要STM32公司那边没有对这个规则进行大改，那么下面这些内容应该都大差不差。\n此外，本文中很多东西其实可以在gcc的手册中找到答案，如果感兴趣可以去看看。\nMakefile TARGET TARGET = MakeTest 设置目标文件名，实际上就是做了个变量，并且把Makefile这个作为参数传入。平时当成工程名即可，如果要换工程，就自己随便重新取个名字。\nDEBUG DEBUG = 1 是否进行调试编译，这里表示是，release版关闭调试时将1改成其它数即可，具体原因后面会讲到。\nOPT OPT = -Og 优化等级，这里使用Og的目的是因为开启了调试，所以优化时需要产生合理的优化而不和调试选项冲突。如果是release版本，可以使用-Os（提高速度并优化体积）或-O3（大幅提高速度但会增大体积）等命令，关于这部分的介绍可以参考知乎回答。\nBUILD_DIR BUILD_DIR = build 编译目标地址，这也是前面提到为什么我会生成一个build文件夹，实际上就是这个变量进行的命名，如果你需要，可以把它修改成任何名字，然后编译出来的所有文件就都会放在这个你新命名的文件夹里面了。不过一般不建议修改，因为大家通用的都是build，不建议太特立独行。\nC_SOURCES C_SOURCES = \\ Core/Src/main.c \\ Core/Src/gpio.c \\ Core/Src/usart.c \\ Core/Src/stm32f4xx_it.c \\ Core/Src/stm32f4xx_hal_msp.c \\ Core/Test/test.c\\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_tim_ex.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_uart.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc_ex.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ex.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_flash_ramfunc.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_gpio.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma_ex.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dma.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_pwr_ex.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_cortex.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal.c \\ Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_exti.c \\ Core/Src/system_stm32f4xx.c 其实从名字我们不难理解，这里放的就是所有用到的c文件。如果我们需要自己写个什么库自己写了什么新的c文件，而且最后会被编译到程序里面的话，就都需要自己在这里添加一行c文件地址。这些地址可以是相对路径，相对的就是makefile文件的地址，而如果不好相对路径，那么也可以设置为绝对路径，例如这里面所有Drivers的文件，如果我们没有配置为复制到工程里面，那么这些文件就会被写为绝对路径并导航到你的CubeMX的sdk存储地址里面。\n此外，注意我添加了一行test.c的路径，这一行就是我自己写的文件，里面定义了一个全局变量，并被对应的test.h使用extern引用，然后在Src文件夹下面被main.c引用test.h后使用。这里我想说明的是，哪怕你用h文件做了个跳板（而不是直接extern过来）然后调用了test.c的东西，而且test.c中连个函数都没有，但是只要你使用了里面的东西，就必须要把文件地址填在上面的文件列表中。\n不过说实话我个人感觉这样做非常麻烦，这里我有点想知道为什么不能直接添加一个文件夹然后遍历下面的所有c文件并添加呢？为此我还稍微查询了一下，找到了这个博客《一点一点学写Makefile(6)-遍历当前目录源文件及其子目录下源文件》，感兴趣可以看看。\nASM_SOURCES ASM_SOURCES = \\ startup_stm32f411xe.s 所有汇编文件的地址，添加方法可以继续参考前面C文件的添加方法，也是绝对地址和相对地址添加即可。而这里的start文件，一般不需要用户自己写，这些厂商在开发好芯片后都至少会提供一个startup的c或s文件的，我们需要做的就只是把他添加进来即可（有的可能不是汇编s文件，如果是c文件，正常添加到前面C文件的路径下面即可，这种情况下汇编这里就不需要添加startup了，不过此外你自己写的其他汇编文件倒是仍然可以正常这样写进去）。\nPREFIX PREFIX = arm-none-eabi- 工具链起始名，说白了就是你使用的开源工具链（当然你有钱也可以买闭源的，不过没啥必要而且使用起来需要再自行研究一下），这里变量的内容是arm-none-eabi-，其实这个是一个gcc工具链的名字，这个工具链全程一般叫arm-none-eabi-gcc，arm表示架构平台，none表示无系统，eabi表示嵌入式应用二进制接口，gcc表示其遵守的GNU GCC的设计规范。这东西我记得之前在哪个很古老的博客上面看到的，但是当时忘了收藏，所以现在找不到了。总之，开发者将这个工具链除gcc部分命名后，里面的所有工具就全部使用这个前缀了，这个东西我们在后面会说。\n如果你需要对linux的arm设备进行开发，那么你可能需要安装arm-linux-eabi-gcc（当然也可能是其他名字或顺序，这个是开发者自己决定的，因为里面还可以添加开发者名称，所以好像没有非常严格的规范），如果你需要对riscv架构的设备进行开发，那么可能需要使用riscv-none-gcc，总之，需要什么自己去芯片官网上先看看有没有推荐，然后下载并改这里的参数即可。\nCC,AS,CP,SZ ifdef GCC_PATH CC = $(GCC_PATH)/$(PREFIX)gcc AS = $(GCC_PATH)/$(PREFIX)gcc -x assembler-with-cpp CP = $(GCC_PATH)/$(PREFIX)objcopy SZ = $(GCC_PATH)/$(PREFIX)size else CC = $(PREFIX)gcc AS = $(PREFIX)gcc -x assembler-with-cpp CP = $(PREFIX)objcopy SZ = $(PREFIX)size endif 这里我们需要分别看两个东西，其中是ifdef部分，这东西是一个判断你有没有定义某个变量，看名字大概也可以猜到，这个就是我们gcc工具链的地址。这就涉及到一个问题，也就是环境变量，如果你将你的工具链的工具目录地址添加到环境变量中，那么我们就可以在命令行中直接通过工具的名称调用这个工具，否则我们就需要输入绝对路径来调用工具。\n比如你的arm-none-eabi-gcc存储在D盘根目录下，那么你很可能需要在环境变量中添加D:\\arm-none-eabi-gcc\\bin\\文件夹地址，此时我们就可以在命令行中直接使用arm-none-eabi-gcc来运行D:\\arm-none-eabi-gcc\\bin\\arm-none-eabi-gcc.exe程序，否则，我们就需要输入完整的绝对路径来调用该程序。\n而GCC_PATH就是用来帮助你添加这个绝对路径的，如果你没有添加环境变量，那么就可以在makefile前面定义GCC_PATH并添加上地址，如D:\\arm-none-eabi-gcc\\bin，那么之后我们会运行ifdef中的程序（如果没定义就直接使用后面else的程序），makefile所有变量的本质其实就是字符替换，此时例如CC，就会被编译为D:\\arm-none-eabi-gcc\\bin\\arm-none-eabi-gcc.exe，其余同理。当然，如果你不想在makefile中添加这个变量，也可以选择在输入make指令的时候添加GCC_PATH=D:\\arm-none-eabi-gcc\\bin。\nPREFIX前面也已经说过了，不赘述。\n然后我们关注到这四个工具，其中gcc出现了两次，分别在CC和AS中，CC中直接使用gcc，其实代表了其默认参数编译C语言，而AS从名字在可以猜是代表了汇编器，其使用了-x参数表示更换编译语言，只不过代表所更换的参数assembler-with-cpp，似乎不只是可以汇编，还可以处理C++？不过我把test.c改名成test.cpp后分别添加到汇编和C的Sources列表中似乎都不行，会编译错误，有点不是很懂，不过总之其可以编译汇编语言成为机器语言是肯定的。\n之后是CP的objcopy，这是个复制文件内容的工具，具体介绍可以参考《objcopy命令介绍 - Dake的信息摘录博客》。\nSZ是一个记录生成程序内部各分段的大小的工具，这个在编译后查看编译出的文件大小进行项目优化的时候会比较有用，makefile中也用了这个工具，在编译到最后会输出这样的内容：\narm-none-eabi-size build/MakeTest.elf text data bss dec hex filename 6524 24 1640 8188 1ffc build/MakeTest.elf 值得注意的是这个size工具只能对elf文件进行解析，而hex和bin文件中是不包含代码段数据段这些东西的。这里的大概意思是我的代码段有6524字节，数据段有24字节，bss段有1640字节，总共加起来有8188字节。关于这些段的介绍，参考可以参考这个博客《堆栈、BSS段、代码段、数据段、RO、RW、ZI等概念区分》。\nHEX,BIN HEX = $(CP) -O ihex BIN = $(CP) -O binary -S 从名字我们就不难看出，这里其实指的就是生成十六进制文件和二进制文件，他们都是可以被烧录到stm32中的代码文件格式，而其中的参数也很好懂，分别就是生成十六进制和二进制所需的参数，没啥好解释的，直接用就行。\nCPU CPU = -mcpu=cortex-m4 选择cpu的架构，因为我使用的是stm32f4的芯片，故这里的架构是cortex-m4，如果是其它架构需要进行修改。\nFPU FPU = -mfpu=fpv4-sp-d16 选择fpu的架构，这里是因为F4系列有fpu，所以可以添加，如果没有就需要关闭，关闭后无法使用fpu对浮点数计算进行加速，相关概念可以参考《【arm-gcc开发STM32】开启stm32f4的FPU》。\nFLOAT-ABI FLOAT-ABI = -mfloat-abi=hard 硬浮点或软浮点，如果有FPU就可以开启硬浮点，这里开启了，相关概念可以参考《浮点 _ 运算处理方式(软硬-mfloat-abi=) _ 浮点协处理器类型 -mfpu=》。\nMCU MCU = $(CPU) -mthumb $(FPU) $(FLOAT-ABI) 此处代表了所有针对MCU硬件的配置，其它的我们已经说过，而这个-mthumb代表的是T32指令集，如果使用了另外一种指令集的架构就需要换这里的参数，具体参考《【解读经典】ARM体系结构与编程（3）》。\nAD_DEFS,C_DEFS AS_DEFS = C_DEFS = \\ -DUSE_HAL_DRIVER \\ -DSTM32F411xE 分别是汇编和C的宏定义，这个项目中的汇编里面因为不需要，所以啥也没写，如果你不需要在汇编程序中增加什么外部的宏定义，就不需要这个。\n而C文件宏定义，真正的宏需要去掉每个宏定义开头的-D，例如此处的-DUSE_HAL_DRIVER，实际上是宏定义了USE_HAL_DRIVER，这些宏定义会影响到c文件里面相关宏的内容，例如这个HAL，其实就代表了HAL库，那么C文件里面引用的东西就会选择HAL库而非LL库的内容。如果你想的话，也可以自己添加一些宏定义在c文件中作为开关，然后在这里添加东西表示打开。\nAS_INCLUDES,C_INCLUDES AS_INCLUDES = C_INCLUDES = \\ -ICore/Inc \\ -IDrivers/STM32F4xx_HAL_Driver/Inc \\ -IDrivers/STM32F4xx_HAL_Driver/Inc/Legacy \\ -IDrivers/CMSIS/Device/ST/STM32F4xx/Include \\ -IDrivers/CMSIS/Include 汇编和C的引用文件所在文件夹，其中因为汇编没有引用外部文件，所以什么也没有，如果有需要自己添加。\n注意这里添加的是文件夹地址而不是具体的文件地址。\n对于C来说，如果在自己的代码#include中使用了相对地址或绝对地址，则可以不添加，如果是只写了引用文件名或非本文件的文件名，则需要添加，以辅助编译器找到对应文件，编译器会主动从上述路径作为相对路径寻找include的文件。这里-I也全部是前缀。\n例如我们需要在Core\\Src\\main.c中引用Core\\Test\\test.h，那么我们需要在main.c中写#include \u0026quot;test.h\u0026quot;并在C_INCLUDES中添加-ICore/Test，而如果我们不在C_INCLUDES中进行添加，那么main.c中就必须写相对或绝对地址，例如#include \u0026quot;../Test/test.h\u0026quot;，这样才能保证我们的编译器能够找准位置。\nASFLAGS,CFLAGS ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections CFLAGS = $(MCU) $(C_DEFS) $(C_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections 编译参数，分别针对汇编和C语言，他们各自前面四个变量参数已经讲过，不再赘述。\n-Wall的功能主要是开启一些警告检测，如果我们的代码出现了不规范的地方，那么通过这个参数，编译器就会检查到这些地方并告知我们。关于这个参数的具体介绍可以参考《gcc -Wall详解》。\n-fdata-sections表示对程序中每个数据创建一个section，而-ffunction-sections则是针对函数创建section，GCC的链接操作是以section作为最小的处理单元，只要一个section中的某个符号被引用，该section就会被加到可执行程序中。关于这个部分具体可以参考《gcc -ffunction-sections -fdata-sections -Wl,–gc-sections 参数详解》\nifeq ($(DEBUG), 1) ifeq ($(DEBUG), 1) CFLAGS += -g -gdwarf-2 endif 语句很好理解，如果DEBUG=1，就执行，如果不是1，就不执行。很显然，这段话就是检测目前是否是调试模式，而注意这里CFLAGS前面其实已经出现过，而这里再次出现时使用的就不再是等号=了，而是+=符号，这个符号在makefile中表示在已有变量后面补上新的符号。\n而补上的其实就是两个新的参数，我们应该不难猜测这两个参数就是针对的调试生成的。\n-g表示的意思是生成调试信息，如果不生成的话我们在使用gdb调试的时候就很难做到代码定位，当然如果你不使用gdb，还可以使用-gformat然后加上你想要的格式。而-gdwarf-2的意思是生成DWARF version2 的格式的调试信息，常用于IRIXX6上的DBX调试器。GCC会使用DWARF version3的一些特性。关于这两个参数的使用，可以参考《GCC 编译及编译选项》。\nCFLAGS CFLAGS += -MMD -MP -MF\u0026#34;$(@:%.o=%.d)\u0026#34; 关于这个部分的内容又是针对CFLAGS，这里出现了三个新的参数，同时后面有一串makefile语法的宏。\n这一串东西的主要功能是生成关联信息，关联信息会对链接产生影响，其中-MMD表示会把关联信息输出到.d文件中，-MF是指定用于存放关联信息的文件，-MP生成的依赖文件里面，依赖规则中的所有 .h 依赖项都会在该文件中生成一个伪目标，其不依赖任何其他依赖项。该伪规则将避免删除了对应的头文件而没有更新 “Makefile” 去匹配新的依赖关系而导致 make 出错的情况出现。\n关于这三个参数以及相关信息的介绍可以参考《GCC -M,-MM,-MMD,-MF,-MT》和《Linux Makefile 生成 *.d 依赖文件以及 gcc -M -MF -MP 等相关选项说明》。\n而后面那一堆宏，说实话我也不太看得懂，毕竟我对Makefile的语法研究不深，不过看样子意思大概是针对MF的，而且大概含义是把所有o文件的名字复制生成对应的d文件，然后配合前面的参数，把关联信息生成到这些d文件中，而且通过build中的确d文件和o文件基本可以对应上，也侧面应证了这个猜想。\nLDSCRIPTS LDSCRIPT = STM32F411CEUx_FLASH.ld 链接脚本，里面包括了链接时各程序的组织架构，以及包括堆栈地址大小等信息，这个东西是CubeMX生成的，所以里面其实很多东西你在CubeMX中都可以修改。这里的内容不只是修改芯片，只要修改了了中断向量、堆栈大小等信息，均需要修改此部分，它其实就代表了你的程序在芯片的ROM中是怎么存储的，感兴趣可以自行查看一下这个文件，我把我的这个文件复制上来给各位看一下（删除了一些无用的注释）：\n/* Entry Point */ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _estack = 0x20020000; /* end of RAM */ /* Generate a link error if heap and stack don\u0026#39;t fit into RAM */ _Min_Heap_Size = 0x200; /* required amount of heap */ _Min_Stack_Size = 0x400; /* required amount of stack */ /* Specify the memory areas */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 512K } /* Define output sections */ SECTIONS { /* The startup code goes first into FLASH */ .isr_vector : { . = ALIGN(4); KEEP(*(.isr_vector)) /* Startup code */ . = ALIGN(4); } \u0026gt;FLASH /* The program code and other data goes into FLASH */ .text : { . = ALIGN(4); *(.text) /* .text sections (code) */ *(.text*) /* .text* sections (code) */ *(.glue_7) /* glue arm to thumb code */ *(.glue_7t) /* glue thumb to arm code */ *(.eh_frame) KEEP (*(.init)) KEEP (*(.fini)) . = ALIGN(4); _etext = .; /* define a global symbols at end of code */ } \u0026gt;FLASH /* Constant data goes into FLASH */ .rodata : { . = ALIGN(4); *(.rodata) /* .rodata sections (constants, strings, etc.) */ *(.rodata*) /* .rodata* sections (constants, strings, etc.) */ . = ALIGN(4); } \u0026gt;FLASH .ARM.extab : { *(.ARM.extab* .gnu.linkonce.armextab.*) } \u0026gt;FLASH .ARM : { __exidx_start = .; *(.ARM.exidx*) __exidx_end = .; } \u0026gt;FLASH .preinit_array : { PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array*)) PROVIDE_HIDDEN (__preinit_array_end = .); } \u0026gt;FLASH .init_array : { PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array*)) PROVIDE_HIDDEN (__init_array_end = .); } \u0026gt;FLASH .fini_array : { PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(SORT(.fini_array.*))) KEEP (*(.fini_array*)) PROVIDE_HIDDEN (__fini_array_end = .); } \u0026gt;FLASH /* used by the startup to initialize data */ _sidata = LOADADDR(.data); /* Initialized data sections goes into RAM, load LMA copy after code */ .data : { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ *(.data) /* .data sections */ *(.data*) /* .data* sections */ . = ALIGN(4); _edata = .; /* define a global symbol at data end */ } \u0026gt;RAM AT\u0026gt; FLASH /* Uninitialized data section */ . = ALIGN(4); .bss : { /* This is used by the startup in order to initialize the .bss secion */ _sbss = .; /* define a global symbol at bss start */ __bss_start__ = _sbss; *(.bss) *(.bss*) *(COMMON) . = ALIGN(4); _ebss = .; /* define a global symbol at bss end */ __bss_end__ = _ebss; } \u0026gt;RAM /* User_heap_stack section, used to check that there is enough RAM left */ ._user_heap_stack : { . = ALIGN(8); PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _Min_Heap_Size; . = . + _Min_Stack_Size; . = ALIGN(8); } \u0026gt;RAM /* Remove information from the standard libraries */ /DISCARD/ : { libc.a ( * ) libm.a ( * ) libgcc.a ( * ) } .ARM.attributes 0 : { *(.ARM.attributes) } } 我们简单看一下，最开始有个ENTRY指令，里面参数是Reset_Handler，显然它指向了一个所谓的复位句柄，那么这个句柄是在哪里呢？对，就是在我们前面说的startup汇编文件中，也就是说我们的入口程序实际上就是针对的这个句柄，然后后面通过这个句柄开始的程序进行逐渐执行，初始化硬件向量表这些，再然后才是开始运行我们的main程序，如果你需要写一些什么Bootloader之类的东西，其实也就是在这里看和改。\n之后我们再继续查看后面的东西，可以发现我们还标注了RAM的大小、堆栈大小、地址等信息，这些信息都会最终被编译到我们的程序中，并且配合我们的烧录器或仿真器被烧录的芯片中的指定位置。\n再之后就是每个区段section了，这些是对你的程序存储的划分，一般情况下我们是不需要对这些东西进行修改的，因为大部分芯片程序的运行存储理念都是差不多的，所以普通情况下我们需要修改的东西基本上就是前面的那些（当然，如果你换了其它芯片的架构，那可能还是要好好研究一下的）。\nLIBS,LIBDIR LIBS = -lc -lm -lnosys LIBDIR = LIBS里面的这几个东西，代表的链接库，其实就是C语言的标准库，比如说这个-lm就代表了链接了标准数学库，不过具体这几个库链接的都是哪几个，我就不是很清楚了，具体可能要参考GCC的手册，但是我感觉没啥必要，毕竟都是标准库，链接了就链接了呗。除非你需要自己添加引用一些其他的链接库或者你需要缩减代码体积的时候，到时候再去研究哪个有用哪个没用就是了。\n而对于LIBDIR，很明显也是一个库地址的意思，如果你使用的是非标准库需要从外面引用的话，就需要在这里添加东西了，添加的方法和前面添加c和汇编库的方法类似。\nLDFLAGS LDFLAGS = $(MCU) -specs=nano.specs -T$(LDSCRIPT) $(LIBDIR) $(LIBS) -Wl,-Map=$(BUILD_DIR)/$(TARGET).map,--cref -Wl,--gc-sections 这个就是链接的参数。\n-specs=nano.specs替换精简c库以缩小代码大小。\n-T使用脚本文件xxx作为链接器脚本，此脚本替换ld的默认链接脚本（而非加进去），因此命令文件必须给出所有输出文件所需的东西，这里的链接脚本我们在前面已经说过了。\n-Wl如果链接器不是直接被使用的，而是被编译器使用（如gcc），则所有的链接命令前面必须加上-Wl参数（或其他特殊编译器对应的参数）。\n-MAP打印一个链接表到map文件中。\n--cref输出一个交叉引用表。如果一个链接映射文件正在被生成，交叉参照表将会被输出到映射文件中。否则，将会被输出到标准输出。\n--gc-sections允许对未使用的段使用垃圾收集。在不支持这个选项的目标上将被自动忽略。默认的行为可用’–no-gc-sections’恢复。\n关于这部分的内容可以参考《LD说明文档\u0026ndash;2.LD命令行命令翻译》\nall all: $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin 默认执行全部编译，生成elf、hex和bin文件，文件地址和文件名在前面已经说过了。\nOBJECTS OBJECTS = $(addprefix $(BUILD_DIR)/,$(notdir $(C_SOURCES:.c=.o))) vpath %.c $(sort $(dir $(C_SOURCES))) OBJECTS += $(addprefix $(BUILD_DIR)/,$(notdir $(ASM_SOURCES:.s=.o))) vpath %.s $(sort $(dir $(ASM_SOURCES))) 这一大段都是makefile的语法，本质上代表了文件替换和名称命名等功能，一般而言我们修改芯片工程或其它什么时候，都不需要修改这个地方。该部分内容较多，另有博客对这一部分进行了比较详细的解释《(GCC)STM32CubeMX生成的Makefile详解》。\nBUILD $(BUILD_DIR)/%.o: %.c Makefile | $(BUILD_DIR) $(CC) -c $(CFLAGS) -Wa,-a,-ad,-alms=$(BUILD_DIR)/$(notdir $(\u0026lt;:.c=.lst)) $\u0026lt; -o $@ $(BUILD_DIR)/%.o: %.s Makefile | $(BUILD_DIR) $(AS) -c $(CFLAGS) $\u0026lt; -o $@ $(BUILD_DIR)/$(TARGET).elf: $(OBJECTS) Makefile $(CC) $(OBJECTS) $(LDFLAGS) -o $@ $(SZ) $@ $(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf | $(BUILD_DIR) $(HEX) $\u0026lt; $@ $(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf | $(BUILD_DIR) $(BIN) $\u0026lt; $@\t$(BUILD_DIR): mkdir $@ 这一部分实际上前面那个博客也介绍得差不多了，但是他没有讲到里面的几个参数的功能。\n其中-c和-o这两个gcc最基础的参数就不说了，重点是第一部分中出现的-Wa,-a,-ad,-alms几个参数是什么意思。说实话，我最开始也没找到，翻了gcc的手册但是只找到关于Wa的一点描述，而且还不咋看得懂。但是解铃还须系铃人，我们想要知道这几个意思其实最好的办法就是去问一下STM32，然后我就去STM32的官方论坛问了一下，对方给出了答复，说明这几个参数其实就是针对GCC的汇编器的，主要功能是生成一些信息还有引用一些库之类的，一般情况下我们对其可以不做更改。关于这里的问答，可以跳转到论坛界面进行查看。\nclean clean: rm -fR $(BUILD_DIR) 这个东西没啥好说的，就是清除编译出来的那一大堆文件，一般情况下写makefile都需要这个东西防止生成东西太多影响空间，使用方法就是make clean。\nflash flash: openocd -f interface/cmsis-dap.cfg -f target/stm32f4x.cfg -c \u0026#34;program $(BUILD_DIR)/$(TARGET).elf reset exit\u0026#34; 烧录程序，这个东西是我自己添加的，需要使用openocd并且配置环境变量，如果没有配置环境变量需要修改一下openocd命令的路径，而如果没有下载openocd的就无法使用这个功能了，这个功能不是必须的，如果需要可以直接使用stm32官方提供的烧录工具。\ndependencies -include $(wildcard $(BUILD_DIR)/*.d) 依赖引用，这里引用就是前面我们生成的关联文件，这些东西有什么用呢，其实目的就是为了生成我们的o文件。如果你看不懂这个东西什么意思，那么我们随便打开一个d文件其实你应该就懂了（如果你看过一点makefile基础）：\nbuild/gpio.o: Core/Src/gpio.c Core/Inc/gpio.h Core/Inc/main.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h \\ Core/Inc/stm32f4xx_hal_conf.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_def.h \\ Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h \\ Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f411xe.h \\ Drivers/CMSIS/Include/core_cm4.h Drivers/CMSIS/Include/cmsis_version.h \\ Drivers/CMSIS/Include/cmsis_compiler.h Drivers/CMSIS/Include/cmsis_gcc.h \\ Drivers/CMSIS/Include/mpu_armv7.h \\ Drivers/CMSIS/Device/ST/STM32F4xx/Include/system_stm32f4xx.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/Legacy/stm32_hal_legacy.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc_ex.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio_ex.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma_ex.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash_ex.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash_ramfunc.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr_ex.h \\ Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h Core/Inc/gpio.h: Core/Inc/main.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal.h: Core/Inc/stm32f4xx_hal_conf.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_def.h: Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f4xx.h: Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f411xe.h: Drivers/CMSIS/Include/core_cm4.h: Drivers/CMSIS/Include/cmsis_version.h: Drivers/CMSIS/Include/cmsis_compiler.h: Drivers/CMSIS/Include/cmsis_gcc.h: Drivers/CMSIS/Include/mpu_armv7.h: Drivers/CMSIS/Device/ST/STM32F4xx/Include/system_stm32f4xx.h: Drivers/STM32F4xx_HAL_Driver/Inc/Legacy/stm32_hal_legacy.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_rcc_ex.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_gpio_ex.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_exti.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_dma_ex.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_cortex.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash_ex.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_flash_ramfunc.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_pwr_ex.h: Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h: 这个是gpio.d，看着是不是很熟悉，感觉和前面那个《(GCC)STM32CubeMX生成的Makefile详解》说的把生成指令展开后C_INCLUDES、C_SOURCES生成的那一套很像？像就对了，这东西就是为了生成.d文件最顶部那个目标文件而存在的，而这里的gpio.d文件中所指的其实就是gpio.o，其余同理。\n结语 好了，看到这你应该基本上理解了这个Makefile内部的功能和作用了，配合我找的这些资料以及官方文档，相信你可以学到更多。\n本文的存在更多是类似一个学习笔记，而且本人本身才疏学浅，里面内容不免可能会存在一些矛盾错误或疏漏的地方，如果发现欢迎指出。\n","date":"2022-03-08T00:30:45+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/stm32cubemx-gcc%E5%B7%A5%E7%A8%8Bmakefile%E5%86%85%E5%AE%B9%E8%AF%A6%E8%A7%A3/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/stm32cubemx-gcc%E5%B7%A5%E7%A8%8Bmakefile%E5%86%85%E5%AE%B9%E8%AF%A6%E8%A7%A3/","title":"STM32CubeMX GCC工程Makefile内容详解"},{"content":"本文是基于廖雪峰大佬的Git教程进行的记忆笔记，主要目的是用于快速回忆和精炼Git的一些常用命令，同时补充结合一些网络上的其它关于git的知识，本笔记不适合直接用来零基础学习，如果需要可以通过前面的链接跳转至廖雪峰的教程。本笔记可能存在疏漏，如有问题欢迎指正。\n安装 仓库管理（repository） 创建仓库 克隆仓库 版本控制 缓存修改 提交修改 查看状态 内容比较 版本日志 撤销修改 删除文件 版本回退 远程操作 添加关联 查看关联 取消关联 上传代码 下载代码 分支管理 连接分支 创建分支 切换分支 合并分支 删除分支 查看分支 现场管理 保存现场 查看现场 恢复现场 删除现场 获取修改 标签管理 添加标签 查看标签 删除标签 推送标签 安装 Linux直接用apt安装，Windows官网下载安装，Mac用Xcode安装，安装完毕后在命令行使用如下内容进行设置：\ngit config --global user.name \u0026#34;Your Name\u0026#34; git config --global user.email \u0026#34;email@example.com\u0026#34; 若要使用Github或Gitee等远程仓库，其初始化过程请在往上自行搜索。\n仓库管理（repository） 创建仓库 git init：将一个文件夹初始化为git仓库，可以在命令行中进入该文件夹并使用此命令。\n克隆仓库 git clone \u0026lt;URL\u0026gt; \u0026lt;name\u0026gt;：将远程仓库拷贝到自己电脑上，其中\u0026lt;URL\u0026gt;为远程仓库地址，\u0026lt;name\u0026gt;为赋值后对该项目进行重命名（文件夹的名字）。\n版本控制 缓存修改 git add \u0026lt;file\u0026gt;：将工作区修改过的文件存入缓存区，其中\u0026lt;file\u0026gt;为需要缓存的文件，可同时添加多个文件，也可以添加文件夹，若输入git add .则将该路径下所有的内容全部添加。\n提交修改 git commit \u0026lt;file\u0026gt; -m \u0026lt;message\u0026gt;：将缓存区的内容提交至本地仓库，其中\u0026lt;file\u0026gt;为需要提交的文件，可多个或文件夹，若不填写则全部提交。-m \u0026lt;message\u0026gt;为添加注释内容，如果在其中添加-a指令，如git commit -am \u0026lt;message\u0026gt;，则可以跳过add步骤，直接工作区内容提交到本地仓库中。同时每次commit也相当于提交了一个新的版本。\n查看状态 git status：查看当前仓库的状态，例如有无未缓存或未提交之类的信息。\n内容比较 git diff：比较工作区和暂存区的差异，可找到例如未缓存但已修改的内容。添加--cached参数则比较暂存区与上一次提交之间的差异，其它参数可在网上查询。\n版本日志 git log：查询过去的版本，包括日期、作者、版本号等信息，添加--pretty=oneline可以精简上述信息至只有版本号和你commit时添加的注释。\ngit reflog：查询未来的版本，即你曾经回退过版本，现在想取消回退，可以通过这个命令进行查询。\n撤销修改 git checkout -- \u0026lt;file\u0026gt;：将工作区的文件恢复为之前暂存区存储的内容。\ngit reset HEAD \u0026lt;file\u0026gt;：将暂存区某文件的内容恢复为之前仓库存储的内容。\n删除文件 git rm \u0026lt;file\u0026gt;：将文件从暂存区和工作区删除，若未使用该语句而直接将文件删除，则暂存区仍然会保留该文件，且在查询状态时告知你删除了文件，此时可以使用撤销修改进行恢复。若删除之前进行过修改，缓存至了暂存区且尚未提交，则此命令需要使用-f参数才可进行强制删除。若仅想删除暂存区内的修改但保留工作区的文件，则需要添加--cached参数。\n版本回退 git reset \u0026lt;--soft|--mix|--hard\u0026gt; \u0026lt;commit_id\u0026gt;：将仓库重置至另外某个版本的状态。\n其中\u0026lt;--soft|--mix|--hard\u0026gt;为三选一参数：\n--soft为变更HEAD所指向的版本（HEAD版本即为分支顶端的别名）。\n--hard为除了变更HEAD所指向的版本外，还会把工程的一切都变成该历史版本的样子，该操作有丢失掉当前所有修改的风险。\n--mix为除了变更HEAD所指向的版本外，还会将暂存区重置以匹配HEAD，但工作区不会改变。\n其中\u0026lt;commit_id\u0026gt;为版本号，通过版本日志可以查询，若设置为HEAD则为当前版本，若使用HEAD^可退回上一个版本，使用HEAD^^或HEAD^2可退回上上个版本，通过增加^的数量或增加数字，可退回的层数以此类推增长。\n远程操作 添加关联 git remote add \u0026lt;name\u0026gt; \u0026lt;URL\u0026gt;：将本地已有仓库与一个远程仓库进行关联，其中\u0026lt;name\u0026gt;为对该远程仓库取得别名，常用origin作为该别名，\u0026lt;URL\u0026gt;为远程仓库的地址。\n查看关联 git remote -v：查看当前全部已关联库。\n取消关联 git remote rm \u0026lt;name\u0026gt;：删除某个关联的\u0026lt;name\u0026gt;分支。\n上传代码 git push \u0026lt;name\u0026gt; \u0026lt;branch\u0026gt;：将本地仓库名为\u0026lt;branch\u0026gt;的分支上传至远程\u0026lt;name\u0026gt;仓库并合并，若两边分支名不同则应使用\u0026lt;brach_local\u0026gt;:\u0026lt;branch_origin\u0026gt;分别填写上两边的分支名然后上传，若两边版本差异则需要添加--force参数强制推送，若想未来简化git push操作，可在\u0026lt;name\u0026gt;前添加-u参数，未来git push即可直接代替git push \u0026lt;name\u0026gt; \u0026lt;branch\u0026gt;指令。\n下载代码 git pull \u0026lt;name\u0026gt; \u0026lt;branch\u0026gt;：将远程\u0026lt;name\u0026gt;主机\u0026lt;branch\u0026gt;分支的代码合并到本地，也可使用\u0026lt;branch_origin\u0026gt;:\u0026lt;branch_local\u0026gt;来确定分支，当团队之间合作代码push推送失败时，可以使用git pull来获取最新的提交，然后本地修改合并后再推送。注意，pull操作会直接合并，如果在合并前需要对照之类的操作应该使用fetch。\n分支管理 连接分支 git branch --set-upstream-to \u0026lt;branch\u0026gt; \u0026lt;name\u0026gt;/\u0026lt;branch\u0026gt;：如果pull失败并提示no tracking information，则说明本地分支和远程分支没有连接，使用上述方法方法即可完成连接。\n创建分支 git branch \u0026lt;name\u0026gt;：创建一个名为\u0026lt;name\u0026gt;的分支，创建分支并不会对之前分支的工作区产生改变。\ngit switch -c \u0026lt;branch\u0026gt; \u0026lt;name\u0026gt;/\u0026lt;branch\u0026gt;：跟踪一个远端\u0026lt;name\u0026gt;主机的\u0026lt;branch\u0026gt;分支到本地。\n切换分支 git checkout \u0026lt;name\u0026gt;：切换到\u0026lt;name\u0026gt;分支，若中间加上-b参数，则可以创建并切换。\ngit switch \u0026lt;name\u0026gt;：新版git提供的新指令，用于替代checkout，切换到\u0026lt;name\u0026gt;分支，若中间加上-c参数，则可以创建并切换。\n合并分支 git merge \u0026lt;name\u0026gt;：将\u0026lt;name\u0026gt;分支合并到当前分支，如果合并产生了冲突，则需要手动合并冲突（即手动将文件编辑为我们想要的内容，git将在文件合并冲突位置处添加符号标注）。合并时如果加上--no-ff参数即可使用普通模式合并，这样合并后的历史有分支，可以看出曾经做过合并，而如果不加则看不出来曾经做个合并。\n删除分支 git push \u0026lt;name\u0026gt; --delete \u0026lt;branch\u0026gt;：删除远端\u0026lt;name\u0026gt;仓库的\u0026lt;branch\u0026gt;分支。\ngit branch -d \u0026lt;branch\u0026gt;：删除本地\u0026lt;branch\u0026gt;分支，在删除前需要先切换到其它分支，若d改为大写D，则为强制删除。\n查看分支 git branch：查看本地分支。\ngit branch -r：查看远端分支。\n现场管理 保存现场 git stash：当急需继续对某另一分支进行操作而当前任务尚未结束无法提交，可以先保存现场，去处理完其它任务后再回来重新工作，保存现场后当前工作区将会恢复为上一次commit时的状态，直到恢复现场。\n查看现场 git stash list：查看现在已保存的现场，每个保存的现场均会拥有一个id进行标识。\n恢复现场 git stash pop：恢复现场并删除保存记录，如果产生冲突，文件会出现和合并时类似的改变。\ngit stash apply \u0026lt;id\u0026gt;：恢复id为\u0026lt;id\u0026gt;的现场但不会删除记录，需要手动删除\n删除现场 git stash drop \u0026lt;id\u0026gt;：删除id为\u0026lt;id\u0026gt;的现场。\n获取修改 git cherry-pick \u0026lt;commit_id\u0026gt;：从其它地方获取更改，但不同于merge，其获取的更改是以commit为单位而非分支，所以\u0026lt;commit_id\u0026gt;即为所交commit的id，可以在对应分支通过log查询。\n标签管理 添加标签 git tag \u0026lt;TAG\u0026gt;：为一个commit打上标签，\u0026lt;TAG\u0026gt;为标签内容。通过git tag -a \u0026lt;TAG\u0026gt; -m \u0026lt;message\u0026gt; \u0026lt;commit_id\u0026gt;可以为id为\u0026lt;commit_id\u0026gt;的提交打上标签\u0026lt;TAG\u0026gt;并添加说明文字\u0026lt;message\u0026gt;。\n查看标签 git tag：可以查看历史打过的标签。\n删除标签 git tag -d \u0026lt;TAG\u0026gt;：删除名为\u0026lt;TAG\u0026gt;的标签。\n推送标签 git push \u0026lt;name\u0026gt; \u0026lt;TAG\u0026gt;：标签本身不会自动上传到远端服务器，通过此指令可以将\u0026lt;TAG\u0026gt;推送到远端\u0026lt;name\u0026gt;主机上。\ngit push \u0026lt;name\u0026gt; :refs/tags/\u0026lt;TAG\u0026gt;：通过本指令可以将已上传至远端的标签删除。\n","date":"2022-01-15T21:07:07+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/git%E7%AC%94%E8%AE%B0/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/git%E7%AC%94%E8%AE%B0/","title":"Git笔记"},{"content":"Configuring DSP libraries on STM32CubeIDE\n","date":"2021-11-05T14:06:52+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%B7%A5%E5%85%B7%E9%93%BE/%E5%A6%82%E4%BD%95%E5%9C%A8stm32cubeide%E4%B8%AD%E4%BD%BF%E7%94%A8cmsis-dsp%E5%BA%93%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E5%9C%B0%E5%9D%80/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%B7%A5%E5%85%B7%E9%93%BE/%E5%A6%82%E4%BD%95%E5%9C%A8stm32cubeide%E4%B8%AD%E4%BD%BF%E7%94%A8cmsis-dsp%E5%BA%93%E5%AE%98%E6%96%B9%E6%96%87%E6%A1%A3%E5%9C%B0%E5%9D%80/","title":"如何在STM32CubeIDE中使用CMSIS DSP库"},{"content":" 注意 视频 再观printf函数 print为什么要加个f 格式符是什么时候被解析的？ 如何实现变参函数 来实现一个printf吧 printf是否有检测功能 printf只能输出在命令行吗 结语 注意 本文为针对C语言的基础技术向，更适合刚入门需要进阶的同学技术进阶使用，当然我会尽量用简单的语言让哪怕是初学者也可以看得懂，核心知识有如下部分：\n变参函数的概念和使用 __attribute__(())的概念和使用 printf函数的实现 视频 本文可以结合视频一起食用，效果更佳：视频链接\n再观printf函数 相信绝大部分同学写的第一个程序，应该就是大名鼎鼎的Hello World程序了，这个程序闻名于著名的《C语言程序设计(The C Programming Language)》（C语言之父丹尼斯·里奇（Dennis Ritchie）著）一书中，非常经典，以至于程序员们学习任何语言，都会先用这个语言运行个Hello World出来。\n在C语言中，这个程序一般写出来是这个样子的：\n#include \u0026lt;stdio.h\u0026gt; int main() { printf(\u0026#34;Hello World!\\n\u0026#34;); return 0; } 如果在命令行运行这个程序的话，你将获得一行Hello World!输出。\n如果你是一个不喜欢思考的初学者，可能会认为printf也许是C语言里面最基础最简单的函数了，毕竟这个程序这么简洁、格式这么规范、使用这么简单、甚至闭着眼睛都不会写错。\n但如果我们往细究往深了想呢？printf它是如何实现的呢？我们是否可以实现一个自己的printf函数呢？为什么它会运行在命令行中而不是从屏幕某个规定的位置显示呢……\n这时你可能会发现，woc，printf似乎比我们想象的要复杂太多了，你以为的简单小函数，其实却是个超级大boss。不过不要担心，我们会从基础开始讲，而且我会保证尽量说得简单明了。\nprint为什么要加个f 现在，我们重新抱着认真学习的态度来观察printf这个函数吧。首先我们从名字来解析一下。\n我们知道，在英语中，print有打印、印刷的意思，这个词语完全可以解释将字符串打印到电脑屏幕上这个过程，那么为什么这个函数的名字要加上f呢？\n其实这个f是format的缩写，即格式化的意思，printf其实为格式化打印的意思。那么格式化这个是怎么来的？\n现在想想，如果我们希望在屏幕上输出一个变量该怎么做呢？最简单的方法就是使用格式字符，就如下面这个程序一样：\n#include \u0026lt;stdio.h\u0026gt; int main() { int a = 25; printf(\u0026#34;a = %d\\n\u0026#34;, a); return 0; } 这个函数将会在屏幕上输出一行a = 25，在这个过程中%d就是变量a的格式符，它在字符串中占位，然后字符串在输出的时候，遇到了这个占位符，就会解析a的具体值，并把它转换为字符串的形式输出出来。\n现在从这个功能中我们其实可以发现很多问题，我们来逐一解释：\n格式符是什么时候被解析的？ 这个问题是我在学习理解printf的时候第一个思考的问题，即格式符是如何什么时候被解析的，答案有两种，对应了两种不同的结果。\n首先是在编译阶段被解析，这种方式的话也有两种情况，一是编译器有针对printf这类函数的优化，二是编译器会主动把字符串中的格式符变成某种执行效果。而另一种方案是printf函数内部实现的解析格式符。\n我们可以用如下的程序来测试（为了方便，后面我们生成程序的名称都为demo.exe）：\n#include \u0026lt;stdio.h\u0026gt; int main(int argc, char* argv[]) { int a = 1, b = 2; char* str = \u0026#34;a=%d\\n\u0026#34;; printf(str, a); printf(argv[1], b); putchar(\u0026#39;\\n\u0026#39;); return 0; } 然后我们把它们编译完成后，在命令行中使用./demo.exe b=%d来运行（如果使用的某种IDE，可以打开工程文件夹，找到输出exe文件，然后在文件夹中按住shift+右键，打开powershell，然后用./文件名.exe b=%d的方式运行即可）。\n最后我们会得到两行输出a=1和b=2。\n对于上面的程序，我们来理解一下：\n其中首先是main函数的部分，可以发现我们在里面写了两个参数，其实这种写法才是C语言main函数的标准写法，具体可以参考我之前写的《C的main函数解析》这篇博客，这里不作详述，这里只说一下其中第八行的argv[1]其实就代表了我们开启程序时输入的第一个参数字符串，在我们的示例里面也就是b=%d一句。那么我们从第一个输出a=1可以看出，这里的输出语句并不是直接写在printf函数里的，但是占位符仍然有效，而第二个b=2则表明了我们通过外部未知的字符串中的占位符也实现了占位符效果。故可知占位符实际上是在printf中被解析的。\n既然知道了这个，那么printf解析的方案其实也就很容易出来了，程序通过检测百分号%寻找占位符，并根据不同的占位符将数字转换成字符串。\n如何实现变参函数 既然知道了如何解析格式符，那么我们似乎就可以实现printf函数了？可是等一下，我们在写printf的时候为什么可以写多个参数？\n一般而言，如果我们仅仅跟着学校学习的那点C语言知识，肯定不会讲什么变参函数的，但是printf这个函数的存在却离不开变参函数这个概念。而且，配合变参函数，你还可以实现一些类似于其它更加高级语言的重载、默认参数等功能。\n如果需要实现变参函数，那么有一个库文件就很关键了，那就是stdarg.h。你首先需要包含这个库，然后按照规定的写法就可以实现变参函数了。例如下面我就写了个简单的变参整数加法函数：\n#include \u0026lt;stdarg.h\u0026gt; int MySum(int count, ...) { va_list num;\t// 一个宏定义类型，通过它我们可以访问参数数据 int sum = 0; va_start(num, count);\t// 确定起始参数 for(int i = 0; i \u0026lt; count; i++) { sum += va_arg(num, int);\t// 从起始参数开始取一个int类型的数据并加到sum中，同时num将会指向下一个参数 } va_end(num);\t// 释放 return sum; } 通过这个函数我们即可以实现一个变参的加法。我们来看一下这里的写法，其实很简单，除了格式要求在函数的形参列表末尾加上三个点表示这是个变参函数外，重点也就一个数据类型和三个函数。\n不过为了防止看不懂，我们来从原理上理解一下这个功能。（此部分仅作参考，我并没有实际去看编译器的内容，而源文件也被隐藏起来了，故下面的内容大多是基于我自己的分析）。\n首先是三个点的作用，很显然这个东西是给编译器看的，因为如果不加三个点，那么我们使用函数的时候，实参与形参个数对不上，编译器就会报错，而这三个点就可以告诉编译器，这是个变参函数，实参多了少了你别叫唤。\n然后是一个数据类型即va_list，这个类型从某种意义上感觉应该是个空指针，这个指针可以用来在栈中取数据。当然它也可能是个结构体，但至少绝对拥有指针的功能。\n而剩下三个函数中，第一个调用的是va_start，它应该是用来对va_list这个指针赋初值。这里我认为应该是程序在编译运行后，函数的实参应该是放在一个栈里的，并且是有规律有顺序的存放的，那么我们要取一个数据，只要确定了这个数据的首地址（va_list指向的位置）和长度（数据类型），就可以把数据正确取出来了，取出来后因为数据被整齐地排列着，所以指针被完美地跳转到下一个实参的首地址。而va_start其实作用就是确定这个实参列表整体的首地址。\n然后是第二个函数，在程序中被反复调用的va_arg函数，显然通过我们刚才的分析，应该不难知道这个函数其实就是用来取数据同时移动指针的，在这个函数的参数中，除了要取数据的va_list，还包括了要取的数据类型（虽然不知道为什么这个数据类型可以直接被写成类型的名称形式，应该是宏定义的一种用法），然后通过这个函数，我们就可以按照格式取出数据，同时把va_list跳转到下一个参数的位置。\n最后第三个函数，我们可以调用va_end把指针指向一个空地址，防止野指针对内存造成破坏。\n除此之外，其实stdarg中还有很多其它的函数，不过我们平常不大用得到，如果感兴趣的话可以自行研究研究。\n现在，我们来试试刚刚写的程序吧，在主函数中如此调用：\nint result = MySum(5, 1, 2, 3, 4, 5); printf(\u0026#34;%d\\n\u0026#34;, result); 最后我们可以得到一个15的输出。\n来实现一个printf吧 现在我们来实现一个printf吧，为了方便我用比较简单潦草的程序只写了一个%d的识别，而且用了一些goto语句，不过总体来说还是可以看懂的。\nvoid MyPrint(char format[], ...) { va_list args; va_start(args, format); int i = 0; char intStr[10] = {0}; while (1) { switch (format[i]) { case \u0026#39;%\u0026#39;: i++; switch (format[i]) { case \u0026#39;d\u0026#39;: itoa(va_arg(args, int), intStr, 10); for(int i = 0; i \u0026lt; sizeof(intStr); i++) { if(intStr != \u0026#39;\\0\u0026#39;) { putchar(intStr[i]); } else { break; } } i++; break; default: putchar(\u0026#39;%\u0026#39;); putchar(format[i]); i++; break; } break; case \u0026#39;\\0\u0026#39;: goto OUT; default: putchar(format[i]); i++; break; } } OUT: return; } 上面这个程序其实你们自己都可以写，如果还是有点不懂的话可以仔细读一下，里面用的函数都是非常基础的API，现在我们测试一下：\nMyPrint(\u0026#34;%d %d\\n%c %x\u0026#34;, 1, 2, 3); 这个程序可以用来测试MyPrint的以下功能：\n正常输出打印字符串 格式字符%d是否有用 %c和%x应该没用 %c和%x的存在不会影响编译警告，同时也不会读取更多或更少的参数 现在，只要我们继续填充内容，就可以实现自己的printf功能了，甚至可以添加一些自己的格式符，来实现一些更加复杂高级的输出。\nprintf是否有检测功能 我们经过刚才的研究发现，在使用printf的时候哪怕格式字符和数据的个数对不上号，我的编译器似乎也不会报错（这里仅指代MinGW，如果你使用的codeblocks或者其它开源IDE，可能结果与我一样），但是有的编译器不一样，例如针对嵌入式的gcc-arm-none-eabi编译器，或者一些其它的IDE自研编译器，它们有可能会报错或警告，为什么呢？printf是否拥有某种检测功能，来防止我们错误地多填或者少写实参呢？事实上是有的，不过有的编译器没有用或者没有实现，这种功能一般专属于GNU版本的编译器，那就是使用__attribute(())_功能。\n我们知道，平常我们编程的时候，编译器说什么，我们就得听什么，必须完全听从编译器的指挥。从绝大部分情况来看，它很方便也很可靠，毕竟不用动脑子按规范来就行。但是C语言是一个非常贴近硬件的语言，我们很多时候可能会完成一些匪夷所思的操作来实现某种功能，还有时候我们有些操作非常危险需要用一种方法来时刻提醒我们的大脑，那么我们就可以使用__attribute__了，这个关键字可以被编译器识别，然后根据其参数，编译器将会执行相应的操作。从另一种方面来讲，__attribute__是专门针对编译器的指令，我们可以用它实现一些很方便的功能，最经典的就是__attribute__((weak))了，这个功能可以让函数被弱化，如果在同一个程序中遇到了两个同名同参同返回的函数，那么编译器就会去编译没被弱化或者说更强的那一个函数，这样我们就可以实现函数的重写覆盖功能。\n当然，__attribute__的用法其实有很多，我们再举一个常用的例子，也就是__attribute__((pack))这个功能，它可以把结构体尽量地压缩到最小体积（平常为了保证速度，结构体会按照速度最优的方案来存储）。\n而这里，我们同样可以使用__attribute__((format(printf, 1, 2)))，来实现对printf的格式检测，这样如果我们的格式符和实参个数不对应，就可以被检测到了。（不过MinGW好像没法实现这个功能，如果有使用gcc-arm-none-eabi的同学可以试试）\nprintf只能输出在命令行吗 目前我在Windows上使用MinGW测试，暂时没有发现其printf有重定向功能，不过这个应该是为了安全考虑，毕竟你在Windows上实现在其它地方打印，一般需要使用Windows提供的API，否则很容易出问题，而如Linux这些系统，大多也是如此。\n不过对于嵌入式来说，重写pritnf就是很平常很普通的任务了，我们一般会把printf重写在串口上，但是在写了屏幕驱动后，也可以把printf重新封装到屏幕上，而Windows运行的printf应该就是把printf连接在了Windows的屏幕驱动中，然后通过调用驱动的方式来实现的数据打印功能。\n本质上printf其实可以当作连续按照格式使用putchar的函数，而putchar也就打印字符的函数，那么我们只要可以确定每个putchar打印的位置，那么printf的使用就迎刃而解了。\n结语 现在你已经基本懂得了printf是如何实现的了，那么用这个方法反推，其实你应该也可以实现scanf的功能，因为它们的思想都是大致相同的。从觉得printf是一个非常简单的函数，到觉得printf好像非常麻烦，再到我们现在将它几乎完整地剖析了一遍，现在回头看，它的实现机理其实并不算简单，但也没那么复杂，重点是要主动去思考，去想，去理解，去实验。\n","date":"2021-08-19T21:02:23+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/%E6%B7%B1%E5%BA%A6%E5%BA%95%E5%B1%82%E5%89%96%E6%9E%90%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84printf/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/%E6%B7%B1%E5%BA%A6%E5%BA%95%E5%B1%82%E5%89%96%E6%9E%90%E4%BD%A0%E6%89%80%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84printf/","title":"深度底层剖析，你所不知道的printf"},{"content":"在学习LwIP的时候，发现源码中出现了#define LWIP_MEMPOOL(name, num, size, desc) MEMP_##name这样的语句，了解到了C编译器中##这个连接符。顺便查了一下资料，还发现了#这个字符转换符。\n功能 ##可以作为宏定义中的变量替换，#则作为字符替换，一般他们都是用于带参宏定义中。\n比如：\n#define func1(a) printf(#a\u0026#34;\\n\u0026#34;) #define func(num, a) func##num(a) int main() { func1(hello); func(1, hello); return 0; } 上述程序将会输出两个hello。\n第一个hello源自于func1(hello);这句话因为宏定义将会变成printf(“hello”);其中hello之所以变成字符串，就是因为在宏定义的时候使用了#，另外相邻且未被任何字符隔开的两个字符串在C中会被合并在一起，故这里相当于还加上了一个回车。\n第二个hello源自于func(1, hello);这句话因为宏定义首先会变成func1(hello);其中之所以参数1会出现在变量名中，就是因为在宏定义中使用了##，它相当于将宏定义中的参数num变成了真正Token中的内容。Token可以理解为我们在C语言中写的变量名、函数名等等。然后func1(hello);又变成printf(\u0026quot;hello\u0026quot;);。\n应用 它们的应用不在于提高代码效率或缩减体积，而在于简化编程难度和提高易读性。\n比如，我有一堆字符串口令，为了方便，我对它们进行了宏定义：\n#define TEST_HELLO \u0026#34;Hello\u0026#34; #define TEST_BYE\t\u0026#34;Bye\u0026#34; #define TEST_NAME1\t\u0026#34;Tom\u0026#34; #define TEST_NAME2\t\u0026#34;Jerry\u0026#34; 现在，我希望把这几个口令以一定的方式打印出来，最直接的方法就是如下：\nprintf(\u0026#34;%s %s %s %s\u0026#34;, TEST_HELLO, TEST_NAME1, TEST_BYE, TEST_NAME2); 不过上述的方式太单一了，而且封装程度不好，如果我们未来想到一个新的问候词要替换呢？可能有人想到用这样的方法来替换：\nvoid SaySomething(char* sth1, char* sth2, char* name1, char* name2) { printf(\u0026#34;%s %s %s %s\u0026#34;, sth1, name1, sth2, name2); } 这样封装后我们使用：\nSaySomething(TEST_HELLO, TEST_NAME1, TEST_BYE, TEST_NAME2); 就可以完成上面的工作了，但是似乎还是太麻烦而且可扩展性以及封装度有点差，这时我们可以观察上面的所有问候语都是以TEST_开头的且后面的问候句与下划线后的文字直接相关，而人的名字则以TEST_NAME开头，然后接上一个数字编号，那么我们是否可以依靠这个来做点什么呢？\n#define SaySomething(sth1, sth2, name1, name2) printf(#sth1\u0026#34; %s \u0026#34;#sth2\u0026#34; %s\u0026#34;, TEST_NAME##name1, TEST_NAME##name2) 通过这样一个封装，我们就可以实现前面的功能了，用法如下：\nSaySomething(Hello, Bye, 1, 2); 之所以它们可以连接在一起是因为C语言中直接相连的两个字符串是会被视为一个整体的。\n由此可见当我们宏定义了一大堆功能类似名字类似且可以相互替换的常量时，#和##就变得非常有用了。\n效果图 ","date":"2021-06-16T18:22:21+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E5%AE%8F%E5%AE%9A%E4%B9%89%E8%BF%9E%E6%8E%A5%E7%AC%A6%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E5%AE%8F%E5%AE%9A%E4%B9%89%E8%BF%9E%E6%8E%A5%E7%AC%A6%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/","title":"C宏定义连接符#和##及其应用"},{"content":"这是在一次研讨会中发现的我知识上的纰漏，在此进行记录。\n当时想给学弟学妹讲一下关于动态内存的问题，在讲述时我用了一个初始化变量时不能用变量作为下标。例如：\nint i = 5; char test[i]; 然而事实上，在这样写了之后我发现编译没有出错，这和我最开始学C语言时老师给我们讲的内容并不相同。我最初怀疑时因为此时i已经被赋值了而后在此期间没有进行计算，所以编译器可能将其直接作为一个宏定义来解析了。但是我们又尝试了如下两种写法。\nint i = 5; i++; char test[i]; int i; scanf(\u0026#34;%d\u0026#34;, \u0026amp;i); char test[i]; 此时编译器仍然没有报错，而且生成的程序运行起来也是正常的。故而这样的写法其实是正确的，其运行逻辑应该是这种语句在被编译的时候成为了类似于malloc的语句，同时在后面的某个位置自动添加了free，不过因为我对汇编并不是特别熟，只能从理论层面大概分析一下，具体内容各位可以使用反汇编自行查看。\n不过需要注意的是，在定义的时候不能直接赋值：\nint i = 5; char test[i] = {0, 1, 2, 3, 4}; 原因可能在于等号赋值是先右后左，虽然在普通直接使用常量定义数组的时候没有关系，但那应该是在编译状态就可以计算出的内容，只要赋值的长度不超就没事，而这种变量赋值则类似于一种动态分配，编译器并不能预测在运行时这个变量究竟是多少，哪怕你知道它只能是这么多，但是编译器并没有那么智能，不如就直接报错处理了。\n考虑到最开始学到这个知识的时候老师讲课用的书是谭浩强的那本，限于时代因素那本书使用的C语言标准应该是在C89左右，而当时实验课我们使用DEV-C++进行编译也会遇到这种问题，故而我怀疑可能在C99或者C11的版本中发生了某种改变，毕竟我现在用的是C11的gcc了。不过具体是什么时候不重要，记住这个结论就好了。\n另外我真认为各高校确实该换本教科书了……\n","date":"2021-04-19T17:05:43+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E8%AF%AD%E8%A8%80%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E5%8F%98%E9%87%8F%E5%AE%9A%E4%B9%89%E6%95%B0%E7%BB%84/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E8%AF%AD%E8%A8%80%E5%8F%AF%E4%BB%A5%E4%BD%BF%E7%94%A8%E5%8F%98%E9%87%8F%E5%AE%9A%E4%B9%89%E6%95%B0%E7%BB%84/","title":"C语言可以使用变量定义数组"},{"content":"因为我是电子专业的，学校没教过C#，学下来也基本上都是野路子东看西看基于需求地乱学，故而对C#的理解并不是很深。然而个人项目需要自定义一个控件来实现动态连线功能，不得不从入门直接跳到高级应用进行学习，这里做一个记录以对后面有类似需要的朋友一个参考。目前只是实现了一个简单的变色按钮功能，但是需要使用的大部分技术都用上了，如果需要实现其它更复杂功能的话就把本文当个抛砖引玉吧。\n本文参考了《[C#] （原创）一步一步教你自定义控件——01，TrackBar》,但是在其基础上增加了设置控件区域的功能。另外因为那篇文章被很多人盗用，原文已经不可考，所以不贴地址给他们引流了，如果有需要可以直接百度这个名字即可。\n新建工程 新建一个项目，选择Windows窗体控件库(.NET Framwork)，然后选择地址、命名。\n正常情况下工程里应该有一个UserControl1.cs文件，不要它，直接删除，然后新建一个没有模板的类。\n将此类的访问属性改为public并且继承自Control，如果Control下出现波浪线报错的话是因为没有引用其命名空间，可以按住Alt + Enter后选中第一个命名空间再按一次Enter进行修改，VS将自动添加上using System.Windows.Forms;。\n添加属性 然后我们根据需求进行添加属性，因为这个功能只是需要实现一个圆形开关，那么我们需要知道的属性就有开关打开时的颜色，开关关闭时的颜色，圆形开关的直径，开关的状态四个属性。颜色使用Color类进行定义，直径使用int或者float进行定义，开关状态用布尔进行定义，并且给他们赋值上初始值。\nprivate Color openColor = Color.FromArgb(128, 255, 128);\t// 开启默认浅绿色 private Color closeColor = Color.FromArgb(255, 128, 128);\t// 关闭默认浅红色 private int diameter = 20;\t// 直径默认20，且为整数类型 private bool isOpen = false;\t// 默认关闭 然后为他们添加上各自的访问器：\n我们可以注意到，他们各自的get访问器其实没啥特别的，但是set访问器中除了基本的赋值以外，每个访问器中都有Invalidate();这么一句，它的工作目的是重新绘制一遍控件。\n一般来说，如果你调整的这个属性将会影响到当前控件的外观，那么就有必要加上这么一句，它的功能类似于将原本的控件从你的屏幕上删除，然后根据新的定义创建一个新的控件。\n然后我们再观察直径的访问器中使用了Size = new Size(diameter, diameter);这么一句话，它的目的是什么呢？其实从语义上就可以推测知道，是重新赋值了大小属性，其中宽度和长度变成了直径的。但是理论上其实不加这句也没有问题，因为我们后面会使用一个指定边界函数实现相同功能，但是如果不加上的话我们在使用Winform的设计模式的时候就容易出现bug，调整大小的时候，如果调大就会出现内容消失的情况，需要手动去调整一下才能重新刷新出来。我理解的原因是设计模式其实开启了一个模拟窗口，但是这里我们修改属性值的时候只有一些基础属性会被显示出来而不会触发具体的方法函数。故而我们不能运行后面那个指定边界函数进行样式的刷新，导致出现视觉bug。\n然后另一个需要注意的是，我们在IsOpen中使用了一个TestSwitch的事件，如果我们的开关状态发生了改变，那么就会触发这个事件。后来如果我们需要，可以为这个事件增加功能，一旦IsOpen被赋值，就可以执行相应的功能。\n不过我们如果需要使用这个功能的话，就还需要添加上委托和事件的定义，一般情况下我们在命名空间内写委托，然后在类里定义事件。\n但是，我们知道如果想使用VS的设计器的话可以利用属性栏修改控件的属性，所以我们自定义的控件自然也想要相同的功能。此时需要输入[Category(\u0026quot;属性分类\u0026quot;), Description(\u0026quot;功能描述\u0026quot;)]即可添加这些功能。其中属性分类的目的是为了方便我们找到我们自定义的这些功能，而功能描述则是方便使用的时知道该如何填入所需的属性。如果这里出现了报错，同样是因为没有引用所需的命名空间，只需要像前面一样按住Alt + Enter然后进行修正即可。\n以下是我定义的所有的属性以及他们的描述。\nprivate Color openColor = Color.FromArgb(128, 255, 128); [Category(\u0026#34;DemoUI\u0026#34;), Description(\u0026#34;开启时的颜色\u0026#34;)] public Color OpenColor { get { return openColor; } set { openColor = value; Invalidate(); } } private Color closeColor = Color.FromArgb(255, 128, 128); [Category(\u0026#34;DemoUI\u0026#34;), Description(\u0026#34;关闭时的颜色\u0026#34;)] public Color CloseColor { get { return closeColor; } set { closeColor = value; Invalidate(); } } private int diameter = 20; [Category(\u0026#34;DemoUI\u0026#34;), Description(\u0026#34;圆直径\u0026#34;)] public int Diameter { get { return diameter; } set { diameter = value; Size = new Size(diameter, diameter); Invalidate(); } } private bool isOpen = false; [Category(\u0026#34;DemoUI\u0026#34;), Description(\u0026#34;开关状态\u0026#34;)] public bool IsOpen { get { return isOpen; } set { isOpen = value; Invalidate(); TestSwitch?.Invoke(this, new TestEventArgs(isOpen)); } } 添加方法 完成了上述工作后，我们还需要为这个控件添加一些方法，然后他就可以成为真正的控件了。\n首先，我们需要重写SetBoundsCore方法，这个方法可以定义控件的边界工作，根据微软给的定义，通过这个方法可以控制控件的Left、Top、Width、Height和BoundSpecified值，其中Left和Top控制着控件的位置，而Width和Height控制着控件边界的大小，而最后一个参数是枚举，不过功能没搞懂是做啥的，但是实际应用中不管也没事，因为我们一般都是重写Width和Height就好了。\nprotected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified) { base.SetBoundsCore(x, y, Diameter, Diameter, specified); } 通过这个重写，我们可以让边界大小始终等于圆的直径，其中base的意思是在重写是基于原本的方法进行重写。\n然后我们还需要设置一个可以改变开关值的方法，因为我希望得到的效果是在点击控件时不会改变，而在放开时：如果在控件内放开鼠标可以改变值，而如果在控件外放开鼠标则保持原本只不变。这种情况下我们只需要重写OnMouseUp控件就好了，我重写的内容如下，内容一看就懂，就不多写注释了：\nprotected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); if (Region.IsVisible(e.Location)) { if (IsOpen) { IsOpen = false; } else { IsOpen = true; } } } 然后我们再重写OnPaint方法，这个方法在我们每次调用Invalidate()的时候都会用到（就是前面每次我们调用set访问器的时候都会使用的那个函数），它的功能就是把控件在屏幕上画一遍（只是画一遍，并没有Invalidate中的清除原控件的功能）。\n这里我们使用GDI+来画控件，当然事实上你也可以调用图片然后来实现，他们的原理相似。\n首先我们需要让GDI+的分辨率提高（其实这个控件太简单了可能用不着，但是如果未来在画一些复杂的控件的时候可能需要）：\ne.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; 然后我们现在需要把圆画出来，因为在后面要切割，所以这里其实怎么画都没问题，不过为了严谨我们还是正规点画个圆就行。\nif(isOpen == true) { e.Graphics.FillEllipse(new SolidBrush(OpenColor),0, 0, Diameter, Diameter); } else { e.Graphics.FillEllipse(new SolidBrush(CloseColor), 0, 0, Diameter, Diameter); } 这两句话的意思是使用开启颜色或关闭颜色画个直径为我们设定直径的圆。\n然后最重要的一步来了，虽然我们当前画出来了一个圆，它的样子和我们所需要的样式相同了，但是其本质上还是一个矩形，只是矩形上面画了一个圆而已，而在点击的时候其实点在这个矩形的空白部分我们仍然等同于点到了这个控件。\n这时候，我们需要用Region把这个圆的区域抠出来，具体做法就是新建一个System.Drawing.Drawing2D.GraphicsPath的对象，然后用它在正确位置抠出一个和我们画出的圆一样大小的圆，这样就刚好吧我们需要的圆抠出来了。\nSystem.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath(); path.AddEllipse(0, 0, Diameter, Diameter); Region = new Region(path); 现在，我们就真正有了一个圆形的开关了。\n添加事件 之前我们在属性中使用的事件，可以自己新增事件库进行管理，新建类，然后将其设为public并继承自EventArgs，接着新建事件并设计这个事件的参数和执行的操作。\nusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DemoControls { public class TestEventArgs : EventArgs { public TestEventArgs(object value) { Value = value; } public object Value { get; set; } } } 同时我们还可以把这个事件设为默认事件，在开头部分我们定义这个控件类的默认事件为我们前面注册的TestSwitch。\n使用这个控件 那么该如何使用这个控件呢？你可以新建一个工程（一般情况下建议新建工程来使用控件，而控件库单独用一个工程来管理，你未来所有的控件都可以放在控件库里），如果在本解决方案中的话，只需要在解决方案管理器中右键点击你的控件库工程名，选择生成，然后你就可以当前解决方案的任意工程中使用你控件库里的所有控件了。\n而如果你需要在外部使用你的控件库，可以在生成后，进入到你的工程文件夹，Debug或者Release文件夹里的dll文件。如果你需要在某一个工程中使用这个控件的话，就在新工程里点击工具-选择工具箱项，点击浏览，然后添加刚才那个dll文件即可。\n现在我们在当前解决方案试试看吧：\n生成文件、新建工程、然后开始使用设计页面，可以发现设计页面最开头添加了我们的控件库：\n然后我们拖出来两个，设置一下他们的大小和颜色：\n同时双击第一个控件增添事件功能：\n最后启动：\n成功。\n完整的工程下载地址\n","date":"2021-03-21T22:45:35+08:00","image":"/posts/%E6%A1%8C%E9%9D%A2%E7%AB%AF/winform%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%80%E4%B8%AA%E5%9C%86%E5%BD%A2%E6%8C%89%E9%92%AE/cover.jpg","permalink":"/posts/%E6%A1%8C%E9%9D%A2%E7%AB%AF/winform%E8%87%AA%E5%AE%9A%E4%B9%89%E4%B8%80%E4%B8%AA%E5%9C%86%E5%BD%A2%E6%8C%89%E9%92%AE/","title":"Winform自定义一个圆形按钮"},{"content":"接上篇关于Arduino基础环境配置、界面介绍和C语言基础，这一篇的内容为具体如何在Arduino中进行编程。\n在VSCode上配置Arduino 什么是VSCode VSCode，即Visual Studio Code，是微软制作的一个开源免费编辑器，当今始接最热门的主流代码编辑器之一。百度vscode或者点我给出的这个链接，可以进到官网下载。编辑器与IDE（集成开发环境）不同，VSCode更加像一个可以加载插件的记事本，不过如果配置得当，VSCode也可以用来当作一个简陋的IDE使用。\n为什么用VSCode Arduino IDE本身其实只是一个非常简陋的IDE，没有代码补全、丰富的高亮、代码跳转重构等功能，而这些我们都可以在VSCode上进行实现，这可以是不必要的（如果你能力足够强用记事本直接写代码也是可行的），但是作为初学者或者开发者使用这些功能能够让你快速学习或者实现你想要实现的功能。\n怎么配置 先下载并安装VSCode。\n接着下载插件，在最左边选择扩展按钮，然后搜索并安装我标出的这几个插件，你就能有一个可以装Arduino的中文VSCode了（我的VSCode与你们的应该样式不同，因为我安装了样式的插件）：\n然后打开安装好的VSCode，左上角选择：文件-打开文件夹，然后选择一个你以后专门保存Arduino代码的文件夹打开。\n在左上角文件选项中选择首选项-设置。\n然后设置界面的右上角，选择打开设置（json）\n然后我们就进入到了一个JSON文件界面，在原本的文件开头如下内容，不过需要注意：下面第一行的path为你安装Arduino的位置，我安装在D:/Software/Arduino文件夹里的，如果设置自己的地址也请按照我这种双斜杠的格式来写。\n\u0026#34;arduino.path\u0026#34;: \u0026#34;D:\\\\Software\\\\Arduino\u0026#34;, \u0026#34;C_Cpp.intelliSenseEngine\u0026#34;: \u0026#34;Tag Parser\u0026#34;, \u0026#34;editor.insertSpaces\u0026#34;: true, \u0026#34;files.autoGuessEncoding\u0026#34;: true, \u0026#34;arduino.logLevel\u0026#34;: \u0026#34;info\u0026#34;, \u0026#34;explorer.confirmDelete\u0026#34;: false, \u0026#34;editor.detectIndentation\u0026#34;: false, 点击Ctrl+s保存JSON文件，然后重启VSCode，现在你的VSCode环境就配置好了。\nArduino的程序内容 打开例程 现在我们先来打开一个官方例程，首先打开Arduino IDE，选择文件-示例-Basic-Blink，这是Arduino官方提供的一个灯光闪烁程序。\n点开后按Ctrl+S，IDE会告诉你该文件处于只读状态，需要将项目保存在其它位置，点击确定，然后将文件保存在我们前面设置的VSCode工作文件夹里，然后我们就可以退出并通过VSCode打开它了。\n例程将会是我们很好的学习资源，这些例程有些来自于开发板，有些来自于函数库，有些来自于官方，但是我们如果已经安装好的东西，找例程并且想要在VSCode中进行修改测试或者仅仅是查看，都可以通过这种方式来。\n程序结构 我们现在可以发现其实这个Blink中有两个函数setup和loop，它们的功能我在下图中标示了：\nsetup和loop是Arduino的逻辑核心，普通的Arduino程序都是基于这两个函数进行运行的，一般我们在setup中初始化环境，然后在loop区域写我们需要实现的功能。\n如果想知道这两个函数是怎么实现的，可以查看{你的Arduino安装地址}\\hardware\\arduino\\avr\\cores\\arduino中main.cpp和Arduino.h文件，大概意思就是我们在程序中实现的setup和loop函数将会通过Arduino.h被引用，然后在main.cpp中被执行，Arduino.h只是一个引用传递文件，这里我们重点关注main.cpp：\n功能函数 现在我们再来看看Blink文件：\n// the setup function runs once when you press reset or power the board void setup() { // initialize digital pin LED_BUILTIN as an output. pinMode(LED_BUILTIN, OUTPUT); } // the loop function runs over and over again forever void loop() { digitalWrite(LED_BUILTIN, HIGH); // turn the LED on (HIGH is the voltage level) delay(1000); // wait for a second digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW delay(1000); // wait for a second } 前面那一堆介绍注释我就不复制了，有兴趣自己看，我们现在关注程序中出现的功能函数：pinMode()、digitalWrite()、delay()。\n根据注释其实我们可以知道它们的功能分别为初始化引脚功能，引脚电平改变，时间延迟，如果在VSCode中，我们可以按住Ctrl键然后鼠标单击该函数，即可进入该函数的定义界面。不过我们现在暂时不需要点击去知道这些功能，等以后你想自己实现一个Arduino的时候可以再尝试去看。\n然后我们再看参数部分，OUTPUT、HIGH、LOW这些似乎比较好理解，但是LED_BUILTIN是什么东西？不用担心，我们同样Ctrl点击它，即可查看它的定义：\n它被宏定义为了1，现在我先告诉你这个1其实就是Arduino板子上标注了1的那个引脚，但是这个函数具体是怎么个原理呢？我们怎么知道一个函数需要怎么使用呢？\n查看手册 对于上述两个问题，最好的方案当然是查看手册了。打开Arduino IDE，选择帮助-参考，然后我们就打开了我们的功能函数手册，一个保存在你电脑里的网页，参考手册看上去很多而且是全英语的，但是不用担心，我来告诉你怎么查看它。\n首先可以看到手册分成了三列，第一列的大标题是Structure（结构体），第二列是Variables（变量），第三列是Functions（函数）。\nStructure 其中我们看第一列的Structure内容很多，但是除了setup和loop函数，其实就是C语言基础的东西。\nVariables Constants 而第二列变量里，第一个大类Constants（常量）（是的，他把常量划分到变量列里了，不过不影响，只是个分类方法而已），这些是一些宏定义的量，如果你需要知道他有什么用，点开查看即可，不过一般基础使用常用的内容有以下几个：\n不知道电平是什么的我们在后面讲。\nData Type 这个就是数据类型，C++与C之间的差别在于C++多了几个数据类型，其它没什么变化（不过不同芯片中同一类型占用的存储空间大小可能各自不同（也就是他的最大最小值可能会有差别），不过一般基础使用只要不遇到特别大或者特别小的一般不会遇到这些问题）我在这里标注了它们不同的地方：\nConversion 这里面是一堆强制转换函数\nVariable Scope \u0026amp; Qualifiers 变量作用域和限定符，主要用于限定作用域，其中static和const用法在基础阶段需要多练习掌握，而volatile则在真正开始做大项目之后开始慢慢掌握（提前掌握也挺好，只是怕你用不好）。\nUtilities 公共功能，其中sizeof主要在于你不知道数据类型的大小的时候使用（就是我前面说的最大最小值可能存在差别的地方）。\n而PROGMEM是一个内存控制标识，基础使用应该用不到，当你在存储处理大量数据时可以使用。\n电路基础 在讲Function之前，我想先讲述以下电路基础，便于理解这些函数的功能，这里先假设你知道基础的电压（电平）、电流、电阻是什么，如果不知道还请去百度。\n模拟电路 平常我们见到的像是音频波纹这些，都属于模拟信号，它们在时间上是连续的，一般来说我们正常制作和能感知到的电路都是模拟电路，我们可以把模拟电路某点的电压在时间上的改变用图像来标示，下面是一个模拟电路的波形示例：\n数字电路 数字电路其实是我们人为抽象出来的电路，也被称为逻辑电路。有一些半导体，比如LED二极管就有一个特性，当两引脚两端电压低于某个界限时它是相当于断路的，而如果高于这个电压，它又是相当于短路的，而断路和短路两种状态就可以等效为0和1两个数字，0表示其中电流不存在，断路，即低电平，而1表示电流存在，短路，即高电平。当然实际上的定义是一个数学上的概念，不过我们这里姑且这么理解就行了。而半导体因为这个特性就拥有了逻辑功能，加上一些特殊的半导体还有一些其它特性，人们把它们制作成了芯片。\n所以我们平常使用的芯片之类的都是半导体元件，而在外界和内部的处理信息方式都是对数字电路的处理。\n但是具体多高的电平是高电平多低是低电平呢。一般来说对于一个普通的芯片，它会有两种电源引脚，一个叫GND，一个叫VCC，GND作为参考基准电平，我们统一将其标定为0V，那么VCC和GND的电压差就是工作电压，我们可以直接用VCC的压值来直接表示。一般常见的工作电压有3.3V和5V，而UNO即为5V，在与UNO这类5V单片机定义电压分界的时候，往往定义的3V往上为1，往下为0，但是为了稳妥起见，芯片自身输出的1就是其工作电压，而0就是其GND的电压。\n模数转换和数模转换 模拟电路和数字电路其实是可以相互转换的，一般我们如果需要从外界读取一个模拟量（就是我们不只是需要简单知道其电平为1还是0，而是需要知道具体电压），就可以使用模数转换然后得到结果。如果我们需要输出一个模拟量，就可以通过数模转换将数字信号生成模拟信号。\nPWM 这个属于数字信号的内容，因为我们知道输出电压在电路中其实等效于输出能量，而在周期时间内通过控制高低电平输出时间占比，就可以控制单位时间内能量的输出总量，实现脉冲调制的功能。当然这个功能不仅仅能用于控制能量，但是一般来说我们在基础阶段知道这一个功能即可。\n接上面的功能函数 Functions 这些功能函数其实就是Arduino官方设定的一些基础功能，而我们未来可能在使用途中会遇到更多的功能函数，但是从理论上那些功能大多数都是可以通过这些基础函数进行实现的，我接下来用图片罗列出它们的功能，不需要详细去记用法，如果需要用到的就直接再点开详细看就行，不然里面很多不常用的你记了后面也会忘，知道有些啥就行。\n普通引脚功能函数 时间函数 数学函数 字符判断函数 随机数 位操作 中断 通信 不过上面其实只是一小部分，我们回到这个网页顶部，然后这里有个Libraries，点开它，里面罗列了一堆库的用法介绍。\n我们现在究竟需要掌握些什么 相信你看到这么多功能感觉特别复杂了，不过不用担心，很多功能其实不太常用，我现在说一下一些比较常用的功能和函数，这些你去学习就好了。\n上面勾出来的三个库，你需要大概去掌握它们，其中SPI和I2C与前面Serial那个串口（UART）是非常常见而且重要的三种通信协议，你需要知道它们大概的通信流程。这些功能可以在Arduino IDE种官方提供的例程和这个参考手册综合学习。 EEPROM那个功能其实不是那么的重要，但是掌握它有助于你去理解芯片底层的存储机理。 前面digital I/O和analog I/O的六个函数需要掌握，这是最基础的内容。 掌握delay函数。 掌握中断原理并能自己写一两个中断。 如果你可以使用我上面写的这五条里的功能，配合我前面讲的这些学习方法，Arduino的基础内容就基本难不倒你了。\n后记 说来惭愧，我本以为会语言精炼地讲通这些，但是还是存在很多没讲好的地方，还请多多包含。本教程为一个入门教程，重点在于告诉你该如何快速去掌握Arduino这个平台并在后期深入学习，但是真正要学会还是得自己去深入了解掌握，毕竟你永远不可能通过一本书甚至这么一篇文章就成为高手。\n其实之所以我在里面并没有怎么讲具体你要去实现什么怎么做，而是从底层原理和学习方法来讲，是因为每个人想要做的事是不同的，我如果将每个功能挨个讲完，那复杂程度就配不上Arduino这种专门为简化编程而生的平台了。\n","date":"2021-02-02T15:17:03+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/arduino%E6%9E%81%E9%80%9F%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E4%B8%A4%E7%AF%87%E6%96%87%E7%AB%A0%E8%AE%A9%E4%BD%A0%E4%BC%9A%E7%94%A8arduino%E4%B8%8B/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/arduino%E6%9E%81%E9%80%9F%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E4%B8%A4%E7%AF%87%E6%96%87%E7%AB%A0%E8%AE%A9%E4%BD%A0%E4%BC%9A%E7%94%A8arduino%E4%B8%8B/","title":"Arduino极速入门教程——两篇文章让你会用Arduino（下）"},{"content":"开坑使我快乐，本教程仅供初学者入门学习使用，要想深入还是得靠自己。本教程假设读者为完全无编程基础的普通人，所以行文可能会稍显啰嗦，但相信仍然会是目前比较精炼简短的教程。\nArduino是啥 一个开源的硬件开发平台，其特点在于简单易学易上手，而且国内外资源多，有啥问题也好解决。\nArduino要些什么知识 基础 有这些基础你基本上可以用Arduino做出网上常见的60%的项目。\nC语言基础和一点面向对象语言编程概念 Arduino编程时使用C++，但是C++是兼容C语言的，一些简单基础的部分有C语言基础就能实现。 在基础使用时我们仍然会使用到面向对象编程的东西，先掌握概念和一点基础的东西，初学阶段够用就行。 初中电学知识和一点数电模电概念 知道短路、断路概念还有电压电流电阻之间的关系计算，这是硬件制作最基础的东西。 有一些涉及电容、电感、半导体的内容，这些一般属于模电知识，初学的时候不需要了解，因为我们可以用别人做好的电路，稍微连连线就行。 数字电路会是一个很常用的概念，不过初学阶段你只需要知道二进制计算就行。 时间、精力和钱 虽然Arduino的资料很多，但是偶尔你也可能遇到百度上找不到的问题（也有可能是你因为知识不够无法形容出来的问题），要把这些问题解决你需要花很多时间来搜索、学习、试错。 学习是一个很耗费精力的事，事实上你最好喜欢一样知识或者有一个明确需要学习它的理由，才能更好坚持下来。 Arduino属于硬件开发平台，和平常我们电脑手机上开发一个程序软件游戏不同，硬件购买时将会花费大量的金钱，我会在后面不时提一下怎么尽量省钱去买这些硬件，但是实际学习中你仍然要做好花钱的准备。 不过不要被我上面说的吓到了，毕竟硬件如果能做好也是可以带来收入的，很多东西都是会者不难，难者不会，现代社会赚钱不就是赚信息差嘛。我在学Arduino没多久的时候，靠着百度搜搜改改，就用它接到了我人生中的第一笔单子，用了一下午赚了三百块。 进阶 这些是你觉得自己已经行了之后可以继续深入学习的方向，本教程会不时提及但是不做详述。\nC++语言编程 C++是Arduino的核心语言，虽然运行效率略低于直接用C，但是其带来的方便性可以让我们无视这几毫秒。 网上说C++有点难学，但是不要担心，你只是需要用它来做Arduino而已，不会怎么用到C++那些乱七八糟的复杂特性。 嵌入式编程 当你可以自己从底层上将一个全新的芯片做成Arduino的时候，你就可以算嵌入式编程入门了。 有句话叫嵌入式门槛在门里，前期简单入门做点简单事不用担心，但是越到后面学习难度越陡峭，请提前做好准备。 操作系统 这里涉及到嵌入式编程的高级操作了，主要目的是为了实现多线程（让芯片同一时间处理多件事），不需要去详细学，接触到嵌入式后自然会学到 如果你从软件那边往嵌入式这边学的话也会学到这个东西，操作系统往上就是我们平常在系统环境上的编程，往下就是针对硬件的编程 网络编程 这个主要是未来物联网开发会更加多，甚至现在所谓的嵌入式编程和物联网编程都有点代称的感觉了，所以你需要掌握一定的网络编程的知识 如果你有操作系统上的网络编程开发经验，那么你在理解这一块上会比较容易，而如果你学会了这块，后面想学互联网行业的技术也会比较容易 复杂的电路知识 初中那点电路知识在这肯定是不够的，高频射频电路、高速电路的设计，这些也是可以学习的一个点（但是不是必学的，因为这一块实际上会有专门的开发人员），当然实际上这里也包括了半导体设计、FPGA啥的（这又是另外一个行业了，但是了解一些知识有助于更好地理解一些底层原理） 玩Arduino的大多都是对极客、发明家之类的感兴趣的朋友，所以会电路设计肯定会让你节省下更多的时间精力和钱 非常非常多的时间、精力和钱 你要深入学习那么路线就肯定是很陡峭的，事实上我在上面罗列的这些东西也只是所有知识的冰山一角，不过你既然都看到这了想必也下定决心了，所以，加油吧。 学习须知 请务必保持不求甚解的态度，所有的东西你不需要挨个记住，只需有个大概印象即可，因为有些知识并不是你后来会常用的，花时间去记浪费时间又容易造成挫败感，而经常用的东西你在多用了之后自然就记住了。\n有个大概印象后需要多自己做项目，做点小玩意，可以搜索借鉴代码，但是要多思考，这样才能帮助你融会贯通所学的知识，也才能让你从一大堆知识中总结凝练出能够帮助到自己的东西。\n环境搭建 在看完这一章之前先别买硬件，免得吃亏买贵了。\n百度Arduino，你一般会看到最前面的两个链接，其中一个是英文官网，另二个是中文社区。这两个网站里面都有安装包可以下载，我们这里举例为选择官网下载，点击我上面给的链接即可进入英文官网，然后界面中依次选择：Software-Download，在出现的下载界面里点击你需要的程序包。\n如果选择ZIP file你将获得一个压缩包，这个属于免安装的方式，直接解压就可以使用，更方便，但是稳定性弱点（一般也没啥）。其中Windows平台不建议点那个Windows app选项，那个是从Windows应用商店下载安装的，这种安装方式有时候要搞点花的不方便。\n下载好后你将获得一个安装程序或者压缩文件，将Arduino安装或解压到你一个找得到的位置即可，它可能会让你安装一个驱动，同样安装即可。\n然后你的Arduino环境就搭建好了，非常简单。\n界面介绍 现在打开你的Arduino（实际上它叫Arduino IDE，即Arduino的集成开发环境）：\n让我们来看看界面长啥样：\n可以看到，下载安装好后它就是一个不大简单的中文界面，现在我将其放大然后标注一下主界面的内容：\n配置 为了方便起见，在此对我们的Arduino IDE进行配置一下，点击文件-首选项：\n然后我们可以看到新界面，也没啥大变化，多了个行号和可折叠符号，不喜欢也可以去掉，不过建议留着，比较方便：\n设置完了，我们再看看工具栏中还有一些其它什么有用的功能没：\n文件 首先点开文件，这里面最重要的除了刚才讲过的首选项界面，另一个就是示例了，里面给出了很多例子供以学习：\n编辑 然后是编辑界面，这里有一个复制到论坛和复制为HTML可以把代码以特定格式进行全部复制，便于分享；\n另外下面还有三行关于注释、缩进的功能，如果再框选一大段文字后通过快捷键进行操作的话，可以实现块注释和块缩进：\nvoid setup(){ // put your setup code here, to run once: } void loop(){ // put your main code here, to run repeatedly: } 项目 然后是项目界面，其中编译、上传功能在上面已经说过了，通过编程器上传即既编译又上传，而导出已编译的二进制文件则是将编译好生成的将会上传到芯片中的文件先导出保存，然后通过其它软件进行烧录上传；\n而显示项目文件夹顾名思义就是找到项目当前地址，而加载库则为添加所需库文件，至于添加文件顾名思义就是把一些文件内容添加到工程里：\n工具 工具栏是一个很重要的部分，我们经常要在这里设置东西，其中自动格式化就是调整代码的缩进让他符合C++语言推荐规范。项目存档就是把项目通过zip封装起来进行保存。修正编码并重新加载我也不知道是个啥，目前没用过。管理库就是对库文件进行管理。串口监视器和绘图器都是串口工具，但是一个直接显示文字，一个可以把数字以图像形式绘制。下面的东西是开发板设置，后面我会详细讲。编程器和烧录引导程序一般用不到，后面会提。\n帮助 帮助界面提供的都是官方的英文网址链接，可以在网站上进行查询相应的需求功能，不过一般重要的只有参考功能，其它可以不管。\n硬件 所谓硬件，一般就是指的单片机开发板，单片机是一块小芯片，可以理解为一个简化版的电脑CPU，通过配合各种传感器实现各种功能，我们平常经常使用的运动手环、汽车导航等都是单片机制作的，事实上我们手机的芯片从定义上也是高级单片机的一种。而单片机开发板就是把单片机焊接在一个可以用于开发工作的电路板上制成的。\n官方Arduino（是的，虽然是开源硬件，但是它仍然有个官方，类似于基金会的形式存在）使用的单片机为AVR系列（这些所谓系列一般就是指的芯片提供厂家），其中最著名的Arduino开发板名字叫Arduino UNO（音同乌诺），官方的长这样，买下来应该要一百多：\n他的芯片是AVR系列的Atmega328p单片机，当然因为这是开源的产品，所以也有很多非官方的板子（这不是盗版，因为硬件是开源的，事实上通过电路图你自己也可以来做一个板子），网上我见过最便宜的只要十块左右（当时打折，总之一般不超过二十，当然有一些添加了新的外设那种会很贵但是买了也不亏，不过目前如果你不懂就别买），总之别买贵了。初学者买UNO是最方便的，看到这里你可以去买UNO板了，但是其它的东西也还是先别买（最好找那种送USB线的，因为这种方口USB比较少见）。然后我们可以看到UNO的板载资源有这些：\n其中USB接口可以用来供电或者上传程序，DC插头只能用于供电，左右两边的两排孔是用于接线的。而上面那很大一坨的芯片，就是ATMEGA328p，是核心芯片，而复位按键功能类似于重启按钮。其它的暂时不需要知道，因为平常也不用。\n除了UNO以外，Arduino官方还用AVR系列的单片机制作了micro、mini、mega、leonardo等一系列官方Arduino。而民间也有使用AVR制作的Arduino，比如之前出名了一阵的bad USB（一个USB入侵设备），另外还有更多通过其它芯片制作的Arduino，例如STM32的Maple系列、STM32duino系列、ESP32、ESP8266的Nodemcu等等，当然如果你后面技术精湛了，也可以尝试把自己喜欢的芯片做成Arduino然后自己给它命名并发布出去。\n不过现在我们还是继续讲UNO吧，因为Arduino将底层的东西封装起来，所以很多时候这些代码是可以互用的。\n然后你可以在淘宝上搜索以下东西：\n面包板（大小无所谓） 杜邦线（公对公、母对母、公对母三种最好都买） 电阻（直插式的，精度要求不高，一包几百个只要几块钱） LED（发光二极管，直插式的，随便什么颜色） 上述这些东西买下来差不多三十块左右，不用买多了。\n然后下单，等待你的快递，我们在这段时间学学C语言基础。\nC语言基础 因为一般来说会学这个的大多还是知道编程是个啥的，但是我们还是要考虑到所有人群，故而这里简单介绍一下C语言基础学习方案。\n因为我们只需要一个基础，而且你等待快递的时间一般也就三天左右，所以我这里只提供一个非常简要的学习方案：\n打开菜鸟教程的C语言教程。\n看完如图所示的部分\n尝试写出这样一个程序：\n先写一个函数，这个函数的功能为根据参数设定在屏幕上打印与参数相等行的“Hello World!”。在封装好函数后，实现如下功能：用户输入行数，如果行数为奇数则执行上述函数的功能，如果为偶数则不执行，并且通过switch语句实现判断，如果行数为1-5，则分别在最后输出一行He、ll、oW、rl、d!。\n如果可以实现上述程序，那么你基本上就掌握了C语言的基础了，然后我们后面再进行后面的教程。\n后言 本教程只有两章，下一章内容即为具体编程部分：链接。\n","date":"2021-02-01T20:06:25+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/arduino%E6%9E%81%E9%80%9F%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E4%B8%A4%E7%AF%87%E6%96%87%E7%AB%A0%E8%AE%A9%E4%BD%A0%E4%BC%9A%E7%94%A8arduino%E4%B8%8A/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/arduino%E6%9E%81%E9%80%9F%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E4%B8%A4%E7%AF%87%E6%96%87%E7%AB%A0%E8%AE%A9%E4%BD%A0%E4%BC%9A%E7%94%A8arduino%E4%B8%8A/","title":"Arduino极速入门教程——两篇文章让你会用Arduino（上）"},{"content":"因为数据结构课在使用malloc函数的时候一直很迷惑，为什么一定需要在前面加上一个强制转换语句，像是这样：int *a = (int *)malloc(sizeof(int)*3);。\n为此我在菜鸟教程的malloc()函数介绍中找到了关于malloc的声明：void *malloc(size_t size)，显然加上一个强制转换语句并不是标准语法必须的东西，但是在菜鸟教程下面的举例中是按照强制转换的写法来写的，可惜没有说为什么。为此我尝试了不加强制转换语句的malloc来直接分配空间，在gcc编译后并没有报错或者发出警告。\n这就很神奇了，我换了多个姿势来对这两种用法进行测试，包括但不限于不同大小long *a = malloc(sizeof(int)*2)或者数组结构int *b = malloc(size(int)*3)，他们都没有报错或警告，而强制转换亦是如此。\n就在我怀疑是某种错误的如同void main这种异类写法产生了曼德拉效应时，我看到了另一篇博客的记录：在ANSI/ISO标准C下，我们是可以不使用强制转换来直接使用malloc的，并且使用强制转换还可能掩盖malloc()声明错误时产生的重要警告，反而不如直接使用malloc。但是使用malloc强制转换的好处在于，可以更方便地移植到C++中，因为C++似乎并不支持这种隐式转换。\n根据这个帖子我大概猜测了一下国内这种喜欢在malloc函数前加强制转换命令的原因，除了个别学校在教学生C语言的时候把C/C++混为一谈，导致学生用C++的语法来理解C语言（这种情况真的不少），还有很大原因是因为国内高校很喜欢用Dev-C++这类常年未更新用着远古标准的IDE。\n","date":"2020-12-26T18:14:00+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/malloc%E7%9A%84%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/malloc%E7%9A%84%E5%BC%BA%E5%88%B6%E8%BD%AC%E6%8D%A2/","title":"C/C++中malloc的强制转换"},{"content":"作为一名砖业的CV攻城狮候补，在使用vim时因为vim的剪贴板是一块单独的内存，不能很方便地CV我们在网上找到的优秀代码，这实在让我感到非常头疼，故而在网上搜了一下如何让vim能够使用系统剪贴板，在这里记录一下。\n首先在终端里执行vim --version | grep clipboard指令，这句话的意思大概是在vim的配置中搜索clipboard（剪贴板），然后我们可以看到一个配置名全称就是clipboard，它前面的符号有两种情况，如果是+号则说明可以在vim中使用系统剪贴板，只是需要一点特殊的指令。而如果是-号则说明暂时不能在vim中使用系统剪贴板，在这种情况下我们需要下载插件。根据我在网上看到的各种帖子，有两种方法，一是sudo apt-get install vim-gnome另一个是sudo apt-get install vim-gtk，我先是使用了第一种安装vim-gnome，但是不知道为什么无法安装，显示的已被引用。\n我搜了很多帖子并且尝试了很多方法，但是这个vim-gnome始终无法安装上去，所以这个方法就被我pass了，不过我没有直接使用vim-gtk这种方法，而是直接用.vimrc文件来配置了一下，先是通过vim处理.vimrc配置文件：vim ~/.vimrc，然后在.vimrc文件中输入一行set clipboard=unnamedplus这句话的意思是让vim的剪贴板与外部剪贴板连接。然后我就直接可以在vim中使用y、p、d、x指令与系统剪贴板连通了。感觉这句话的意思有可能是让系统剪贴板和vim剪贴板的地址直接相等，这样在这段内存中Linux系统与vim就可以直接公用了。\n然后因为各种原因，我重装了一个新的Linux，这一次我使用了上面说的第二种方法，安装了vim-gtk，然后在安装了这个插件后，使用vim --version | grep clipboard就可以看见clipboard的配置前面的符号变成了+号，这个时候如果进入vim，就可以使用“+y这类语句调用系统剪贴板了。详细可见知乎回答。\n","date":"2020-12-25T21:10:10+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/%E5%A6%82%E4%BD%95%E5%9C%A8vim%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%B3%BB%E7%BB%9F%E5%89%AA%E8%B4%B4%E6%9D%BF/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/%E5%A6%82%E4%BD%95%E5%9C%A8vim%E4%B8%AD%E4%BD%BF%E7%94%A8%E7%B3%BB%E7%BB%9F%E5%89%AA%E8%B4%B4%E6%9D%BF/","title":"如何在Vim中使用系统剪贴板"},{"content":"本文的目的是作为一个记录，不引战，同时为有同样瞎搞爱好者提供一个学习思路……尽量全部推荐和使用免费开源工具，减少版权纠纷，降低学习成本，大概算有用吧\n我的技术栈： 工具： 硬件： 电路设计 CAD 硬件编程 AVR STM32 Holtek Arduino TI系列 FPGA 电路仿真 软件开发 C/C++ C# Java 前端三件套 Golang Python 办公 不务正业 我的技术栈： 实际上有很多并不是很熟，请酌情参考\n硬件开发（EDA设计、FPGA、STM32、AVR、Arduino、Holtek、CAD、TI）\n软件编程（C/C++、Java、C#、Python、前端三件套（事实上JS还没上日程）、Golang）\n不务正业（修图、矢量图设计、视频）\n工具： 硬件： 毕竟我还是一个电信学子，本行还是要抓好\n电路设计 KiCad：一个超级好用的开源EDA软件，功能主要就是普通的PCB设计，支持多层板设计以及一些高级特性，如果从全能性上比AD肯定有差距，但是作为日常普通使用其实已经够了。最重要的是它支持插件功能，这好像都是开源软件的标配了，用丰富的插件可以实现很多高级功能，甚至如果你编程开发能力不错，可以试试自己写插件。\n立创EDA：同样是个免费的EDA软件，部分功能性上存在一定缺失而且没有离线版本是硬伤，但是学习成本是目前我觉得最低的一个，而且里面有特别多的现成封装和原理图封装，如果不是保密产品或者高精尖产品，用这个就足够了。\nAltium Designer：国内很火的收费EDA软件，比较适合用于工作，还是很有学习的价值，但是为了支持正版化我还是没咋用。不考虑插件的基础上功能性上优于KiCad。\nCadence：实际上是Cadence公司推出的一系列EDA工具，从专业性上优于以上所有，但是太难学而且一套下来国内很多公司不一定会去买。\n综上，我按照一下几个角度进行比较：\n学习便利：立创EDA\u0026gt;KiCad\u0026gt;Altium Designer\u0026gt;Cadence\n就业便利：Altium Designer\u0026gt;Cadence\u0026gt;KiCad\u0026gt;立创EDA\n薪资：Cadence\u0026gt;Altium Designer\u0026gt;KiCad\u0026gt;立创EDA\n个人推荐：如果是比较专业一点的编程肯定选KiCad，毕竟是免费开源软件，所有代码都是可以自己看得到的，而且扩展性也比较高。立创虽然方便但是有时候太图方便容易丢失计数，不过如果是偶尔做点玩具电路，那立创还是首选。\nCAD FreeCAD：同样是一个开源软件，主要面向3D建模，但是同样也有面向2D的功能，总体上性能很强，支持自制插件和添加插件，而且和上面的KiCad有合作内容，有个专属插件就是给KiCad做3D模型的。缺点还是很明显，国内的社区没做起来，资料严重缺失，而且曲面支持比较差。 OpenCAD：也是一个开源软件，学习曲线我愿称之最高，属于编程建模，通过输入描述性语句来对建模内容进行修改用了一下感觉还挺带感的，其实有图形编程基础的适应一下就可以了，但是难也是真的难。其在FreeCAD中也有插件，比FreeCAD感觉更小众，但是感兴趣的可以试试。 OnShape：我目前正在用的一个在线建模软件，针对学生免费（有没有其他的免费版我不太确定，但是当时注册的时候看到好像是有的），老板好像是原来PTC的老板，履历真的强，在PTC搞出了Creo，然后跳出去做了Solidworks，现在又弄了个新的OnShape。优点是作为一个云平台，OnShape可以在很辣鸡的机子上运行，甚至土豪一点可以花1500美元买个高级版，支持手机上建模，而且提供免费版，免费版的缺点好像就只是仓库只支持5个私人模型，其他建模必须开源放在社区，总之就是特别香。缺点也是在于其是一个云平台，没网就不能用，而且作为国外服务器比较慢……而且你懂的，如果商业的东西放在上面可能哪天就没了。（这个平台前半年不知道为啥不能注册，但是后半年也可能是最近突然好了，怀疑是川宝导致的） Creo：我之前在一个比赛里学的，用的教育版，阉割了一些功能，不过整体还是很好用，如果不是商业用途的东西我个人其实很推荐，而且它的曲面真的强，扩展性还有工作性能也是目前我用过最好的。不过国内好像不咋流行，似乎因为它的建模逻辑有点反常识（虽然我不咋觉得，但是我认识的一些吐槽Creo的帖子都是这么说的），应该说喜欢的特别喜欢，讨厌的也特别讨厌比较两极化的产品，而且正版一套下来价格比较高。 Solidworks：感觉是目前国内最火的建模软件，价格低而且不吃太多配置（如果是小型建模），国内资料也多，但是可能是我习惯了Creo的建模逻辑，用着SW的建模逻辑就觉得很不舒服，而且因为其为了降低建模难度，进行了很多偏常识的修改，仅限于我来说有时候会感觉这也不行那也不行的烦恼，如果是就业的话学学挺好，但是个人体验上我是真的不喜欢。 Fusion360：AutoDesk的旗下产品，自动桌作为老牌公司做的东西那确实不错，提供教育免费版，正版国内暂时只能找代理购买，用了一下感觉还是可以的，属于半离线半在线的产品，功能插件这些也很多，个人觉得和OnShape不相上下，但是它的免费时常比OnShape短，所以我还是倾向于OnShape。如果是国内的小公司感觉用这个产品应该挺不错的，他好像还有个升级版，不过没怎么了解了。另外AutoDesk家另一个AutoCAD用着来画2D图比其他几个都要舒服点。 另外像是UG、犀牛之类的软件，因为没咋用过就不发表意见了。这几个产品说实话都各有千秋，我从一下几个角度进行比较一下：\n（这块我不是啥专业人士，虽然尽量客观但是仅供参考）\n就业：SolidWorks\u0026gt;Creo\u0026gt;Fusion360\u0026gt;FreeCAD\u0026gt;OnShape\u0026gt;OpenCAD\n专业度：Creo\u0026gt;Fusion360≈Solidworks≈OnShape\u0026gt;FreeCAD≈OpenCAD\n学习便利：Fusion360≈OnShape≈Solidworks\u0026gt;Creo\u0026gt;FreeCAD\u0026gt;OpenCAD\n个人推荐：如果是普通使用的话用OnShape，如果网不好而且不介意麻烦点就用OpenCAD或FreeCAD，如果是学生可以试试Creo的教育版，就业的话其实建议SW和Creo都学，两个建模思路有一定互补性，Fusion360适合有教育版而且网不太好上OnShape的。\n硬件编程 AVR AVR Studio：虽然大家都说ICC AVR什么比较好，但是说实话我觉得AVR Studio已经不差了，而且最重要的是免费开源。\nSTM32 CubeIDE：开源大法好，而且遇到问题时这种基于eclipse的IDE可以很容易在网上找到解决方案\nHoltek gcc-arm：相比于直接使用IDE这种方法很麻烦而且不实用，我选gcc的最主要目的其实只是因为我们的一个项目需要而已，如果不是必须的话还是建议用合泰自家的HT-IDE编程\nArduino vscode+PlatformIO：vscode里的Arduino插件对于很多使用过Arduino的用户应该都比较熟悉了，至于PlatformIO则是一个更加集成化的平台，很方便，但是如果不认真去学习了解也会很麻烦。详情可以直接浏览器搜PlatformIO或者在vscode里面找PlatformIO的插件。\nArduinoIDE pro：社区正在搞得高级版ArduinoIDE，基于eclipse的，但是目前好像还有很多bug，如果愿意折腾可以去尝试一下，如果有能力而且感兴趣也可以试试去参与开发。\nTI系列 CCS：TI官方IDE，免费的，基于eclipse，功能支持做的还是挺好，支持energia的调试\nEnergia：TI官方Arduino版本，对于普通MSP430可能一般般，就和普通Arduino差不多，但是对于MSP432有特别方便的多线程支持，可以参考我之前的博文。\nFPGA vivado：Xilinx家的软件，因为当时学的时候就是学的Xilinx的FPGA，所以就用这个了，免费的，直接到Xilinx官网下载即可。\n我知道肯定很多人用Keil或者IAR来开发单片机的，但是个人建议还是尽量支持正版或者开源吧，防止哪天被搞了。\n电路仿真 其实之前的几个EDA都有电路仿真功能，这里单独再介绍几个（其实都没用过，一般我用KiCad的仿真差不多就够了，下面很多都是在社区看到的）：\nMultisim：老牌软件了，很多学校都是学的这个，不过缺点就是不免费，但是很常见，就业什么的可以学学。\n开源工具：NGspice、Open Circuit Design 、gplEDA Homepage 、gEDA Project\u0026rsquo;s Homepage\n以上开源工具皆来自于开源社区的讨论总结。\n软件开发 C/C++ Code::Blocks：在Windows平台上我比较喜欢的IDE，开源免费，支持很多功能，里面我看到的就有Arduino、Arm、AVR等单片机的开发，不过因为爱好原因没有接触太多其它功能，总之特别强大就对了。\nvim：老牌编辑器了，从其功能上来看，目前觉得除了vscode可以在便利程度对其造成威胁，这基本上是个无敌的编辑器。不过要用vim就需要搭配gcc、makefile这些功能一起用，学习曲线挺陡峭的，但是说实话非常利于学习底层的东西。\nvscode：大热的宇宙第一编辑器，但是我对vscode的学习还不是很好，关于其开发方式等都不是很懂，还有待学习。\nVisual Studio：宇宙第一IDE，不过一般这都是用来开发GUI之类的，只是会用到C/C++，但是平时普通小工具的开发没必要用。\nC# Visual Studio：毕竟是亲儿子，微软在VS中对C#的支持真的特别好，而且中文支持也很爽，有社区版免费，总之爽就行了。\nJava eclipse：开源免费，当前开源界的当红IDE，搭配各种插件可以有特别多的便利用途。\nIDEA：有教育版，说实话用着比eclipse舒服，但是毕竟有时限，而且考虑到很多公司为了节省成本更多会使用eclipse，尽量少用吧。\nMyEclipse:包括了eclipse的企业级JavaIDE，普通用没有必要，但是为了就业可以去学学。\n前端三件套 vscode：前端就用vscode，毕竟不需要太花里胡哨的功能，所有花里胡哨都是要靠自己来实现的。\nGolang vim：因为需求原因需要在Linux下开发， 感觉vim比其它如vscode更加合适。\nGoland：jetbrains开发的软件，有教育版，但是有时限，方便是方便但是不想用。\nPython Anaconda：这并不是一个IDE，而是一个工具包，包含了很多python的开发环境、工具包啥的，主要是我平常用Python主要还是做点科学计算的东西，所以就用这个包里附带的那些开发环境就好了，像是Pycharm啥的虽然好用但是平常用不到。\n办公 Typora：超好用的markdown编辑器，本质上也可以当成一个浏览器来开发，平常不想用word写东西的时候就用这个了。\nCopyTranslator：一个方便的翻译软件，有类似于划词翻译的功能，不过本身不具有词典，需要搭配百度、谷歌的翻译来运行。\nMATLAB：工科学生必备\nVirtual BOX：虚拟机，开源免费，代替VMWare\ngeek：一个卸载软件，因为不想用那些软件助手来给我发广告，geek为一个开源软件\nListary：双击Ctrl搜索文件真的很方便，有免费版，而且附带很多高级功能\nDropit：一个收纳归类软件，开源免费，快速整理文件，不过学习成本有点高\nSnipaste：开源截屏软件，f1快速截屏，不需要单独开qq了\nCaptura：开源录屏软件，而且附带其它像是直播等功能\nOBS Studio：开源录屏软件，主打直播用，不过我没咋用过，当时只是单纯下载下来和Captura进行比较的，功能更多但更难学。\nScreeToGIF：开源GIF录制软件，不过界面我不喜欢有点丑\n不务正业 Blender：开源艺术3D建模软件，做点动画渲染啥的比较好，我喜欢用来渲染我的PCB效果图\nShotCut：开源剪辑软件，其它类似产品可以有达芬奇，但是我用不到达芬奇那么复杂的功能，就用这个够了\nGIMP：开源修图软件，PS替代品，在Linux环境下更好用\nInkscape：总是会念成inkspace的矢量图编辑软件，AI替代品，在Linux环境下更好用\nCura：3D打印切片软件，开源免费\n","date":"2020-12-20T16:58:48+08:00","image":"/posts/%E6%9D%82%E8%B0%88/%E6%88%91%E7%9A%84%E7%9E%8E%E6%90%9E%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E9%9B%86%E4%BB%A5%E5%8F%8A%E5%AF%B9%E4%B8%80%E4%BA%9B%E5%B7%A5%E5%85%B7%E7%9A%84%E6%AF%94%E8%BE%83/cover.jpg","permalink":"/posts/%E6%9D%82%E8%B0%88/%E6%88%91%E7%9A%84%E7%9E%8E%E6%90%9E%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E9%9B%86%E4%BB%A5%E5%8F%8A%E5%AF%B9%E4%B8%80%E4%BA%9B%E5%B7%A5%E5%85%B7%E7%9A%84%E6%AF%94%E8%BE%83/","title":"我的瞎搞开发工具集以及对一些工具的比较"},{"content":"终于开始学习Linux的C语言编程了嘤嘤嘤，有种终于入门的感动\n关于Linux中C语言编程包括vim、gcc、makefile这些工具的用法这些不是本次的主题，我在这里就不详细展开了，本文只阐述一下main函数的参数调用问题。\n工具 基本原理 实现与解析 工具 VirtualBox 6.1.12，Ubuntu 20.04，Code::Blocks，C\n基本原理 众所周知，main函数对于系统来说其实也只是一个普通的函数，它作为一个接口与系统进行连接，每次系统调用main函数生成的程序文件。而Linux系统其实又是通过C语言写出来的，本质上其实也只是一个C程序。而调用C生成的程序文件其实就等同于将Linux的一个程序运行指针调用这个程序的地址，然后这个程序就执行起来了，而我们可以通过main函数的两个参数接口在调用这个程序的时候进行传参。\n实现与解析 // Hello.c，用于测试main参数传递 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; int main(int argc, char *argv[]) { printf(\u0026#34;程序地址为：%s\\n\u0026#34;, argv[0]); printf(\u0026#34;本次传入参数共有%d个\\n\u0026#34;, argc - 1); int i = 1; while(i \u0026lt; argc)\t// 通过循环遍历传入所有参数 { switch(argv[i][0])\t// 通过首字母进行快速定位 { case \u0026#39;w\u0026#39;: case \u0026#39;W\u0026#39;: { if(strcmp(argv[i], \u0026#34;World\u0026#34;) == 0) { printf(\u0026#34;Hello World!\\n\u0026#34;); break; } else printf(\u0026#34;Is W/w but not World~\\tHello %s\\n\u0026#34;, argv[i]);\t// 是W/w但是并非World break; } case \u0026#39;l\u0026#39;: case \u0026#39;L\u0026#39;: { if(strcmp(argv[i], \u0026#34;Linux\u0026#34;) == 0) { printf(\u0026#34;Hello Linux!\\n\u0026#34;); break; } else printf(\u0026#34;Is W/w but not World~\\tHello %s\\n\u0026#34;, argv[i]);\t// 是L/l但是并非Liunx break; } default: printf(\u0026#34;This is %s\\n\u0026#34;, argv[i]); break; } i++ } return 0; } 这里我们可以看到main函数中有两个参数，分别是int类型的argc和char**类型的argv两个参数，当然这两个参数的名字可以变换，就像普通函数的形参一样，但是通常大家都用这两个，相当于约定俗成了。可以看出argv是一个指向char数组的指针数组的数组指针，听上去有点绕，但是用char * argv[]应该就比较好理解了，或者说甚至可以直接当成一个二维数组，其中第一维是一堆地址，每个地址分别指向一个char数组的首地址，而char数组则构成了第二维。而argc则记录了其中第一维的指针argv[]的个数。\n不过值得注意的是，argv有一个默认值argv[0]为程序自身所处地址，比如说如果我们在Windows下检测一个在D:\\Code下的test.exe函数，其argv[0]的值就为：D:\\Code\\test.exe。\n我用如下程序进行了测试：\n#include \u0026lt;stdio.h\u0026gt; int main(int argc, char * argv[]) { printf(\u0026#34;当前文件的路径：%s\\n\u0026#34;, argv[0]); return 0; } 输出结果如下：\n而我们刚才说过argc记录的argv[]的个数，所以同理，我们知道argc的默认值为1。\n所以我们其实就可以把*argv[] 当成一堆指令，每个指令argv[i]在传入到程序中后就是一个char数组的形式。用归类拓展的方式去理解，其实我们平时在cmd中使用的help xx也许其实也相当于是一个程序，help是函数名而xx则是一个参数（当然事实上是不是这样我也不知道，只是做一个例子便于理解，不过想来相差不大）。\n而有了这些认识，我们再看一下上面的程序，就不难理解我的操作了：\n在输出了地址和本次传入参数个数后，程序将会从我传入的第一个参数（也就是argv[1]）开始，一直到最后一个参数，分别将其作为指令进行判断，如果为Linux或World则输出Hello World，其他指令则以This is xx进行输出，而如果判断出首字母为L/l或W/w的指令则会多输出一句Is W/w but not World~\\t，最终运行结果如下：\n","date":"2020-12-20T00:14:50+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E7%9A%84main%E5%87%BD%E6%95%B0%E8%A7%A3%E6%9E%90/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E7%9A%84main%E5%87%BD%E6%95%B0%E8%A7%A3%E6%9E%90/","title":"C的main函数解析"},{"content":"这是对我之前《如何假装用C语言为嵌入式写一个新语言》进行一个举例，以便更好地理解这种编程方式。\n报头(S)报尾(E) 控制指令 程序逻辑指令 举例 报头(S)报尾(E) 报头和报尾 ASCII BUGUS 42 55 47 55 53 BUGUE 42 55 47 55 45 CODES 43 4f 44 45 53 CODEE 43 4f 44 45 45 VORCS 56 4f 52 43 53 VORCE 56 4f 52 43 45 CONSTS 43 4f 4e 53 54 53 CONSTE 43 4f 4e 53 54 45 RVARS 52 56 41 52 53 RVARE 52 56 41 52 45 SAVES 53 41 56 45 53 SAVEE 53 41 56 45 45 WVARS 57 56 41 52 53 WVARE 57 56 41 52 45 控制指令 指令内容 指令标号（用户不可见） 参数1 参数2 输入输出选择（基础IO模块） 0x02 引脚选择（暂时0x01-0x0a） 输入0输出1 输出高低选择（基础IO模块） 0x03 引脚选择（暂时0x01-0x0a） 低电平0高电平1 毫秒级延时（时序模块） 0x04 延时时间（暂时0x01-0xff） 程序逻辑指令 指令内容 指令标号 信息1 信息2 信息3 信息4 信息5 信息6 循环启动器（程序基础控制模块） 0x01 CODE返回位 VORC返回位 CONST返回位 RVAR返回位 SAVE返回位 WVAR返回位 举例 加入用户输入了这样一个程序：\nflowchart TD st([开始]) set5[设置5号引脚为输入] set6[设置6号引脚为输出] set6high[设置6号输出为高电平] con{\"while(1)?\"} blink_delay1[延时20ms] blink_set0[设置6号输出为低电平] blink_delay2[延时20ms] blink_set1[设置6号输出为高电平] st--\u003eset5--\u003eset6--\u003eset6high--\u003econ con--\u003e|是|blink_delay1 blink_delay1--\u003eblink_set0--\u003eblink_delay2--\u003eblink_set1--\u003econ而我们的MCU工作流程如下：\ngraph TB subgraph MCU key(自动指针读)--\u003ecase[进入case] case--\u003epara[参数获取] para--\u003eisVar(自动指针读) isVar--常量--\u003econst(自动指针读) isVar--变量--\u003ereadvarP(自动指针读) readvarP--read指针--\u003ervar[读变量数组] rvar--\u003eans[输出] const--\u003eans ans--\u003eisSave(自动指针读) isSave--不保存--\u003ecase isSave--保存--\u003ewritevarP(自动指针读) writevarP--write指针--\u003ewvar[写变量数组] wvar--\u003ecase end subgraph 指令表 keyline[指令表CODE]--\u003ekey isVarline[判断选择变量0/常量表1VORC]--\u003eisVar constline[常量表CONST]--\u003econst readvarline[变量表的read指针表RVAR]--\u003ereadvarP isSaveline[判断是1否0保存输出表SAVE]--\u003eisSave writevarline[变量表的write指针表WVAR]--\u003ewritevarP end这里我们只截取其中一个函数的工作流程和并对循环方式进行说明：\nsequenceDiagram participant P as 系统 participant C as CODE participant V as VORC participant CO as CONST participant R as RVAR participant S as SAVE participant W as WVAR par 调用函数 C-\u003e\u003e+P: 0x02 P--\u003e\u003e-C: 下标=1 Note over C,P: 输入输出选择 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=1 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x05 C--\u003e\u003e-CO: 下标=1 Note over CO, C: 5号引脚 and 参数2 V-\u003e\u003e+C:0x01 C--\u003e\u003e-V: 下标=2 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x00 C--\u003e\u003e-CO: 下标=2 Note over CO, C: 输入 end end par 调用函数 C-\u003e\u003e+P: 0x02 P--\u003e\u003e-C: 下标=2 Note over C,P: 输入输出选择 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=3 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x05 C--\u003e\u003e-CO: 下标=3 Note over CO, C: 6号引脚 and 参数2 V-\u003e\u003e+C:0x01 C--\u003e\u003e-V: 下标=4 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x01 C--\u003e\u003e-CO: 下标=4 Note over CO, C: 输出 end end par 调用函数 C-\u003e\u003e+P: 0x03 P--\u003e\u003e-C: 下标=3 Note over C,P: 输出高低选择 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=5 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x05 C--\u003e\u003e-CO: 下标=5 Note over CO, C: 6号引脚 and 参数2 V-\u003e\u003e+C:0x01 C--\u003e\u003e-V: 下标=6 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x01 C--\u003e\u003e-CO: 下标=6 Note over CO, C: 输出高电平 end end loop 循环启动器 Note over P,W: 上位机中记录此时的下标：CODE=3,VORC=6,CONST=6,RVAR=0,SAVE=0,WVAR=0 par 调用函数 C-\u003e\u003e+P: 0x04 P--\u003e\u003e-C: 下标=4 Note over C,P: 毫秒级延时 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=7 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x14 C--\u003e\u003e-CO: 下标=7 Note over CO, C: 20ms延时 end end par 调用函数 C-\u003e\u003e+P: 0x03 P--\u003e\u003e-C: 下标=5 Note over C,P: 输出高低选择 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=8 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x05 C--\u003e\u003e-CO: 下标=8 Note over CO, C: 6号引脚 and 参数2 V-\u003e\u003e+C:0x01 C--\u003e\u003e-V: 下标=9 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x00 C--\u003e\u003e-CO: 下标=9 Note over CO, C: 输出低电平 end end par 调用函数 C-\u003e\u003e+P: 0x04 P--\u003e\u003e-C: 下标=6 Note over C,P: 毫秒级延时 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=10 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x14 C--\u003e\u003e-CO: 下标=10 Note over CO, C: 20ms延时 end end par 调用函数 C-\u003e\u003e+P: 0x03 P--\u003e\u003e-C: 下标=7 Note over C,P: 输出高低选择 and 传入形参 par 参数1 V-\u003e\u003e+C: 0x01 C--\u003e\u003e-V: 下标=11 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x05 C--\u003e\u003e-CO: 下标=11 Note over CO, C: 6号引脚 and 参数2 V-\u003e\u003e+C:0x01 C--\u003e\u003e-V: 下标=12 Note over V,C: 选择常数 CO-\u003e\u003e+C: 0x01 C--\u003e\u003e-CO: 下标=12 Note over CO, C: 输出高电平 end end par 调用函数 C-\u003e\u003eP: 0x01 Note over C,P: 循环回溯 and 传入形参 par CODE P-\u003e\u003eC: 下标=3 and VORC P-\u003e\u003eV: 下标=6 and CONST P-\u003e\u003eCO:下标=6 and RVAR P-\u003e\u003eR:下标=0 and SAVE P-\u003e\u003eS:下标=0 and WVAR P-\u003e\u003eW:下标=0 end end Note over P,W: 将下标指针返回到循环启动器开始时的状态 end这里需要注意的是在这个例子中我们其实并没有输出值的，所以没有对输出值进行操作，5号的demo版暂时先不做又返回值的功能。\n通过以上的序列图，我们就可以写出我们的报文了：\ngraph TB BUGUS[BUGUS 42 55 47 55 53] BUGUE[BUGUE 42 55 47 55 45] CODES[CODES 43 4f 44 45 53] CODEE[CODEE 43 4f 44 45 45] VORCS[VORCS 56 4f 52 43 53] VORCE[VORCE 56 4f 52 43 45] CONSTS[CONSTS 43 4f 4e 53 54 53] CONSTE[CONSTE 43 4f 4e 53 54 45] RVARS[RVARS 52 56 41 52 53] RVARE[RVARE 52 56 41 52 45] SAVES[SAVES 53 41 56 45 53] SAVEE[SAVEE 53 41 56 45 45] WVARS[WVARS 57 56 41 52 53] WVARE[WVARE 57 56 41 52 45] CODE[CODE 02 02 03 04 03 04 03 01] BUGUS--\u003eCODES--\u003eCODE--\u003eCODEE VORC[VORC 01 01 01 01 01 01 01 01 01 01 01 01] CODEE--\u003eVORCS--\u003eVORC--\u003eVORCE CONST[CONST 05 00 05 01 05 01 14 05 00 14 05 01 03 06 06 00 00 00] VORCE--\u003eCONSTS--\u003eCONST--\u003eCONSTE RVAR[RVAR] CONSTE--\u003eRVARS--\u003eRVAR--\u003eRVARE SAVE[SAVE] RVARE--\u003eSAVES--\u003eSAVE--\u003eSAVEE WVAR[WVAR] SAVEE--\u003eWVARS--\u003eWVAR--\u003eWVARE--\u003eBUGUE​ 将该报文通过串口发送到单片机，单片机即可进行工作运行。\n","date":"2020-12-01T22:09:18+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E8%87%AA%E5%88%B6%E8%84%9A%E6%9C%AC%E8%AF%AD%E8%A8%80/%E9%80%9A%E8%BF%87%E6%8A%A5%E6%96%87%E5%AF%B9%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%BF%9B%E8%A1%8C%E7%BC%96%E7%A8%8B%E4%B8%BE%E4%BE%8B/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E8%87%AA%E5%88%B6%E8%84%9A%E6%9C%AC%E8%AF%AD%E8%A8%80/%E9%80%9A%E8%BF%87%E6%8A%A5%E6%96%87%E5%AF%B9%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%BF%9B%E8%A1%8C%E7%BC%96%E7%A8%8B%E4%B8%BE%E4%BE%8B/","title":"通过报文对嵌入式进行编程举例"},{"content":"我们在使用无线嵌入式器件的时候，或多或少对于AT指令都有所了解——通过通信口发送一串字符，以AT开头，换行符结尾，中间插入指令内容，就可以实现对芯片寄存器的配置。这种指令式配置类似于蓝牙、WiFi等设备可能比较简单，但是如果我们打算用它去实现一个比较复杂的编程呢？是否也可以通过这种方式，是否可以在不使用编译器的情况下，通过指令调取内部程序并执行？我将会通过一个小demo进行测试这种编程方式的可行性。 本文的程序可以在这里下载\n使用工具 工作流程 指令程序设计 功能映射并单指令运行 多指令运行 上位机编程 传递参数 返回值 思路解析 参数 返回值 变量和常量 输入判断 输出判断 报文 总结和反思 使用工具 Code::Blocks， C语言\n工作流程 整个系统的工作流程比较简单，如下图所示：\ngraph LR Send[报文发送端] --通信--\u003e ReCV subgraph 单片机 ReCV[报文接收端]--\u003eMem[存储] Mem--\u003eRead[读取指令] Read--函数和参数--\u003eWork[完成操作] Work--返回值--\u003eMem end这里的报文内容其实来自于上位机对下位机内部已有程序的映射，例如我在单片机中已经设置好了一个函数void Exp()，那么我们就可以在上位机制作一个模块，使用者如果调取了这个模块那么上位机就会自动将该模块对应的指令值插入到报文中，假设这里是0x05，那么下位机如果在报文中检测到了0x05，就会自动执行Exp()函数。\n当然详细内容会更复杂，我们现在只是有了一个基本概念，还需要继续完善内容。\n指令程序设计 如果我们需要实现指令编程，那么内部肯定不止有Exp()一个程序，先不考虑指针传参的情况，我们根据参数和返回值的不同情况可以将函数分为以下几种情况：\n无参数无返回值 无参数有返回值 有参数无返回值 有参数有返回值 而实际上参数的个数不定，我们暂时先不考虑不定参数的情况，可以分为：\n单个参数 多个参数 而实际上多个参数的情况可以包含单个参数，那么我们就在这个demo中设置如下几种情况。\n无参数无返回值函数 无参数有返回值函数 两个参数无返回值函数 三个参数有返回值函数 为此，我设计了如下几个函数：\n/* * 全局变量声明 */ int static flag = 0; /* *\t函数声明和定义 */ void SayHello() // 打印一行Hello World! { printf(\u0026#34;Hello World!\\n\u0026#34;); } int NowFlag() // 将flag值自增一然后打印出来并返回 { flag++; printf(\u0026#34;flag = %d\\n\u0026#34;, flag); return flag; } void ShowSum(int a, int b) // 打印两个参数之和 { printf(\u0026#34;%d + %d = %d\\n\u0026#34;, a, b, a+b); } int GetSum(int a, int b, int c) // 打印并返回三参数之和 { int sum = a + b + c; printf(\u0026#34;%d + %d + %d = %d\\n\u0026#34;,a, b, c, sum); return sum; } 现在我们在main中测试调用他们并运行一下：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; /* * 全局变量声明 */ /* *\t函数声明和定义 */ int main() { SayHello(); NowFlag(); NowFlag(); ShowSum(GetSum(0, 1, 2), 2); return 0; } 运行结果为：\n功能映射并单指令运行 我们已经写好并确定以上四个程序可以运行了，接下来我们对不同的功能进行编码映射，这样我们就可以选择不同指令完成不同的操作了。\nint main() { unsigned char instruction = 0x03; // 指令 switch(instruction) // 判断指令 { case 0x01: { SayHello(); break; } case 0x02: { NowFlag(); break; } case 0x03: { GetSum(0, 1, 2); break; } case 0x04: { ShowSum(1, 2); break; } default: { break; } } return 0; } 通过这个程序，我们可以完成单个预设指令实现操作了，这里选择指令0x03进行操作：\n多指令运行 但是只能实现上面这中单指令运行很明显是远远不够的，我们还需要实现多指令运行，即一次性可以运行多条指令。\n方法也很简单，只需要一个循环和一个数组就可以实现。\n#define MAX_LEN 6 int main() { unsigned char instruction[MAX_LEN] = {0x03, 0x02, 0x01, 0x02, 0x04, 0x06}; // 指令 for(int i = 0; i \u0026lt; MAX_LEN; i++) { switch(instruction[i]) // 判断指令 { case 0x01: { SayHello(); break; } case 0x02: { NowFlag(); break; } case 0x03: { GetSum(0, 1, 2); break; } case 0x04: { ShowSum(1, 2); break; } default: { break; } } } return 0; } 这样子就可以运行多条语句了：\n上位机编程 不过很显然，现在这样子和上位机编程有很大区别，至少使用者就很没有参与感。所以我们在这里制作出一个编程功能，然后把生成报文发送给下位机程序，然后来执行。\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define MAX_LEN 60 /* * 全局变量声明 */ int static flag = 0; /* *\t函数声明和定义 */ void SayHello() // 打印一行Hello World! { printf(\u0026#34;Hello World!\\n\u0026#34;); } int NowFlag() // 将flag值自增一然后打印出来并返回 { flag++; printf(\u0026#34;flag = %d\\n\u0026#34;, flag); return flag; } void ShowSum(int a, int b) // 打印两个参数之和 { printf(\u0026#34;%d + %d = %d\\n\u0026#34;, a, b, a+b); } int GetSum(int a, int b, int c) // 打印并返回三参数之和 { int sum = a + b + c; printf(\u0026#34;%d + %d + %d = %d\\n\u0026#34;,a, b, c, sum); return sum; } void Coding(unsigned char codeStr[]) // 上位机编程 { printf(\u0026#34;请按顺序输入程序编号：1|SayHello, 2|NowFlag, 3|GetSum, 4|ShowSum, 5|编译\\n\u0026#34;); int i; // 记录行号 int inst; // 程序编号，这里其实最好用 for(i = 0; i \u0026lt;= MAX_LEN; i++) // 程序不能超过最大长度 { printf(\u0026#34;%d| \u0026#34;, i + 1); // 格式占用 scanf(\u0026#34;%d\u0026#34;, \u0026amp;inst); // 获取指令 if(inst == 5) // 如果选择了编译则跳出循环 { printf(\u0026#34;编译成功！\\n\u0026#34;); codeStr[i] = 0x06;\t// 任意可以结束下位机switch的命令 break; } else if(inst \u0026gt; 5 || inst \u0026lt; 1) // 如果输入指令错误，则提示错误并重新输入 { printf(\u0026#34;无当前指令！请重新输入！\u0026#34;); i--; } else // 如果正确输入，则将内容记录 { codeStr[i] = inst * 0x01; } printf(\u0026#34;\\n\u0026#34;); } } int main() { unsigned char instruction[MAX_LEN];// 指令存储器 Coding(instruction); // 获取指令 for(int i = 0; i \u0026lt; MAX_LEN; i++) { switch(instruction[i]) // 判断指令 { case 0x01: { SayHello(); break; } case 0x02: { NowFlag(); break; } case 0x03: { GetSum(0, 1, 2); break; } case 0x04: { ShowSum(1, 2); break; } default: { break; } } } return 0; } 这时当前的程序，我们可以根据宏定义中MAX_LEN的长度制作最大等长的程序设计，我随便编了一下，效果感觉还不错：\n传递参数 现在我们已经可以完成任意的无参无返回的函数执行了，但是光是这样肯定远远不够，我们需要实现参数传递，为此我们需要另外新定义一个数组用来存储参数，然后每次在选择了指令后都会进入参数输入界面：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define MAX_LEN 60 /* * 全局变量声明 */ int static flag = 0; /* *\t函数声明和定义 */ void SayHello() // 打印一行Hello World! { printf(\u0026#34;Hello World!\\n\u0026#34;); } int NowFlag() // 将flag值自增一然后打印出来并返回 { flag++; printf(\u0026#34;flag = %d\\n\u0026#34;, flag); return flag; } void ShowSum(int a, int b) // 打印两个参数之和 { printf(\u0026#34;%d + %d = %d\\n\u0026#34;, a, b, a+b); } int GetSum(int a, int b, int c) // 打印并返回三参数之和 { int sum = a + b + c; printf(\u0026#34;%d + %d + %d = %d\\n\u0026#34;,a, b, c, sum); return sum; } void Coding(unsigned char codeStr[], int intStr[]) // 上位机编程 { printf(\u0026#34;请按顺序输入程序编号：1|SayHello, 2|NowFlag, 3|ShowSum, 4|GetSum, 5|编译\\n\u0026#34;); int i; // 记录行号 int j = 0; // 记录参数 int paraNum; // 记录参数个数 int inst; // 程序编号 for(i = 0; i \u0026lt;= MAX_LEN; i++) // 程序不能超过最大长度 { printf(\u0026#34;%d| \u0026#34;, i + 1); // 格式占用 scanf(\u0026#34;%d\u0026#34;, \u0026amp;inst); // 获取指令 switch(inst) { case 1: { codeStr[i] = inst * 0x01; break; } case 2: { codeStr[i] = inst * 0x01; break; } case 3: { codeStr[i] = inst * 0x01; for(paraNum = 0; paraNum \u0026lt; 2; paraNum++) { printf(\u0026#34;请输入参数%d:\u0026#34;, paraNum + 1); scanf(\u0026#34;%d\u0026#34;, intStr+j); j++; } break; } case 4: { codeStr[i] = inst * 0x01; for(paraNum = 0; paraNum \u0026lt; 3; paraNum++) { printf(\u0026#34;请输入参数%d:\u0026#34;, paraNum + 1); scanf(\u0026#34;%d\u0026#34;, intStr+j); j++; } break; } case 5: { printf(\u0026#34;编译成功！\\n\u0026#34;); codeStr[i] = 0x06;\t// 任意可以结束下位机switch的命令 return; } default: { printf(\u0026#34;无当前指令！请重新输入！\u0026#34;); i--; } } printf(\u0026#34;\\n\u0026#34;); } } int main() { unsigned char instruction[MAX_LEN];// 指令存储器 int intParaStr[MAX_LEN * 3]; // int参数存储器，这里假设最大可能情况作为空间 Coding(instruction, intParaStr); // 获取指令 int pIntPara = 0; // 确定参数 for(int i = 0; i \u0026lt; MAX_LEN; i++) { switch(instruction[i]) // 判断指令 { case 0x01: { SayHello(); break; } case 0x02: { NowFlag(); break; } case 0x03: { int a = intParaStr[pIntPara]; pIntPara++; int b = intParaStr[pIntPara]; pIntPara++; ShowSum(a, b); break; } case 0x04: { int a = intParaStr[pIntPara]; pIntPara++; int b = intParaStr[pIntPara]; pIntPara++; int c = intParaStr[pIntPara]; pIntPara++; GetSum(a, b, c); break; } default: { break; } } } return 0; } 我们再次测试一下：\n返回值 在记录返回值这一块，我终究还是遇到了问题，虽然从理论上只是一个数据传递的过程，但是因为我之前随意的态度导致代码过于凌乱，不得不咽下苦果在原有基础上进行大改。不过理论不变，所以前面的内容仍然是正确的，只是写代码的时候会很痛苦。\n程序一共分为五个文件，这里先贴出程序：\n/****************************************************************** * @File name: MainWorking.c * @Author: Yun * @Date: 2020-11-24 * @Description: 本文件为c主函数 ******************************************************************/ #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #include \u0026#34;UserProgram.h\u0026#34; #include \u0026#34;AppProgram.h\u0026#34; int main() { unsigned char instruction[MAX_LEN];// 指令存储器 CtrlOut O; CtrlIn I; unsigned int OutStr[MAX_LEN]; Coding(instruction, \u0026amp;O, \u0026amp;I); MCUCtrl(instruction, OutStr, \u0026amp;O, \u0026amp;I); } /****************************************************************** * @File name: UserProgram.c * @Author: Yun * @Date: 2020-11-24 * @Description: 本文件用于存储用户操作函数的定义 ******************************************************************/ #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #include \u0026#34;UserProgram.h\u0026#34; /* 无参无返回begin */ /********************************************************* * Function name ：P_SayHello * Description ：打印一行Hello World * Parameter ：None * Return ：None **********************************************************/ void P_SayHello() { printf(\u0026#34;Hello World!\\n\u0026#34;); } /* 无参无返回end */ /* 无参有返回begin */ /********************************************************* * Function name ：P_NowFlag * Description ：内置一静态变量flag，每次调用会让flag自增一，然后输出值并返回 * Parameter ：None * Return ：int, flag当前的值 **********************************************************/ int P_NowFlag() { int static flag = 0; flag++; printf(\u0026#34;flag = %d\\n\u0026#34;, flag); return flag; } /* 无参有返回end */ /* 有参无返回begin */ /********************************************************* * Function name ：P_ShowSum * Description ：将两数相加并打印 * Parameter ： * @a 作为加数的第一个整数 * @b 作为加数的第二个整数 * Return ：None **********************************************************/ void P_ShowSum(int a, int b) { printf(\u0026#34;%d + %d = %d\\n\u0026#34;, a, b, a+b); } /* 有参无返回end */ /* 有参有返回begin */ /********************************************************* * Function name ：P_GetSum * Description ：将三数相加并打印，返回结果 * Parameter ： * @a 作为加数的第一个整数 * @b 作为加数的第二个整数 * @c 作为加数的第三个整数 * Return ：int, 三个参数之和 **********************************************************/ int P_GetSum(int a, int b, int c) { int sum = a + b + c; printf(\u0026#34;%d + %d + %d = %d\\n\u0026#34;,a, b, c, sum); return sum; } /* 有参有返回end */ /****************************************************************** * @File name: AppProgram.c * @Author: Yun * @Date: 2020-11-23 * @Description: 本文件用于存储上位机控制函数和下位机控制函数的定义 ******************************************************************/ #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;stdbool.h\u0026gt; #include \u0026#34;AppProgram.h\u0026#34; #include \u0026#34;UserProgram.h\u0026#34; /******************************************* 上位机输出值控制 ********************************************************/ /********************************************************* * Function name ：OutSavingCtrl * Description ：用于控制保存输出，如果控制有返回值的函数则可以使用 * 在 * Parameter ： * @O 结构体指针，该结构体内包含有控制所需的数组和值 * Return ：None **********************************************************/ void OutSavingCtrl(CtrlOut *O) { int check; printf(\u0026#34;请问是否保存输出结果？(1|是||否|0):\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;check); if(check == 0) // 不保存 { O-\u0026gt;O_isSaving[O-\u0026gt;pO_isSaving] = 0; // 记录此处不保存输出结果 O-\u0026gt;pO_isSaving++; // 让下标指向下一位 printf(\u0026#34;不保存此操作不输出...\\n\u0026#34;); } else if(check == 1) // 保存 { O-\u0026gt;O_isSaving[O-\u0026gt;pO_isSaving] = 1; // 记录此处保存输出结果 O-\u0026gt;pO_isSaving++; // 让下标指向下一位 IsAutoSaving(O); // 判断是否自动保存 } else // 错误 { printf(\u0026#34;输入错误，请重新输入！\\n\u0026#34;); OutSavingCtrl(O); } } /********************************************************* * Function name ：IsAutoSaving * Description ：用于控制自动保存，被用于OutSavingCtrl函数中，不建议单独使用 * Parameter ： * @O 结构体指针，该结构体内包含有控制所需的数组和值 * Return ：None **********************************************************/ void IsAutoSaving(CtrlOut *O) { int check; // 判定是否自动保存 unsigned int addr; // 保存地址下标 printf(\u0026#34;请问是否自动保存？(1|自动||手动|0):\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;check); if(check == 0) // 手动保存 { printf(\u0026#34;请手动输入保存地址下标：\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;addr); printf(\u0026#34;此操作保存地址下标为：%d\\n\u0026#34;, addr); O-\u0026gt;O_SavingAddr[O-\u0026gt;pO_SavingAddr] = addr; // 记录当前保存地址下标 O-\u0026gt;pO_SavingAddr++; // 让下标指向下一位 } else if(check == 1) // 自动保存 { printf(\u0026#34;自动保存此操作输出值...\\n\u0026#34;); O-\u0026gt;O_SavingAddr[O-\u0026gt;pO_SavingAddr] = O-\u0026gt;O_AutoSavingAddr; // 记录自动保存下标 printf(\u0026#34;此操作保存地址下标为：%d\\n\u0026#34;, O-\u0026gt;O_AutoSavingAddr); O-\u0026gt;O_AutoSavingAddr++; // 自动保存个数加一 O-\u0026gt;pO_SavingAddr++; // 让下标指向下一位 } else { printf(\u0026#34;输入错误，请重新输入！\\n\u0026#34;); IsAutoSaving(O); } } /******************************************* 上位机输入值控制 ********************************************************/ /********************************************************* * Function name ：InReadCtrl * Description ：用于控制参数，有参数的操作都需要使用此函数 * Parameter ： * @I 结构体指针，该结构体内包含有控制所需的数组和值 * @paraNum 操作的参数个数 * Return ：None **********************************************************/ void InReadCtrl(CtrlIn *I, int paraNum) { int check; // 用于判断输入来源是否为返回值或手动输入 int tempVal; // 用于临时存储参数 unsigned int tempAddr; // 用于临时存储下标 for(int i = 0; i \u0026lt; paraNum; i++) { printf(\u0026#34;参数%d|请问该参数为其它函数输出还是手动输入值?(1|输出||手动|0):\u0026#34;, i); scanf(\u0026#34;%d\u0026#34;, \u0026amp;check); if(check == 0) // 手动保存 { I-\u0026gt;I_isRead[I-\u0026gt;pI_isRead] = 0; // 记录参数来自手动输入 I-\u0026gt;pI_isRead++; // 让下标指向下一位 printf(\u0026#34;请手动输入该参数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;tempVal); I-\u0026gt;I_ReadVal[I-\u0026gt;pI_ReadVal] = tempVal; // 记录参数 I-\u0026gt;pI_ReadVal++; // 让下标指向下一位 } else if(check == 1) // 自动保存 { I-\u0026gt;I_isRead[I-\u0026gt;pI_isRead] = 1; // 记录参数来自输出值 I-\u0026gt;pI_isRead++; // 让下标指向下一位 printf(\u0026#34;请输入输出保存下标：\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;tempAddr); I-\u0026gt;I_ReadVal[I-\u0026gt;pI_ReadVal] = tempAddr; // 此时记录下标值 I-\u0026gt;pI_ReadVal++; // 让下标指向下一位 } else { printf(\u0026#34;输入错误，请重新输入！\\n\u0026#34;); i--; } } } /*********************************************** 上位机编程 **********************************************************/ /********************************************************* * Function name ：Coding * Description ：用于上位机编程，此为上位机主程序，通过该程序可以编程CtrlOut结构体和CtrlIn结构体，以及一个操作指令数组，生成为传输给下位机的代码 * Parameter ： * @codeStr[] 操作指令集数组 * @O 结构体指针，输出控制结构体 * @I 结构体指针，输入控制结构体 * Return ：None **********************************************************/ void Coding(unsigned char codeStr[], CtrlOut *O, CtrlIn *I) // 上位机编程 { InitIO(I, O); printf(\u0026#34;请按顺序输入程序编号：1|SayHello, 2|NowFlag, 3|ShowSum, 4|GetSum, 5|编译\\n\u0026#34;); int line; // 记录指令号 int inst; // 程序编号 for(line = 0; line \u0026lt;= MAX_LEN; line++) // 程序不能超过最大长度 { printf(\u0026#34;%d| \u0026#34;, line + 1); // 格式占用 scanf(\u0026#34;%d\u0026#34;, \u0026amp;inst); // 获取指令 switch(inst) { case 1: // 无参数无返回值 { codeStr[line] = inst * 0x01; break; } case 2: // 无参数有返回值 { codeStr[line] = inst * 0x01; OutSavingCtrl(O); break; } case 3: // 有参数无返回值 { codeStr[line] = inst * 0x01; InReadCtrl(I, 2); break; } case 4: // 有参数有返回值 { codeStr[line] = inst * 0x01; InReadCtrl(I, 3); OutSavingCtrl(O); break; } case 5: { printf(\u0026#34;编译成功！\\n\u0026#34;); codeStr[line] = 0xff;\t// 任意可以结束下位机switch的命令 return; } default: { printf(\u0026#34;无当前指令！请重新输入！\u0026#34;); line--; break; } } printf(\u0026#34;\\n\u0026#34;); } printf(\u0026#34;当前已达到可编程最大长度！\u0026#34;); } /********************************************************* * Function name ：InitIO * Description ：用于初始化结构体，这样可以让IO结构体指针指向开头，该函数已被用于Coding和MCUCtrl函数，不建议单独使用 * Parameter ： * @I 结构体指针，输入结构体 * @O 结构体指针，输出结构体 * Return ：None **********************************************************/ void InitIO(CtrlIn *I, CtrlOut *O) { I-\u0026gt;pI_isRead = 0; I-\u0026gt;pI_ReadVal = 0; O-\u0026gt;O_AutoSavingAddr = 0; O-\u0026gt;pO_isSaving = 0; O-\u0026gt;pO_SavingAddr = 0; } /******************************************* 下位机输出值控制 ********************************************************/ /********************************************************* * Function name ：MCUOutCtrl * Description ：用于下位机输出控制，每次有返回值的函数都需要将输出值通过此函数进行操作 * Parameter ： * @OutStr[] 返回值保存数组 * @O 结构体指针，输出控制结构体 * @OutVal 函数返回值 * Return ：None **********************************************************/ void MCUOutCtrl(int OutStr[], CtrlOut *O, int OutVal) { if(!(O-\u0026gt;O_isSaving[O-\u0026gt;pO_isSaving])) // 不保存 { O-\u0026gt;pO_isSaving++; // 下标指向下一位 return; // 结束输出控制 } else // 保存 { O-\u0026gt;pO_isSaving++; // 下标指向下一位 OutStr[O-\u0026gt;O_SavingAddr[O-\u0026gt;pO_SavingAddr]] = OutVal; // 当前操作对应的保存地址记录输出值 O-\u0026gt;pO_SavingAddr++; // 下标指向下一位 } } /******************************************* 下位机输出值控制 ********************************************************/ /********************************************************* * Function name ：MCUInCtrl * Description ：用于下位机输入控制，每次有参数的函数都需要将参数通过此函数进行操作，且每个参数都要操作 * Parameter ： * @OutStr[] 返回值保存数组 * @I 结构体指针，输入控制结构体 * @InVal 整型指针，输入控制结构体，即参数以指针传入 * Return ：None **********************************************************/ void MCUInCtrl(int OutStr[], CtrlIn *I, int *InVal) { if(!(I-\u0026gt;I_isRead[I-\u0026gt;pI_isRead])) // 手动保存 { I-\u0026gt;pI_isRead++; *InVal = I-\u0026gt;I_ReadVal[I-\u0026gt;pI_ReadVal]; I-\u0026gt;pI_ReadVal++; } else // 自动保存 { I-\u0026gt;pI_isRead++; *InVal = OutStr[I-\u0026gt;I_ReadVal[I-\u0026gt;pI_ReadVal]]; I-\u0026gt;pI_ReadVal++; } } /*********************************************** 下位机控制 **********************************************************/ /********************************************************* * Function name ：MCUCtrl * Description ：用于下位机控制，下位机通过IO指令判断执行对应的操作 * Parameter ： * @instruction[] 操作指令集数组 * @OutStr[] 返回值保存数组 * @O 结构体指针，输出控制结构体 * @I 结构体指针，输入控制结构体 * Return ：None **********************************************************/ void MCUCtrl(unsigned char instruction[], int OutStr[], CtrlOut *O, CtrlIn *I) { InitIO(I, O); for(int i = 0; i \u0026lt; MAX_LEN; i++) { switch(instruction[i]) // 判断指令 { case 0x01: { P_SayHello(); break; } case 0x02: { int a = P_NowFlag(); MCUOutCtrl(OutStr, O, a); break; } case 0x03: { int a, b; MCUInCtrl(OutStr, I, \u0026amp;a); MCUInCtrl(OutStr, I, \u0026amp;b); P_ShowSum(a, b); break; } case 0x04: { int a, b, c; MCUInCtrl(OutStr, I, \u0026amp;a); MCUInCtrl(OutStr, I, \u0026amp;b); MCUInCtrl(OutStr, I, \u0026amp;c); int d = P_GetSum(a, b, c); MCUOutCtrl(OutStr, O, d); break; } default: { return; } } } } /****************************************************************** * @File name: UserProgram.h * @Author: Yun * @Date: 2020-11-23 * @Description: 本文件用于存储用户操作函数的声明 ******************************************************************/ #ifndef PROGRAMMING_H_INCLUDED #define PROGRAMMING_H_INCLUDED /* 无参无返回begin */ void P_SayHello(); /* 无参无返回end */ /* 无参有返回begin */ int P_NowFlag(); /* 无参有返回end */ /* 有参无返回begin */ void P_ShowSum(int a, int b); /* 有参无返回end */ /* 有参有返回begin */ int P_GetSum(int a, int b, int c); /* 有参有返回end */ #endif // PROGRAMMING_H_INCLUDED /****************************************************************** * @File name: AppProgram.h * @Author: Yun * @Date: 2020-11-23 * @Description: 本文件用于存储上位机控制函数和下位机控制函数的声明以及相应参数的存储 ******************************************************************/ #ifndef APPPROGRAM_H_INCLUDED #define APPPROGRAM_H_INCLUDED #define MAX_LEN 60 // 程序语句最长 /* 结构体 */ /*********************************** * Struct name ：CtrOut * Description ：用于控制输出返回值 * 在上位机使用的时候：首先判断当前是否需要保存返回值，并依次把结果记录在O_isSaving数组中 * 然后如果需要保存，则选择是否需要自动保存，自动保存等效于新建变量，手动保存等于赋值已有变量 * 如果是手动保存，则需要手动输入保存变量下标，在下位机中我们把保存的变量都放在一个数组中，参数可以通过下标从这里调用内容 * 如果是自动保存，则自动下标会自增1然后记录当前自动下标，比如第一次自动记录时自动下标为0，然后第二次为1 * // 注意，如果在手动保存时选择了比自动下标更大的数可能会导致存储内容被覆盖 * 在下位机使用的时候：实际应用中下位机需要单独另建空间对这些内容进行保存，在demo中我们会省略这部分内容 * 首先下位机在执行有返回值的操作时，会进行判断此次操作是否需要保存返回值，此时需要检测O_isSaving数组 * 在确定需要保存后，将会从O_SavingAddr数组中取出当前操作的下标，然后将其保存到自定义的函数返回值保存空间的指定下标位置 * Member ： * @O_isSaving[MAX_LEN] ：用于记录当前操作是否保存返回值 // 当且仅当有返回值时必须记录，1保存，0不保存 * @pO_isSaving ：数组下标，扫描O_isSaving数组 // 每次读写后必须移动到下一位 // 初始化时需要赋值为0 * @O_SavingAddr[MAX_LEN] ：用于记录当前保存值地址 // 当且仅当保存返回值时必须记录，如果为自动保存则自动记录当前O_AutoSavingAddr，如果为手动保存则记录输入下标值 * @pO_SavingAddr ：数组下标，扫描O_SavingAddr数组 // 每次读写后必须移动到下一位 // 初始化时需要赋值为0 * @O_AutoSavingAddr ：用于记录当前自动保存的个数 // 当且仅当自动保存时将此大小记录在O_SavingAddr中后自增一，否则不对此值执行任何操作 // 初始化时需要赋值为0 ***********************************/ typedef struct CONTROL_OUT { bool O_isSaving[MAX_LEN]; unsigned int pO_isSaving; unsigned int O_SavingAddr[MAX_LEN]; unsigned int pO_SavingAddr; unsigned int O_AutoSavingAddr; }CtrlOut; /*********************************** * Struct name ：CtrIn * Description ：用于控制输入参数值 * 在上位机使用的时候：首先判断当前参数来自手动输入还是来自输出返回值，这里可以把输入值理解为常量，把输出返回值理解为已有值变量，并将此记录在I_isReading数组中， * 然后如果来自于输入值，即常量，则在I_ReadVal数组中记录一个数字 * 如果来自于返回值，即变量，则在I_ReadVal数组中记录一个下标，该下标可以在函数返回值保存数组中进行取值 * //事实上这是一种不严谨的做法，因为两种数据类型一个是有符号，一个无符号，但是这里我们的数组比较短，所以可以暂时简便起见如此使用 * 在下位机使用的时候：实际应用中下位机需要单独另建空间对这些内容进行保存，在demo中我们会省略这部分内容 * 首先下位机在执行有参数的操作时，会对每个参数进行判断当前参数来自于一个变量还是常量， * 根据我们在I_isRead数组中的记录，如果为常量，即输入值，则直接将对应位置的I_ReadVal中的值取出作为参数 * 否则为变量，即返回值，需要先从I_ReadVal中取出对应位置的值作为下标，在自定义的函数返回值保存将对应下标的值取出作为参数 * Member ： * @I_isRead[MAX_LEN * 3] ：用于记录当前操作函数是否来自返回值 // 当且仅当有参数时必须记录，每个参数都要记录，1来自返回值，0来自输入值 * @pI_isRead ：数组下标，扫描I_isReading数组 // 每次读写必须移动到下一位 // 初始化时需要赋值为0 * @I_ReadVal[MAX_LEN * 3] ：用于记录参数值，顺序表，对应每个参数 // 如果来自返回值则根据输入下标从O_SavingVal数组提取，如果来自输入值则直接记录输入 * @pI_ReadVal ：数组下标，扫描I_ReadVal数组 // 每次读写必须移动到下一位 // 初始化时需要赋值为0 ***********************************/ typedef struct CONTROL_IN // 控制参数值 { bool I_isRead[MAX_LEN * 3]; unsigned int pI_isRead; int I_ReadVal[MAX_LEN * 3]; unsigned int pI_ReadVal; }CtrlIn; void OutSavingCtrl(CtrlOut *O); void IsAutoSaving(CtrlOut *O); void InReadCtrl(CtrlIn *I, int paraNum); void Coding(unsigned char codeStr[], CtrlOut *O, CtrlIn *I); void InitIO(CtrlIn *I, CtrlOut *O); void MCUOutCtrl(int OutStr[], CtrlOut *O, int OutVal); void MCUInCtrl(int OutStr[], CtrlIn *I, int *InVal); void MCUCtrl(unsigned char instruction[], int OutStr[], CtrlOut *O, CtrlIn *I); #endif // APPPROGRAM_H_INCLUDED 写这段程序着实花了我挺大精力，重点是没有参照所以编程思路需要理清。程序还有很多可以改进的地方，具体我放在最后写，我用这段程序形成的代码写了一段程序：\n程序的注释挺多的，要知道具体工作原理请直接看代码。\n思路解析 就我写了这么多程序后，对于这部分的理解大概就是，我们其实在制作一个虚拟机解释器，类似于Java那种，当然这种程度相较于Java这类成熟的语言还远远不够，但是思路挺像，我在此归纳一下：\n首先我们将底层抽象化，让人可以使用自然语言去理解我们想要作什么，这就是我们第一行的作用。这个过程其实类似于制作一个GUI，然后用户可以通过GUI去理解每个函数的作用，当然如果用GUI来制作的话我们可以得到更好的反馈效果。\n然后第二步就是让使用者去编写程序了，因为每个人对编程的理解能力是不同的，我只能尽量从有普通理解能力的使用者出发，设计一些解释性语句，而使用者则根据这些语句输入相应的参数。因为用户其实就是简单地去运行程序，他并不知道也不需要知道底层的内容，他只想知道这个函数该怎么用就可以了。\n而函数的运行除了内部的具体执行，对于应用层编程者来说其实只是涉及到两个关键点：一是函数参数从何而来，二是返回结果向何而去。\n参数 参数的来历对于我们日常编程的人来说，主要分为两种：一是通过const修饰，使用宏定义修饰，或是直接以数字形式呈现的常量；另一个就是被赋值的变量。但是这里有一点需要注意，如果一个变量第一次被手动赋值初始化，然后它就直接传递给一个函数了，虽然从底层逻辑上来说它是一个变量，但是如果只是从实现的角度上来看，它其实和一个被宏定义的常量并没有太大的区别，我在程序里是直接将其认定为一个常量，这不严谨，但是很有效，不然还要另外写个赋值函数。\n所以这里我们就懂了，参数来自于变量和常量，而常量肯定是使用者手动输入的，不作阐释，而变量呢？\n返回值 因为我们清楚，如果我们将第一次被手动赋值初始化的变量当作了常量，那么显然第一次被自动赋值的变量当然就是我们所需要的变量了。而自动赋值，很明显就是被函数赋值。\n所以我们可以推论所有的变量都来自于函数返回值，但是函数返回值不一定非要保存起来作为一个变量，他们是一个包含关系。\n变量和常量 我们可以把常量和变量分为两个数组，然后我们每次只需要确定参数来自于那个常量或那个变量，而返回值是否保存而如果保存的话保存到哪即可。\n所以我们需要两个指针，一个指向变量，一个指向常量，它们会在输入参数和输出返回值时工作。\n输入判断 在不重构的情况下，一个函数如果在定义时有参数，这个参数就必须赋值。\n如果该参数为常量，它的值不会再改变，所以我们可以通过赋值将其永远存在常量数组中；\n如果该参数为变量，它的值可能会发生改变，而且它的值来自于返回值，再不运行之前我们无法得知它的具体值，所以我们记录的不是它的值而是它的地址。\n每个参数在每次操作中只可能是常量或是变量，所以我们可以通过一个数组记录每个参数对应的选择。\n如果选择变量，我们就需要一个指针数组记录其对应的变量数组中的位置，当然使用下标会更加方便。在demo中我使用的下标，但是没有另外建立指针数组，我在程序中有标注和注释，主要是偷一下懒，在不超过int最大值的情况下这么写是没有太大问题的，不过最好还是另外定义一个数组。\n如果选择常量，我们只需要将使用者输入值保存在常量数组中即可。\n这样我们的执行步骤就是：\ngraph LR isConst[判断来自常量还是变量]--常量--\u003eConstSaving[常量数组] isConst--变量--\u003epParaSaving[变量指针数组]--下标指针--\u003eParaSaving[存在于下位机的变量数组] 输出判断 在不使用指针传参的情况下，一个函数如果在定义时有返回值，这个返回值输出可以被保存为变量，也可以不使用。\n如果不保存这个变量，我们可以不对当前函数输出做任何操作。\n如果保存这个变量，那么它就两种情况，一是作为一个新变量进行保存，另一个就是覆盖保存，新变量可以自动生成，我们可以用动态数组控制覆盖和自动，但是为了简单，我在demo中将变量数组定义为两部分，一是已生成部分，另一块是不定部分，只要我要求用户覆盖保存只能在已生成部分内，就不会出现太大问题（当然事实上我没有加这个判断，因为这个demo的使用者只是我，而如果你们要使用，肯定也会看我这篇文章以及程序里的注释），如果可以做出GUI界面，其实我们大可让用户无法知道居然还有不定部分，如果能让所有已经保存的变量都有名词显示那是最好的情况。\n所以我们的执行步骤就是：\ngraph LR isSaving[判断是否存储该返回值]--存储--\u003eisAutoSaving[判断是否自动保存] isAutoSaving--自动--\u003eAutoSaving[自动开辟新的已生成部分] isAutoSaving--手动--\u003eHandSaving[手动输入已生成部分覆盖位置] isSaving--不存储--\u003ebreak[不执行任何操作] AutoSaving--\u003eSaving[记录对应下位机的变量数组下标] HandSaving--\u003eSaving报文 通过以上部分，我们基本上可以得到我们所需的报文了，在不限定格式的情况下我们可以将不同功能的指令形成数组，串入：\n一串函数指令数组 一串判断参数来自常量还是变量的数组 一串常量数组 一串记录输入对应变量下标的数组 一串判断是否存储返回值 一串记录输出对应变量下标的数组 注意，并不是我们在上位机执行的所有判断或操作都需要告知下位机，而下位机中的变量数组也并非来自于报文而是在执行中生成。\n我们在真正传入报文时需要注意校验值，因为可能报文量较大，所以建议每隔几十个数据就校验一次，如果校验失败则重新传输。另外需要给下位机以提醒，告知其此时传输的内容为何值，即报头和报尾，而且尽量让报头和报尾足够长，以防错误检测数据报。\n总结和反思 我们这些程序写出来后，实际上可以看作一个简单的新语言，它的底层是C，但是实际上被另外一种解释器层叠出单纯应用层的特性。我们可以对这种新语言进行包装，因为上位机并无语言限制，可以写成比较好看易交互的界面，然后处理成报文后发送给下位机即可。而下位机则作为一个解释器，对报文进行理解然后执行底层代码。\n在编程的过程中，需要注意上位机和下位机的指针这些是不能互通的，但是我们可以通过下标的方式代替指针，或是通过赋值方法让下位机自动记录指针，这需要非常注意对内存的把控。因为解释器全在下位机，所以上位机不需要太复杂的应用，甚至可以部署在网页上，而通过前端或是应用端丰富的GUI，这种编程方式可以让外行人也能实现编程功能。\n不过这目前只是一个demo方案，我本身也没有开发这种解释型语言的经验，这种编程方法的缺陷也很明显，即占内存、耗时较高，如果用于嵌入式可能不是很适合于告诉场合（当然我们也可以通过对底层函数的封装提高这个效率），但是至少它很有趣。\n我在demo中写了很多注释，但是c基础一般的真要理解起来可能还是需要花一点功夫，另外demo中仍有很多改进空间，至少指针传参、重构函数这类操作还没有实现，而且使用数组传递很占用空间，如果可以使用动态数组会比现在这样好得多。还有我的一些变量名称没有选择好，所以理解起来会比较复杂。\n才疏学浅，漏洞百出，如有意见或建议，欢迎留言评论，如果能点赞收藏关注那就更好不过了，嘿嘿。\n","date":"2020-11-24T22:58:10+08:00","image":"/posts/%E7%AE%97%E6%B3%95/%E8%87%AA%E5%88%B6%E8%84%9A%E6%9C%AC%E8%AF%AD%E8%A8%80/%E5%A6%82%E4%BD%95%E5%81%87%E8%A3%85%E7%94%A8c%E8%AF%AD%E8%A8%80%E4%B8%BA%E5%B5%8C%E5%85%A5%E5%BC%8F%E5%86%99%E4%B8%80%E4%B8%AA%E6%96%B0%E8%AF%AD%E8%A8%80/cover.jpg","permalink":"/posts/%E7%AE%97%E6%B3%95/%E8%87%AA%E5%88%B6%E8%84%9A%E6%9C%AC%E8%AF%AD%E8%A8%80/%E5%A6%82%E4%BD%95%E5%81%87%E8%A3%85%E7%94%A8c%E8%AF%AD%E8%A8%80%E4%B8%BA%E5%B5%8C%E5%85%A5%E5%BC%8F%E5%86%99%E4%B8%80%E4%B8%AA%E6%96%B0%E8%AF%AD%E8%A8%80/","title":"如何假装用C语言为嵌入式写一个新语言"},{"content":"使用过 Arduino 的朋友或多或少应该都见过 Arduino 那种直插的扩展板，不用排线不用焊接，只需排针排母上下对齐然后轻轻一推，就完成了模块化和集成化的完美结合。本文从自己制作的一个板子出发，基于 KiCAD 简单介绍一下扩展板的画法。\n设计思路 第一步：画好内部电路 第二步：画直插引脚符号 第三步：将直插引脚符号导入并连接 第四步：画直插引脚封装 第五步：修改路径 第六步：分配封装 第七步：画 PCB 第八步：导出生产文件并打板 设计思路 首先需要确定自己是要制作什么扩展板，这里我是为了自己的项目设计了一个 stm32f103c8 的板子，功能就是做成一个最小系统板，所有的接口都单独引出。\n第一步：画好内部电路 在Ultra找到需要的芯片的符号并下载，在导入后画出板子的内部电路。内部电路很简单，画出复位电路、供电电路、晶振时钟电路、冲击电流保护、开关显示电路。同时将 BOOT1 拉低，因为这块板子我不需要使用内置 SRAM 启动。\n画好成果如下：\n第二步：画直插引脚符号 因为要做成扩展板，那么很多时候直插引脚的结构往往就是相同的，如果每次使用的时候都要重新画一次直插引脚，那么很显然这会是很复杂麻烦的一件事，所以直接将其做成一个直插引脚符号会很省时省力。在我的设计里面，这个板子是一个六边形，然后每条边上有八个引脚，同时这八个引脚里面前两个始终为 GND 和 3V3，然后我画了如下的这样的符号，为了省时省力，在自认为不会犯错的情况下，我对每个引脚的设置都为 Passive。\n第三步：将直插引脚符号导入并连接 画好符号后，就只需要将所需引脚连接起来即可，而后如果遇到其它的扩展板，也只需要将对应的引脚相互连接即可，这里我下拉了 BOOT0，如果需要让它上拉，只需要在外部电路连接 3V3 即可。\n第四步：画直插引脚封装 和符号同理，这样设计之后就不需要每次都重新打孔，而且还要费心费力地去计算孔的位置，太麻烦，所以直接设计成封装。新建库、封装，通过计算找出所有孔的位置，然后挨个放孔、画边，最终结果如下，记住边框一定要用 Fab 层，这在后面的设计中很重要：\n第五步：修改路径 为了使用这个扩展板的时候所有工程都可以用这个库，可以把库文件夹放在当前工程的上一级路径，然后点击首选项-管理封装库，找到刚才新建的库，对其路径进行修改为$(KIPRJMOD)/../库文件夹名，例如我的路径即为：\n这里的/..可以理解为打开上一级目录，而$(KIPRJMOD)则表示当前工程路径，这样只要每次新建工程都放在和库同级路径下，即可保证该库的使用了。\n第六步：分配封装 回到原理图界面，在标注完成且电器规则检查无误后即可分配封装了。\n第七步：画 PCB 分配完封装就可以直接开始连线了，不过注意的是需要现在电路板设置界面关闭要求封装中定义外框，这里的外框其实指的是 Crt 层，这样的话我们使用 Fab 层作为外框就可以在不干扰 PCB 电气特性的情况下同时不受禁止外框重叠的管束，那么其它元件就可以放在我们的直插引脚封装里面了！剩下的就是排布和拉线即可。\n第八步：导出生产文件并打板 注意控制我们的 PCB 在 10cm*10cm 之间，这样我们就可以使用嘉立创的 5 元打样优惠了。导出生产文件然后打样的过程网上有很多，我在这里就不详述了。\n","date":"2020-11-20T16:24:28+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E7%A1%AC%E4%BB%B6%E8%AE%BE%E8%AE%A1/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8kicad%E7%94%BB%E6%89%A9%E5%B1%95%E6%9D%BF/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E7%A1%AC%E4%BB%B6%E8%AE%BE%E8%AE%A1/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8kicad%E7%94%BB%E6%89%A9%E5%B1%95%E6%9D%BF/","title":"如何使用KiCAD画扩展板"},{"content":"在学习 C 语言的数据结构时，我发现教科书上出现的一些代码实际上并不能很好地在 C 语言环境中运行，而需要改成 C++才可以，在网上搜了一下，这里记录 C 和 C++在函数传参上的区别\nC++的形参有三种写法：\nvoid example_CPP(\u0026amp;a, *b, c){ /*a为引用传参*/ /*b为指针传参*/ /*c为直接传参*/ } 三种写法各有用处，这里我们以实例进行研究——\n首先我们打开自己的 IDE，我用的 CodeBlocks，然后输入一下程序：\n#include \u0026lt;iostream\u0026gt; using namespace std; void example_CPP(int \u0026amp;a,int *b,int c); int main(){ /*定义三个变量并输出其值和地址*/ int x,y,z; x = y = z = 0; example_CPP(x, \u0026amp;y, z); cout\u0026lt;\u0026lt;\u0026#34;x的值:\u0026#34;\u0026lt;\u0026lt;x\u0026lt;\u0026lt;\u0026#34;\\nx的地址\u0026#34;\u0026lt;\u0026lt;\u0026amp;x\u0026lt;\u0026lt;\u0026#34;\\n\u0026#34;; cout\u0026lt;\u0026lt;\u0026#34;y的值:\u0026#34;\u0026lt;\u0026lt;y\u0026lt;\u0026lt;\u0026#34;\\ny的地址\u0026#34;\u0026lt;\u0026lt;\u0026amp;y\u0026lt;\u0026lt;\u0026#34;\\n\u0026#34;; cout\u0026lt;\u0026lt;\u0026#34;z的值:\u0026#34;\u0026lt;\u0026lt;z\u0026lt;\u0026lt;\u0026#34;\\nz的地址\u0026#34;\u0026lt;\u0026lt;\u0026amp;z\u0026lt;\u0026lt;\u0026#34;\\n\u0026#34;; } void example_CPP(int \u0026amp;a,int *b,int c){ /*将3个数分别加一*/ a += 1; *b += 1; c += 1; /*a为引用传参，传的是引用值*/ cout\u0026lt;\u0026lt;\u0026#34;a的值:\u0026#34;\u0026lt;\u0026lt;a\u0026lt;\u0026lt;\u0026#34;\\na的地址\u0026#34;\u0026lt;\u0026lt;\u0026amp;a\u0026lt;\u0026lt;\u0026#34;\\n\u0026#34;; /*b为指针传参，传的是地址*/ cout\u0026lt;\u0026lt;\u0026#34;b的值:\u0026#34;\u0026lt;\u0026lt;*b\u0026lt;\u0026lt;\u0026#34;\\nb的地址\u0026#34;\u0026lt;\u0026lt;b\u0026lt;\u0026lt;\u0026#34;\\n\u0026#34;; /*c为直接传参，传的是实际值*/ cout\u0026lt;\u0026lt;\u0026#34;c的值:\u0026#34;\u0026lt;\u0026lt;c\u0026lt;\u0026lt;\u0026#34;\\nc的地址\u0026#34;\u0026lt;\u0026lt;\u0026amp;c\u0026lt;\u0026lt;\u0026#34;\\n\u0026#34;; } 输出结果如下： 当我们使用直接传参时，实际过程可以理解为我们把原来 z 的值赋值给一个新的变量 c，在对 c 进行了一堆操作后释放 c 的空间，而 z 实际上并没有参与到函数中，而仅仅起到一个赋初值的作用。\n而当我们进行指针传递时，可以理解为我们把 y 的地址赋值给一个指针变量 b，而通过指针 b 对其指向的 y 的存储空间进行操作，我们没有直接对 y 进行操作，但是通过指针 b 我们可以间接修改 y 的值。\n至于我方在使用引用传参时，可以理解为直接传参和指针传参的结合版，我们申请了一个新的变量 a，类似于直接传参的变量 c，但是这个变量的地址被赋值为 x 的指针，从效果上来看就有点类似于把 x 临时当成全局变量传递进函数了。这种传参方式非常优秀，相比直接传参可以修改实参真实值，而相比指针传参则不用担心在调用时漏掉了取地址符。\n不过值得注意的是，这种传参方式并不能在 C 语言中使用，至少目前常用的 C99 标准和 C11 标准里我还没看见可以使用的。以下是我在 C 语言里面进行的测试：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; void example_CPP(int \u0026amp;a,int *b,int c); int main(){ /*定义三个变量并输出其值和地址*/ int x,y,z; x = y = z = 0; example_CPP(x, \u0026amp;y, z); } void example_CPP(int \u0026amp;a,int *b,int c){ /*a为引用传参，传的是引用值*/ /*b为指针传参，传的是地址*/ /*c为直接传参，传的是实际值*/ } 编译器报错：\n这两个错误分别报的函数声明和函数定义\n","date":"2020-11-08T17:10:52+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E5%92%8Cc++%E5%9C%A8%E5%8F%82%E6%95%B0%E8%B0%83%E7%94%A8%E4%B8%8A%E7%9A%84%E5%8C%BA%E5%88%AB/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/c_cpp/c%E5%92%8Cc++%E5%9C%A8%E5%8F%82%E6%95%B0%E8%B0%83%E7%94%A8%E4%B8%8A%E7%9A%84%E5%8C%BA%E5%88%AB/","title":"C和C++在参数调用上的区别"},{"content":"三极管（BJT）与场效应管（FET）的比较 因为学妹在学模电，突然问到我 MOS 与 MOSFET 的噪声谁更大，结果仔细一回想，似乎只是隐约记得 FET 比 BJT 抗噪声能力更强，但是原因却不清楚，毕竟模电还是学得太差了。网上找了好久资料，找到了一个贴吧的帖子，对 BJT 和 FET 的特性进行了比较，参考其内容在这里记录一下。\n三极管（BJT）与场效应管（FET）的比较 历史： 电极区别： 控制类型： 阻抗差别： 载流子： 稳定性（噪声）： 分类： 特性曲线： 放大能力： 灵活性： 偏置差别： 工作要求： 电容差别： 其他： 总结： 历史： BJT 和 FET 都是主要用与放大、开关电路中的常用电子元件。BJT 发明较早，依靠小结构高性能的优势淘汰了电子管，而后因为结构设计上的先天缺陷，FET 便应运而生。不过虽然 FET 发明出来后是为了克服 BJT 的缺陷，但是在实际运用过程中人们发现 BJT 和 FET 各有优劣，最终便都被保留了下来并在如今广泛使用。\n电极区别： BJT：基极（B），发射极（E），集电极（C）\nFET：栅极（G），源极（S），漏极（D）\n这里上下一一对应\n控制类型： BJT：电流控制——通过控制基极电流来控制集电极电流的变化 FET：电压控制——通过控制栅极电压来控制漏极电流变化 阻抗差别： BJT：输入阻抗较低（几百欧姆-几千欧姆），基极电流较大，输出电阻较高，对前级电路影响较大，必须阻抗匹配才能工作 FET：输入阻抗极高（兆欧以上），MOS 管更高，栅极几乎没有电流，对前级电路影响较小，输出电阻同样也很高 载流子： BJT：两种载流子——少子和多子，属于双极性器件 FET：一种载流子——多子，属于单极性期间 稳定性（噪声）： BJT：噪声更大——因为少子参与了导电，而少子容易受到温度的影响，热稳定性较差 FET：噪声更小——因为只有多子导电，热稳定性更好，噪声小 分类： BJT：PNP 和 NPN FET： 按沟道：N 型和 P 型 按原理：JFET 和 MOSFET（增强型、耗尽型） 特性曲线： BJT：截止区、放大区、饱和区、击穿区——转移特性按指数规律变化\nFET：截至区、放大区、可变电阻区、击穿区——转移特性按平方规律变化（非线性失真更大）\n两者可以看作对应关系\n放大能力： BJT：参数——电流放大倍数 β\nFET：参数——跨导 gm\n两者相比 BJT 电压放大倍数更大\n灵活性： BJT：发射极和集电极不能互换，否则 β 过低不能正常工作 FET：在一些特定条件下，源极和漏极可以互换，耗尽型 MOSFET 的栅极电压可正可负，灵活性更高 偏置差别： BJT：需要合适的偏置电流才可以正常工作 FET：需要合适的偏置电压才可以正常工作 工作要求： BJT：焊接过程中温度不能过高，静电无影响 FET：必须对栅极和源极放电，否则感应静电也容易击穿，在存储、运输过程中必须将栅极、源极短路 电容差别： BJT：极间电容较大，耦合电容较大 FET：极间电容较小，耦合电容较小（输入电阻较高） 其他： BJT：功耗更高，但更便宜 FET：功耗低，但是更贵 FET 总结： 在只允许从信号源取较少（高输出阻抗）电流的情况下，选用 FET，达到阻抗匹配目的，适用于低噪高输入电阻的前置电路；\n在信号电压较低，允许取用电流的情况下，选用 BJT。\n作为开关管，FET 的效率较高，多用在大电流、高速开关电源上；\n在环境温度变化较大的场合应使用 FET。\n","date":"2020-11-08T16:09:03+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E7%A1%AC%E4%BB%B6%E8%AE%BE%E8%AE%A1/%E4%B8%89%E6%9E%81%E7%AE%A1bjt%E4%B8%8E%E5%9C%BA%E6%95%88%E5%BA%94%E7%AE%A1fet%E7%9A%84%E6%AF%94%E8%BE%83/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E7%A1%AC%E4%BB%B6%E8%AE%BE%E8%AE%A1/%E4%B8%89%E6%9E%81%E7%AE%A1bjt%E4%B8%8E%E5%9C%BA%E6%95%88%E5%BA%94%E7%AE%A1fet%E7%9A%84%E6%AF%94%E8%BE%83/","title":"三极管（BJT）与场效应管（FET）的比较"},{"content":"MSP432 在 Energia 上的使用（下） 其实我自己都没想到我居然会来填坑，不过说实话这好像也算不上填坑，毕竟之前在MSP430 在 Energia 上的使用（上）这个帖子里讲的是 MSP430，而且说的准备在 VSCode 里装 Energia 其实到现在还没弄，不过这里还是想介绍一下 Energia 的另一项功能，一个专属于 MSP432 的功能。\nMSP432 在 Energia 上的使用（下） 发现 使用方法 TASK1 TASK2 TASK3 发现 事实证明多看官网还是有好处的，之前我在使用 Energia 对 MSP430 进行编程时一直非常疑惑，为什么 TI 公司不直接使用 Arduino 已有的 IDE 而非要做个自己的 Energia 导致不为人知非常冷门，但是官网上的介绍回答了我这一点。打开Energia 官网，进入 Guide 界面，翻阅一下可以看见一个名词——MultiTasking，即多任务。众所周知如果单片机想要实现多任务并行，一般需要自己搭建 RTOS 或者更复杂一点进行专门的时分复用设计，这些方法往往需要较丰富的知识才能实现。而 Energia 就可以非常简单地（其实如果进行一些复杂操作也是挺复杂的）实现多任务操作。而官网在这里其实也给出了关于 Multitasking 的介绍，值得注意的是目前该功能只能作用与 MSP432，MSP432E，CC3320 和 CC1310几种芯片，这也是为什么我把这一章名字改成了 MSP432 而不是 MSP430。（说起来每次打开 Energia 它的启动显示里面也写到了 Energia MT，实际上就是说的这个功能，不过我一直没咋注意）\n使用方法 其实官网原文就有一个大概介绍了，我这里就重复一下。关于 MultiTasking（以下简称 MT）的使用其实非常简单（如果不想做太复杂的话）。\n每次新建工程后，我们会有一个初始界面，上面写的 void setup()和 void loop()（其实就是正常 Energia 或 Arduino IDE 在新建后的工程界面），我们可以把这个界面称之为工程主界面。如果只使用工程主界面写一个程序，那么它只是普通的单线程程序，但是如果你使用的是 MSP432 这类单片机，你可以点击 IDE 右上角的倒三角符号，新建一个 task，随便输入一个名字作为新任务（Task）的名字（注意不要输入后缀，IDE 会自动将其生成为 ino）然后你可以在里面写两个函数，他们便可以当成一个新的 setup 和 loop 函数了，此时如果烧录进芯片后，芯片便会同时运行两个任务（实际上并不是同时运行，只是通过快速转换两个任务的运行最后看上去是同时运行）。\n示例程序有很多，这里介绍其中最简单的 MultiBlink 的程序。\nTASK1 #define LED BLUE_LED void setup() { // initialize the digital pin as an output. pinMode(LED, OUTPUT); } void loop() { digitalWrite(LED, HIGH); //turn the LED on by making the voltage HIGH delay(100); // wait for a second digitalWrite(LED, LOW); //turn the LED off by making the voltage LOW delay(100); // wait for a second } TASK2 #define LED GREEN_LED void setupGreenLed() { // initialize the digital pin as an output. pinMode(LED, OUTPUT); } void loopGreenLed() { digitalWrite(LED, HIGH); // turn the LED on delay(500); // wait for half a second digitalWrite(LED, LOW); // turn the LED off delay(500); // wait for half a second } TASK3 #define LED RED_LED void setupRedLed() { // initialize the digital pin as an output. pinMode(LED, OUTPUT); } void loopRedLed() { digitalWrite(LED, HIGH); // turn the LED on delay(1000); // wait for 1/10 second digitalWrite(LED, LOW); // turn the LED off delay(1000); // wait for 1/10 second } 该程序可以让 LaunchPad 上的三色 LED 以多种不同颜色的组合进行闪烁，有一说一还挺炫酷。\n","date":"2020-11-07T19:46:09+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/msp432%E5%9C%A8energia%E4%B8%8A%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8B/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/msp432%E5%9C%A8energia%E4%B8%8A%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8B/","title":"MSP432在Energia上的使用（下）"},{"content":"使用 CCS 导出 TXT 用于 BSL 烧录 ","date":"2020-08-28T16:34:57+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%B7%A5%E5%85%B7%E9%93%BE/%E4%BD%BF%E7%94%A8ccs%E5%AF%BC%E5%87%BAtxt%E7%94%A8%E4%BA%8Ebsl%E7%83%A7%E5%BD%95/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%B7%A5%E5%85%B7%E9%93%BE/%E4%BD%BF%E7%94%A8ccs%E5%AF%BC%E5%87%BAtxt%E7%94%A8%E4%BA%8Ebsl%E7%83%A7%E5%BD%95/","title":"使用 CCS 导出 TXT 用于 BSL 烧录"},{"content":"MSP430 在 Energia 上的使用（上） 说实话我也不确定会不会有后面的笔记，但是这次实验的确相当于没有做完。\nMSP430 在 Energia 上的使用（上） 准备工作 问题意义 实验一：测试传输接口 实验现象： 实验结论： 实验二：移植到 ArduinoIDE 备注 准备工作 终于狠下心花了百元大洋买了块 MSP430F5529LP 板子，准备研究一下板载仿真器在 Energia 中是如何进行烧录的。\n通过 TI 公司的官方文件《MSP430F5529 LaunchPad Development Kit……》，我们其实可以得到 LaunchPad 的原理图，因为整个原理图用了四页，这里就不详细展示了，有需要可以在 TI 官网找。我只截取板载仿真器和 MSP430F5529 芯片的接口部分的图\n其中板载仿真器被 TI 公司命名为 ezFET，应该是简版仿真器的意思，除去电源线部分，我们可以看到信息传输分为两部分——SBW 和 UART。其中 SBW 相当于两线 JTAG，可以当做仿真器接口，而 UART 则是使用的 MSP430 的 BSL 接口，其只能作为烧录口。\n那么看到这其实问题就很简单了，我们需要确定 Energia 在烧录的时候具体是用的哪个接口。\n问题意义 一般情况下，我们在使用 LaunchPad 时，是不需要管这个仿真器是如何工作的，但是如果当我们手里只有一块没有板载仿真器的开发板或者甚至只有一块芯片的时候，我们又该如何对其使用 Energia 呢，是应该用 JTAG 连接吗，还是使用 SBW，亦或是使用 BSL？我们是否能将 EnergiaIDE 的移植到 ArduinoIDE 上呢（因为 Energia 事实上能够使用的资源远远不及 Arduino，而且无法用 VSCode 这个大杀器）？\n实验一：测试传输接口 这个工作其实很简单，因为板子在设计的时候就是通过跳线进行连接的，所以我们只需要调整跳线位置即可进行测试。\n实验现象： 在安装好 Energia 以及板子驱动等之后，连接电脑 USB 与 ezFET 的 MicroUSB 接口，选择好端口和板子型号，选择示例程序 blink，直接点击上传程序。（上传成功，红灯闪烁） 取下 RX 和 TX 跳线，剩下 SBWTCK 和 SBWTDIO，然后再次上传。（上传成功，红灯闪烁） 取下 SBWTCK 和 SBWTDIO，剩下 RX 和 TX 跳线，然后再次上传。（上传失败，显示未检测到链接报错） 实验结论： 在 Energia 中，默认通过板载仿真器 ezFET 得 SBW 接口对 MSP430 芯片进行烧录。\n实验二：移植到 ArduinoIDE ArduinoIDE 本身自带了非常多的库，但是因为兼容性问题，并不支持直接移植到 Energia 上，所以 Energia 上目前可以直接使用的库可谓是非常少。而且无论是 ArduinoIDE 还是 EnergiaIDE 的代码辅助功能都非常垃圾，而如果能使用 VSCode 的 Arduino 插件就可以大大减轻这一问题。\n移植方法很简单，打开 Energia 的目录，找到 hardware，把其中 Energia 文件夹复制到 ArduinoIDE 目录的 hardware 文件夹里。再把 Energia 的 tools 里的东西放在 ArduinoIDE 的 hardware-tools 里即可。这里需要说明的是，在复制完后可以点开 ArduinoIDE，简单做个 demo 或者直接用 energia 的 blink 作为测试，如果出现\nexec: \u0026#34;/bin/msp430-g++\u0026#34;: file does not exist 为开发板 MSP-EXP430F5529LP 编译时出错。 需要修改 hardware-energia-msp430 文件夹里的 Platform.txt 文件，这个文件定义了 CPU 体系结构等（包括编译器、生成过程参数、用于上载的工具等），可能和 ArduinoIDE 无法直接兼容。\n具体修改规范可以参考 Arduino 官方的这个网址（需要科学上网）：https://arduino.github.io/arduino-cli/platform-specification/\n如果暂时不想看这么麻烦的东西（像我一样），可以参考这个帖子：[MSP]将 MSP430 纳入 ARDUINO IDE: 让 arduino 支持 MSP430F5438A\n不过他里面给出的 Platform.txt 有点古老，ArduinoIDE 可能会给出警告，不过警告一般也可以不听，所以凑合着用吧。同时按照他的方法，可能无法完成直接烧录，因为我试了一下报错了。\n移植并不算很成功，然后在 VSCode 里试了一下，一些定义会有点问题，暂时不确定是什么原因……说实话这个实验有点失败，还是能力欠缺了。\n另外我也看了看 PlatformIO，里面对 MSP430 的支持同样很差，果然 MSP430 这种老古董还是有点过气了吗……\n关于移植的方法我后面会再详细研究的（大概），如果研究好了再写这一篇的下章吧（意思是可 bi 能 si 会 qiang 没 po 有 zheng）。\n备注 关于仿真器的问题，如果买到了没有仿真器的板子或者自己做板子，而手里又没有仿真器，除了 BSL 烧录的方法以外，也可以通过用 LaunchPad 板子直接把跳线那里外接出去，也可以当仿真器用。另外，ezFET 是一个开源硬件，详情可以参考官方文件，当然网上也有一些帖子讲了这个玩意的做法，同样可以参考。\n","date":"2020-08-26T17:32:10+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/msp430%E5%9C%A8energia%E4%B8%8A%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8A/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/arduino/msp430%E5%9C%A8energia%E4%B8%8A%E7%9A%84%E4%BD%BF%E7%94%A8%E4%B8%8A/","title":"MSP430在Energia上的使用（上）"},{"content":"3D 打印机打印中途停止且显示挤出头温度过低 3D 打印机打印中途停止且显示挤出头温度过低 打印机型号 问题 解决方法 注意 打印机型号 Anycubic chiron3D\n问题 PLA 在 200℃ 打印过程中突然停止，挤出头在屏幕上显示温度降到约 120 度左右，喷头处于打印件之间出现明显的结节，退回到主菜单显示挤出头温度过低。同时打印件部分平面有明显的缺陷空隙，在点击结束打印后再次进入打印界面无模型文件。\n解决方法 因为故障后无法读取存储卡文件，怀疑是固件问题，通过 USB 接口将电脑连接打印机，使用切片软件（我用的 Cura4.6），选择打印机-\u0026gt;上传固件，同时将之前的 gcode 文件重新生成（不确定这一步是否必须）。\n注意 以上仅为我个人在此次遇到这种问题时的解决方法，并不一定适用于所有机器以及所有情况，其他情况还有：\n使用电脑直接联机打印，可能中间在数据传输过程中出现问题。 储存卡中文件异常，可能是切片软件使用了某种不应该使用的插件，或是在导出或生成过程中出现错误，或是存储卡热插拔导致存储失败。 供电电压不稳定，确定好供电电压是否正常，且确定是否和打印机工作电压相匹配。 如在使用我的解决办法后没有解决且以上可能都排除后，请确定存储卡、挤出头、挤出机等设备是否出现问题，若是非 DIY 产品最好找客服。 ","date":"2020-08-24T23:49:15+08:00","image":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/3d%E6%89%93%E5%8D%B0%E6%9C%BA%E6%89%93%E5%8D%B0%E4%B8%AD%E9%80%94%E5%81%9C%E6%AD%A2%E4%B8%94%E6%98%BE%E7%A4%BA%E6%8C%A4%E5%87%BA%E5%A4%B4%E6%B8%A9%E5%BA%A6%E8%BF%87%E4%BD%8E/cover.jpg","permalink":"/posts/%E5%B7%A5%E5%85%B7%E4%B8%8E%E7%8E%AF%E5%A2%83/3d%E6%89%93%E5%8D%B0%E6%9C%BA%E6%89%93%E5%8D%B0%E4%B8%AD%E9%80%94%E5%81%9C%E6%AD%A2%E4%B8%94%E6%98%BE%E7%A4%BA%E6%8C%A4%E5%87%BA%E5%A4%B4%E6%B8%A9%E5%BA%A6%E8%BF%87%E4%BD%8E/","title":"3D打印机打印中途停止且显示挤出头温度过低"},{"content":"Arduino 通过 USB 转 TTL 无 BootLoader（引导程序）烧录程序的两种办法 Arduino 通过 USB 转 TTL 无 BootLoader（引导程序）烧录程序的两种办法 注意 于 BootLoader 的废话 通过 ArduinoIDE 的 Serial 模式烧录 通过二进制文件烧录 总结 注意 （这个实验室基于 stm32duino 的，avr 单片机并不直接通用，不过如果你准备尝试使用串口来给 avr 单片机（就是 Arduino 官方出的几款 Arduino）烧录 Arduino 的 BootLoader，建议直接放弃因为 avr 单片机并不支持直接使用串口上传程序，而且 Arduino 的 BootLoader 好像目的也就是让 Arduino 能够直接串口烧录程序。\n也就是说你不能在没有 BootLoader 的情况下烧录 avr-Arduino 的 BootLoader，而当你可以用串口给 avr-Arduino 烧录 BootLoader 时就说明你已经有 BootLoader 了，可以不用再烧录一次了）\n于 BootLoader 的废话 因为准备参加电赛，想着如果比赛可以用 Arduino 或许会方便很多，所以准备研究一下 Arduino 的底层程序，学习一下如何把随便一块芯片都能做成 Arduino 来开发。不过这篇帖子和此并没有太大关系，只是属于机缘巧合做的一个小实验而已，对于已知 BootLoader 的原理或者想自己写 BootLoader 的朋友并无太大帮助。\n最开始初学的时候，买了一块 nano 板，结果在我准备做点骚操作的时候（具体忘了做了啥了，好像是写错了个指针？），它被我搞废了，烧录不了程序。然后各种查资料，怀疑是 BootLoader 的问题，需要重新烧 BootLoader。（事实上现在我也不知道具体是不是真的，因为拖延症导致我很久没去碰这块板子，现在不知道扔哪去了。而且当时没有记录的习惯，这个世界又多了个未解之谜）\n当时我并不清除 BootLoader 是个什么东西，毕竟年轻的我看见 hardware 文件夹里那一堆文件就头（tuo）疼（yan），但是总体印象大概就是 Arduino 的便捷和这玩意有很大关系。然后在我脑海中 Arduino 不能没有 BootLoader 这个想法就形成了，于是这次准备把 MSP430 做成 Arduino 的时候我就想到了是不是要烧 BootLoader？但是打开官方给出的 energia 软件，在其目录下并未发现 BootLoader 的文件，而且软件也没有给出烧录引导程序这一选项，所以我对我的认知产生了一丝丝怀疑。 于是，我选择再搜索一下 BootLoader 到底是什么，百度的解释是可以把它当做一个启动引导程序，但是感觉这种解释有点抽象，然后我找到另外一篇文章是说把它当做一个系统，我们的程序就是运行在这个系统的软件，这种说法感觉有一定道理，但是感觉又不是那么对，因为平常如果我们没有 BootLoader，我们也可以对普通单片机烧录程序，但是如果软件没了系统，是无法直接在电脑裸机上运行的。但是这个文章还是给了我一定的思路，所以我搜索了一下“如何使用串口给 avr 烧录程序”，然后我找到了这个。然后结合我目前掌握的知识和已知条件：\n每次 Arduino 在上传程序时都会进行一次复位，我怀疑是为了让 Arduino 进入 BootLoader 模式 没有 BootLoader 的 avr 单片机不能直接在 ArduinoIDE 上烧录程序，在如何使用串口给 avr 烧录程序那篇文章里，提到了需要给 avr 烧录 BootLoader 后才可以通过串口给 avr 直接烧录程序 Arduino UNO 的原理图中，官方最原版使用了一块 atmega16u2，而市面上常见的 UNO R3 都是使用的一块 ch340（USB 转串口芯片）来代替，说明 BootLoader 并没有直接让 avr 芯片拥有 USB 功能 Arduino Leonardo 的原理图就没有使用 ch340 或是其他芯片，而是直接用 USB 数据口连接 USB 接口，而 Leonardo 和 UNO 这些的区别在于其芯片 atmega32u4 可以作为 USB 设备来识别。 所以我姑且对 avr-Arduino 的 BootLoader 进行了这样的推测性理解：Arduino 的 BootLoader 可以当做一段启动程序，它的作用是让 Arduino 可以拥有与电脑直接（如 Leonardo）（或间接（如 UNO）），然后烧录时会把程序发送给 BootLoader 处理，然后 BootLoader 将其放到指定的 ROM 地址作为起始地址。在烧录结束后，芯片将从 BootLoader 部分跳到程序部分。而普通使用时，因为没有复位进入烧录这一行为，BootLoader 会被直接运行到底然后跳到真正程序起始位。如果不烧录 BootLoader，可以节省出 BootLoader 的内存空间，但是需要比较麻烦的接线方式（如 JTAG，ISP 之类）\n当然以上为我的个人推理猜测，目前太忙（tuo）还没有专门系统地学习这一块知识（内容感觉太杂不确定学习路线，有点东学点西学点的样子，如果有大佬能够指导一下方向就太好了），如果有错误非常欢迎指正。\n然后以下才进入正文……\n通过 ArduinoIDE 的 Serial 模式烧录 因为没有找到 MSP430 的引导程序，而 avr 的引导程序又需要烧熔丝，和我的目的（研究 msp430 的 Arduino）有比较大的差别，所以我选择了 stm32 来做测试。\n用的是 stm32f103c8t6 的小蓝板（blue pill），大概步骤为：\nArduinoIDE 安装开发板：http://dan.drown.org/stm32duino/package_STM32duino_index.json stm32f103c8t6 调整好 boot 引脚，连接 usb 转 ttl，然后连接电脑\n随便写个程序，注意引脚名称修改，我这里使用了 blink 例程，把引脚改为了 PC13\nvoid setup() { pinMode(PC13, OUTPUT); } void loop() { digitalWrite(PC13, HIGH); // turn the LED on (HIGH is the voltage level) delay(1000); // wait for a second digitalWrite(PC13, LOW); // turn the LED off by making the voltage LOW delay(1000); // wait for a second } 选择好板子、端口，然后选择烧录模式为 serial 点击上传即可 详细步骤请参考帖子：http://www.elecfans.com/d/1002467.html\n通过二进制文件烧录 这个方法需要下载软件，软件使用时只需要一路点 next 即可，不懂的地方直接用翻译软件翻译一下就懂了，stm32 的二进制烧录方式具体步骤可以参考这个帖子：https://www.cnblogs.com/wangguchao/p/7308657.html\n而 Arduino 的二进制文件可以通过 ArduinoIDE 的项目-导出二进制文件获取，会导出到工作文件夹下。\n总结 这个帖子其实就是做个思路引导，同时也是自己的学习笔记，如果需要烧录 stm32 的 Arduino 程序，在暂时不知道 BootLoader 原理或怎么写的情况下，可以用这种方式凑合着下载，而同理，avr 可以用 isp 或 jtag，msp430 用 jtag 或 sbw 或 bsl 来下载。不过最好再深入学习一下吧……\n以上内容如有错误或遗漏，欢迎指正！\n","date":"2020-08-09T19:51:21+08:00","image":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%B7%A5%E5%85%B7%E9%93%BE/arduino%E9%80%9A%E8%BF%87usb%E8%BD%ACttl%E6%97%A0bootloader%E5%BC%95%E5%AF%BC%E7%A8%8B%E5%BA%8F%E7%83%A7%E5%BD%95%E7%A8%8B%E5%BA%8F%E7%9A%84%E4%B8%A4%E7%A7%8D%E5%8A%9E%E6%B3%95/cover.jpg","permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F/%E5%B7%A5%E5%85%B7%E9%93%BE/arduino%E9%80%9A%E8%BF%87usb%E8%BD%ACttl%E6%97%A0bootloader%E5%BC%95%E5%AF%BC%E7%A8%8B%E5%BA%8F%E7%83%A7%E5%BD%95%E7%A8%8B%E5%BA%8F%E7%9A%84%E4%B8%A4%E7%A7%8D%E5%8A%9E%E6%B3%95/","title":"Arduino通过USB转TTL无BootLoader（引导程序）烧录程序的两种办法"}]