<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>YuMu Blog</title><description>博客</description><link>https://yuumu.asia/</link><language>zh_CN</language><item><title>一些做rag时想到的东西的整理</title><link>https://yuumu.asia/posts/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%9B%B8%E5%85%B3/%E4%B8%80%E4%BA%9B%E5%81%9Arag%E6%97%B6%E6%83%B3%E5%88%B0%E7%9A%84%E4%B8%9C%E8%A5%BF%E7%9A%84%E6%95%B4%E7%90%86/</link><guid isPermaLink="true">https://yuumu.asia/posts/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%9B%B8%E5%85%B3/%E4%B8%80%E4%BA%9B%E5%81%9Arag%E6%97%B6%E6%83%B3%E5%88%B0%E7%9A%84%E4%B8%9C%E8%A5%BF%E7%9A%84%E6%95%B4%E7%90%86/</guid><description>怎么根据实际的场景去做选择</description><pubDate>Tue, 02 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;事情的起因&lt;/h2&gt;
&lt;p&gt;很多对于RAG的考察都喜欢问Agent的长期记忆如何持久化。向量数据库的选择标准是什么。我在此之前其实并未做过什么细致的考察。也没特意研究过选型。&lt;/p&gt;
&lt;h2&gt;现有认知&lt;/h2&gt;
&lt;p&gt;Agent的长期记忆持久化主要通过向量数据库存储embedding实现，具体流程是对话历史，用户偏好只是片段经过embedding转换为向量，存入向量数据库，后续通过语义检索召回相关记忆。这个过程的核心在于解决一个关键问题：当用户有成千上万条历史记录时，怎么快速找到语义相关的那几条？传统数据库擅长精确匹配，但 Agent 需要语义理解。大部分人在和AI对话其实没有特别注重如何投放提示词，而是很随意的提问，比如用户问 “上次那个便宜的推荐”，系统要能关联到历史对话里的价格敏感信息，这就需要向量相似度检索而不是 SQL 的模糊查询。
选择向量数据库时需要关注几个核心维度。性能层面看查询延迟和吞吐量，生产环境通常要求毫秒级响应，像Milvus在百万级数据下能保持较好性能。索引算法很关键，HNSW 适合高稠密向量的快速检索，IVF 系列适合大规模场景。（具体的算法细节想了解可以去wiki百科查一下）。过滤能力决定能否结合元数据筛选，比如按时间范围或用户 ID 过滤记忆，这在多租户场景必不可少。还要考虑混合检索支持，很多场景需要向量检索和关键词检索结合，像 Weaviate 原生支持这种能力。数据规模也影响选型，小规模可用 Chroma 这类嵌入式方案，企业级应用更适合自建 Milvus 集群这些。
至于生态适配。langchain框架是由非常成熟的集成，而我最近在关注的eino里面也支持了一整套如embeding,retreiver组件。挺好的&lt;/p&gt;
&lt;h2&gt;然后&lt;/h2&gt;
&lt;p&gt;我们要更加系统的分析记忆分层架构与持久化链路的话。首先需要知道：要分层管理不同时效的信息。短期记忆就是当前对话的上下文，通常存在内存或 Redis 里，会话结束就清理掉，这部分主要服务于单次对话的连贯性。长期记忆是需要跨会话保留的信息，比如用户的消费偏好、历史咨询问题、积累的知识片段，这部分才需要持久化到向量数据库。有些复杂场景还会有情景记忆，记录特定任务的执行过程，方便后续复盘或继续执行未完成的任务。使用向量数据库是因为传统 SQL 根本匹配不上文字不同语义相近的两句话，但它们语义是相关的。向量化就是把文本映射到高维空间，语义相近的句子在空间中距离更近，这样就能通过计算向量相似度（通常用余弦距离或欧氏距离）找到相关记忆。具体实现上，会通过 OpenAI 的 text-embedding 模型或者开源的 BERT 模型把文本转成 768 维或 1536 维的浮点数向量。&lt;/p&gt;
&lt;h4&gt;把他总结一下就是&lt;/h4&gt;
&lt;p&gt;本预处理，可能需要去除无意义的语气词、标准化格式，这步看似简单但影响后续检索质量。然后调用Embedding 模型转换成向量，这一步要注意选择合适的模型，电商客服场景可能需要对领域术语敏感的模型，通用对话可能直接用 OpenAI 的接口就够了。接下来是向量入库，这里不只是存向量本身，还要把元数据一起存储。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MemoryPersistenceService 记忆持久化服务
type MemoryPersistenceService struct {
	embeddingClient EmbeddingClient
	vectorStore     VectorStore
}
//NweMemoryPersistenceService 创建服务实例
func NewMemoryPersistenceService(embeddingClient EmbeddingClient, vectorStore VectorStore) *MemoryPersistenceService {
	return &amp;amp;MemoryPersistenceService{
		embeddingClient: embeddingClient,
		vectorStore:     vectorStore,
	}
}
// SaveMemory 保存记忆到向量数据库
func (s *MemoryPersistenceService) SaveMemory(ctx context.Context, userId, conversation string) error {
	// 生成向量表示
	embedding, err := s.embeddingClient.Embed(ctx, conversation)
	if err != nil {
		return err
	}

	// 构造元数据
	metadata := map[string]interface{}{
		&quot;user_id&quot;:    userId,
		&quot;timestamp&quot;:  time.Now().UnixMilli(),
		&quot;content&quot;:    conversation,
		&quot;session_id&quot;: getCurrentSessionId(),
	}

	// 存入向量数据库
	return s.vectorStore.Insert(ctx, embedding, metadata)
}

// 依赖的接口定义
type EmbeddingClient interface {
	Embed(ctx context.Context, text string) ([]float32, error)
}

type VectorStore interface {
	Insert(ctx context.Context, embedding []float32, metadata map[string]interface{}) error
}

// getCurrentSessionId 获取当前会话ID（示例实现，实际根据业务逻辑）
func getCurrentSessionId() string {
	// 这里替换为实际的会话ID获取逻辑
	return &quot;session_123&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的元数据界定了后续作甚么样的过滤和排序，检索是流程反过来，把查询到的文本想来概念化，然后再向量数据库做相似度检索。
生产环境还要考虑缓存热点记忆、异步写入、失败重试这些工程问题。理论和实践之间有很大的差距，比如异步写入虽然能提升吞吐量，但要处理好主库写入成功但向量库写入失败的一致性问题。
向量数据库和传统数据库的本质差异在于索引结构的设计目标完全不同。传统数据库的 B + 树索引是为范围查询优化的，比如查年龄在 20 到 30 之间的用户，时间复杂度是 O (log n)。但向量检索要的是 “找最相似的 K 个向量”，这是完全不同的计算模式。如果用 MySQL 暴力计算余弦相似度，百万级数据每次查询要做上百万次浮点运算，延迟会达到秒级甚至更高。
向量数据库专门为高维向量的相似度计算设计了索引结构。比如HNSW 算构建的图索引，把向量空间组织成多层图结构，检索时从顶层快速定位区域，再逐层下钻找到最近邻，时间复杂度是对数级别的。另一种常见的是IVF 索引，先用聚类算法把向量空间划分成若干个区域，检索时只在最相关的几个区域里搜索，大幅减少计算量。&lt;/p&gt;
&lt;h2&gt;混合检索&lt;/h2&gt;
&lt;p&gt;刚刚也提到了元数据，那为什么要用到元数据和这里所提到的混合检索有关，很多业务股则要和向量检索结合。实现思路是先用向量粗排序召回更多候选，再通过元数据过滤重排序得到最终结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package service

import (
	&quot;context&quot;
	&quot;sort&quot;
	&quot;time&quot;
)

// HybridSearchService 混合检索服务
type HybridSearchService struct {
	vectorDB VectorDatabase
}

// NewHybridSearchService 创建服务实例
func NewHybridSearchService(vectorDB VectorDatabase) *HybridSearchService {
	return &amp;amp;HybridSearchService{vectorDB: vectorDB}
}

// SearchMemories 执行混合检索
func (s *HybridSearchService) SearchMemories(ctx context.Context, req *SearchRequest) ([]*Memory, error) {
	// 第一步：向量粗排，召回3倍候选
	candidates, err := s.vectorDB.SimilaritySearch(ctx, req.QueryEmbedding, req.TopK*3)
	if err != nil {
		return nil, err
	}

	// 第二步：元数据过滤
	filtered := make([]*VectorMatch, 0, len(candidates))
	for _, m := range candidates {
		if isWithinTimeRange(m, req.TimeRange) &amp;amp;&amp;amp; matchesUserSegment(m, req.UserID) {
			filtered = append(filtered, m)
		}
	}

	// 第三步：混合打分重排
	scoredMemories := make([]*ScoredMemory, 0, len(filtered))
	for _, m := range filtered {
		score := s.calculateHybridScore(m, req)
		scoredMemories = append(scoredMemories, &amp;amp;ScoredMemory{
			Memory: m.Memory,
			Score:  score,
		})
	}

	// 按分数降序排序
	sort.Slice(scoredMemories, func(i, j int) bool {
		return scoredMemories[i].Score &amp;gt; scoredMemories[j].Score
	})

	// 取TopK结果
	resultCount := req.TopK
	if len(scoredMemories) &amp;lt; resultCount {
		resultCount = len(scoredMemories)
	}
	results := make([]*Memory, 0, resultCount)
	for i := 0; i &amp;lt; resultCount; i++ {
		results = append(results, scoredMemories[i].Memory)
	}

	return results, nil
}

// calculateHybridScore 计算混合检索得分
func (s *HybridSearchService) calculateHybridScore(match *VectorMatch, req *SearchRequest) float64 {
	vectorScore := match.Similarity
	recencyScore := s.calculateRecency(match.Timestamp)
	importanceScore, ok := match.Metadata[&quot;importance&quot;].(float64)
	if !ok {
		importanceScore = 0.5
	}

	// 权重可根据业务调整
	return 0.6*vectorScore + 0.3*recencyScore + 0.1*importanceScore
}

// calculateRecency 计算时间衰减得分
func (s *HybridSearchService) calculateRecency(timestamp int64) float64 {
	ageInDays := (time.Now().UnixMilli() - timestamp) / (1000 * 60 * 60 * 24)
	return float64(math.Exp(-float64(ageInDays) / 30.0)) // 30天衰减模型
}

// ---------------- 依赖的类型定义 ----------------
type SearchRequest struct {
	QueryEmbedding []float32
	TopK           int
	TimeRange      [2]int64 // [start, end] 时间戳范围
	UserID         string
}

type VectorMatch struct {
	Memory     *Memory
	Similarity float64
	Timestamp  int64
	Metadata   map[string]interface{}
}

type ScoredMemory struct {
	Memory *Memory
	Score  float64
}

type Memory struct {
	// 根据实际业务定义Memory的字段
}

type VectorDatabase interface {
	SimilaritySearch(ctx context.Context, embedding []float32, topK int) ([]*VectorMatch, error)
}

// 辅助过滤函数（示例实现）
func isWithinTimeRange(m *VectorMatch, timeRange [2]int64) bool {
	return m.Timestamp &amp;gt;= timeRange[0] &amp;amp;&amp;amp; m.Timestamp &amp;lt;= timeRange[1]
}

func matchesUserSegment(m *VectorMatch, userID string) bool {
	// 实际业务逻辑：匹配用户分群
	return true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;缓存策略&lt;/h2&gt;
&lt;p&gt;在某些业务场景中油热点查询，这个时候在应用层加一层LRU缓存，把最近检索过的query和对应记忆ID缓存下来。
和业务里的redis做缓存防止击穿是一个道理。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以上这些可以总结为：怎么提高召回率，怎么根据具体的情况对数据库进行选型等。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;谈谈多模态&lt;/h2&gt;
&lt;p&gt;多模态在Rag领域里也火了起来。问题在于怎么自己做跨模态对齐，是个值得关注的前沿方向。现在很多 Agent 不只处理文本，还要记忆图片、语音这些多模态信息，这时候需要把不同模态的数据映射到同一个向量空间，用 CLIP 这类跨模态模型做 embedding。实际落地时要考虑不同模态的权重，比如电商客服记住用户发的产品图片比记住语气词更重要，检索时可以给图片向量更高的权重。具体实现上可以分别存储文本向量和图像向量，检索时做两次查询再合并结果，也可以用跨模态模型直接生成统一的向量表示。
从 Agent 记忆往外延伸还能关联到 RAG 的架构设计。Agent 的记忆系统本质上是 RAG 的一个应用场景，都是通过检索来增强生成的准确性。但两者也有差异，RAG 通常是检索固定的知识库，而 Agent 记忆是检索不断增长的交互历史。如果要做更复杂的推理，可以把知识图谱和向量检索结合，先用向量找到相关实体，再通过图谱关系扩展出更多上下文。比如用户问 “那个推荐过红色款的客服是谁”，先通过向量找到 “红色款推荐” 相关的记忆，再通过图谱关系找到对应的客服人员节点。
未来向量数据库会朝着几个方向发展。原生多模态支持会成为标配，不需要自己做跨模态对齐。更智能的索引自动调优，系统能根据查询模式自动选择最优索引策略，而不是靠 DBA 手动配置参数。和大模型的深度整合也是趋势，比如直接在向量数据库里做 Embedding，减少网络传输开销，甚至可能出现向量数据库和推理引擎一体化的产品。这些演进方向都在解决同一个核心问题：让 AI 应用的记忆系统更高效、更智能、更易用。&lt;/p&gt;
</content:encoded></item><item><title>开始读Linux了</title><link>https://yuumu.asia/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/linux%E7%9A%84%E4%B8%80%E4%BA%9B%E6%B5%85%E8%96%84%E8%AE%A4%E7%9F%A5/</link><guid isPermaLink="true">https://yuumu.asia/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/linux%E7%9A%84%E4%B8%80%E4%BA%9B%E6%B5%85%E8%96%84%E8%AE%A4%E7%9F%A5/</guid><description>很想学学有关Linux的内核相关知识，可是一致没啥时间。哎~</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;第零章：linux前情提要&lt;/h1&gt;
&lt;h2&gt;操作系统&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;操作系统是一组程序&lt;/strong&gt;，这组程序的重点在于管理电脑上所有活动以及驱动系统中的所有硬件&lt;/p&gt;
&lt;p&gt;其功能就是让CPU开始判断逻辑于运算数值，让内存开始加载/读出数据于程序代码，让硬盘，网卡可以开始传输数据。让外设开始在硬件上运行&lt;/p&gt;
&lt;h3&gt;系统内核kernel&lt;/h3&gt;
&lt;p&gt;内核主要提供一些管理功能。所以用户不能直接使用，而是&lt;strong&gt;内核程序防止在内存当中的区块是手保护的，并且，启动后一致常驻在内存里&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;系统调用（System Call）&lt;/h3&gt;
&lt;p&gt;操作系统会提供一套api。让人根据api去开发软件。开发的软件会在内核上运行。&lt;/p&gt;
&lt;h3&gt;内核功能&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;系统调用接口&lt;/strong&gt; 沟通内核和应用程序&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;进程管理&lt;/strong&gt; 控制任务，让CPU资源做有效分配。提供良好的CPU调度机制。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存管理&lt;/strong&gt; 由于系统程序代码和数据都必须放到内存中，通常内核会提供虚拟内存。当内存不住可以提供交换分区&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;文件系统管理&lt;/strong&gt; 文件系统的管理，数据的输入输出&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;设备驱动&lt;/strong&gt; 提供内核对硬件的管理，现在大部分可加载模块，可以将驱动程序编译成模块，无需重新编译内核。&lt;/p&gt;
&lt;h3&gt;驱动程序&lt;/h3&gt;
&lt;p&gt;驱动的出现主要是为了解决：不同类型的硬件。操作系统不可能都在他的内核直接支持了。为了让操作系统驱动这个硬件。操作系统通常会提供一个api给硬件开发商。让他们可以根据这个接口设计可以驱动硬件的驱动程序。这样，用户安装驱动程序后，就可以在操作系统使用硬件了。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;得出一个谁都知道，但还是请你听我废话总结以下的结论：&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;操作系统会提供api，让开发驱动的去写他们的驱动程序；驱动程序由厂商提供。不同操作系统所需的驱动程序不相同！！&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;应用程序&lt;/h2&gt;
&lt;p&gt;是根据操作系统提供的API开发出来的软件&lt;/p&gt;
&lt;h3&gt;POSIX&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;POSIX（Portable Operating System Interface，便携式操作系统接口）&lt;/strong&gt;
是由 &lt;strong&gt;IEEE 制定的一个操作系统标准&lt;/strong&gt;，主要定义 &lt;strong&gt;Unix 类操作系统&lt;/strong&gt; 应该如何提供系统调用和接口&lt;/p&gt;
&lt;p&gt;POSIX 的目标： &lt;strong&gt;让程序在不同操作系统上可移植&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;只要操作系统遵守 POSIX 标准，程序就可以在多个系统上运行，无需大量修改。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux&lt;/li&gt;
&lt;li&gt;macOS&lt;/li&gt;
&lt;li&gt;FreeBSD&lt;/li&gt;
&lt;li&gt;AIX&lt;/li&gt;
&lt;li&gt;Solaris&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些系统都遵循 POSIX 标准。&lt;/p&gt;
&lt;h4&gt;POSIX 包含哪些内容？&lt;/h4&gt;
&lt;p&gt;POSIX 定义了一套 &lt;strong&gt;标准 API&lt;/strong&gt;，包括：&lt;/p&gt;
&lt;h5&gt;文件操作&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;open()&lt;/code&gt;、&lt;code&gt;read()&lt;/code&gt;、&lt;code&gt;write()&lt;/code&gt;、&lt;code&gt;close()&lt;/code&gt;、&lt;code&gt;lseek()&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;进程与线程&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt;、&lt;code&gt;exec()&lt;/code&gt;、&lt;code&gt;pthread_create()&lt;/code&gt;、&lt;code&gt;pthread_join()&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;信号处理&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;signal()&lt;/code&gt;、&lt;code&gt;sigaction()&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;内存管理&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;mmap()&lt;/code&gt;、&lt;code&gt;munmap()&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;文件权限&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;chmod()&lt;/code&gt;、&lt;code&gt;chown()&lt;/code&gt;、&lt;code&gt;umask()&lt;/code&gt;&lt;/p&gt;
&lt;h5&gt;进程间通信（IPC）&lt;/h5&gt;
&lt;p&gt;管道、消息队列、共享内存、信号量等。&lt;/p&gt;
&lt;p&gt;这些 API 组成了一个“统一接口”。&lt;/p&gt;
&lt;h4&gt;POSIX 的意义是什么？&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. 程序可移植性提高&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;写一次代码，可以在多个类 Unix 系统运行。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 系统接口标准化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;应用程序开发不依赖某个特定操作系统。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 多线程标准：pthread&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;POSIX 线程（pthread）是跨平台通用的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linux 不是官方 POSIX 认证系统，但：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux 基本遵循 POSIX 标准&lt;/li&gt;
&lt;li&gt;常用系统调用与接口几乎全部兼容 POSIX&lt;/li&gt;
&lt;li&gt;Linux 还扩展了许多非 POSIX 的能力&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1、Linux&lt;/h1&gt;
&lt;p&gt;现在开始谈谈Linux：想到什么写什么咯。&lt;/p&gt;
&lt;p&gt;我们先从安装说起：首先你得理解磁盘分区：磁盘的磁片上面主要可以分为扇区和磁道两种单位。扇区的大小设计分别有512字节和4K字节两种，整块磁盘的第一个扇区都非常重要。早期存在第一个扇区里的是MBR。后来随着磁盘容量的补段扩大，造成一些读写上的困扰。所以后来的新多了一个叫GPT的分区格式。具体对不同分区的讲解可以查看相关资料。这里主要讲讲系统。&lt;/p&gt;
&lt;h4&gt;启动流程中的BIOS和UEFI启动检测程序&lt;/h4&gt;
&lt;p&gt;先说说CMOS和BIOS。其中CMOS的主要功能是为了记录下主板上的重要参数，例如时间、CPU电压频率等。BIOS是写入到主板上某一块Flash和EEPROM的程序，可以在计算机启动时执行以加载CMOS中的参数，并尝试调用引导程序进一步进入操作系统中。其中BIOS程序也可以修改CMOS中的参数。&lt;/p&gt;
&lt;p&gt;硬件参数是嵌入在主板上面的存储器中的，BIOS则是一个写入到主板上的固件（再次说明，固件就是写入到硬件上的一个软件程序）。这个BIOS就是在启动的时候，计算机系统会主动执行的第一个程序。&lt;/p&gt;
&lt;p&gt;接下来BIOS会去分析计算机里面有哪些存储设备。我们以硬盘为例：BIOS会依据用户的设置去取得能够启动的硬盘，并且到该硬盘里面去读取第一个扇区的MBR位置。MBR这个仅有446字节的硬盘容量里面会放置最基本的启动引导程序，此时BIOS就功成圆满，而接下来就是MBR内的启动引导程序的工作了。&lt;/p&gt;
&lt;p&gt;这个启动引导程序的目的是在加载（load）内核文件。由于启动引导程序是操作系统在安装的时候所提供的，所以它会认识硬盘内的文件系统格式，因此就能够读取内核文件，然后接下来就是内核文件的工作，启动引导程序与BIOS也功成圆满，将之后的工作就交给大家所知道的操作系统。&lt;/p&gt;
&lt;p&gt;简单地说，整个启动流程到操作系统之前的过程是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;BIOS：启动时主动执行的固件，会认识第一个可启动的设备；&lt;/li&gt;
&lt;li&gt;MBR：第一个可启动设备的第一个扇区内的主引导记录块，内含启动引导代码；&lt;/li&gt;
&lt;li&gt;启动引导程序（bootloader）：一个可读取内核文件来执行的软件；&lt;/li&gt;
&lt;li&gt;内核文件：开始启动操作系统。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要注意，如果你的分区表为GPT格式的话，那么BIOS也能够从LBA0的MBR兼容区块读取。&lt;/p&gt;
&lt;h4&gt;文件系统特性&lt;/h4&gt;
&lt;p&gt;文件除了实际内容通常还包括Linux操作系统的文件权限（rwx)与文件属性（拥有者，用户组，时间参数等）。文件系统会将两部分数据分别存放在不同区块。权限与属性会放到inode中。实际内容则放到数据区块中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;超级区块&lt;/em&gt; 记录整个文件系统的整体信息，包括inode与数据区块的总量、使用量、剩余量，以及文件系统的格式与相关信息等&lt;/li&gt;
&lt;li&gt;&lt;em&gt;inode区块&lt;/em&gt; 记录文件的属性，一个文件占用一个inode，同时记录此文件数据所在的区块号码&lt;/li&gt;
&lt;li&gt;&lt;em&gt;数据区块&lt;/em&gt; 存放文件的实际内容&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>孜同学整理的AD9833移植stm32f407使用手册</title><link>https://yuumu.asia/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/ad9833%E5%BA%94%E7%94%A8%E6%89%8B%E5%86%8C/</link><guid isPermaLink="true">https://yuumu.asia/posts/%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%95/ad9833%E5%BA%94%E7%94%A8%E6%89%8B%E5%86%8C/</guid><description>这是一份手把手教你如何去写AD9833这款ADC的驱动。</description><pubDate>Tue, 09 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;AD9833应用手册&lt;/h1&gt;
&lt;h2&gt;AD9833功能&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;可输出正弦波，三角波，方波。&lt;/li&gt;
&lt;li&gt;稳定频率范围0-2MHz，低频调频精度1Hz（误差&amp;lt;5%）。&lt;/li&gt;
&lt;li&gt;幅值恒定输出500 - 600mVpp，输出正负交流电。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用方法&lt;/h2&gt;
&lt;h3&gt;硬件连接&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;三线SPI&lt;/p&gt;
&lt;p&gt;STM32作为SPI的主机，AD9833作为从机。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NSS ---&amp;gt;FSYNC&lt;/li&gt;
&lt;li&gt;SCK&lt;/li&gt;
&lt;li&gt;SDATA&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;VCC和GND可直接连接STM32。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;软件设置&lt;/h3&gt;
&lt;h4&gt;STM32CubeMX配置&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751888.png&quot; alt=&quot;image-20251129151414477&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Mode：&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751890.png&quot; alt=&quot;image-20251129104005388&quot; /&gt;
这里有八种Mode，大致分为三类：&lt;em&gt;&lt;strong&gt;全双工，半双工，单向收发&lt;/strong&gt;&lt;/em&gt;。这里STM32作为主机，AD9833作为从机，我们只需要向从机单向发送数据，所以选择&lt;em&gt;&lt;strong&gt;Transmit only Master&lt;/strong&gt;&lt;/em&gt;(只由主机发送)。&lt;/p&gt;
&lt;p&gt;SPI通信详细知识参考：&lt;a href=&quot;https://blog.csdn.net/u010632165/article/details/109460814?ops_request_misc=%7B%22request%5Fid%22%3A%2207d20a67b0d9b3110d9a1321116bff38%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&amp;amp;request_id=07d20a67b0d9b3110d9a1321116bff38&amp;amp;biz_id=0&amp;amp;utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-109460814-null-null.142%5Ev102%5Epc_search_result_base1&amp;amp;utm_term=SPI&amp;amp;spm=1018.2226.3001.4187&quot;&gt;SPI协议详解（图文并茂+超详细）-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hardware NSS Signal&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751891.png&quot; alt=&quot;image-20251129104756755&quot; /&gt;&lt;/p&gt;
&lt;p&gt;勾选此项意味着使用SPI硬件片选引脚，此引脚为固定引脚。若后续有引脚使用冲突，可随意定义GPIO引脚输出高低电平模拟片选，这里选择Disable即可。我这里为测试SPI通信，遂勾选此项。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Frame Format（帧格式）
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751892.png&quot; alt=&quot;image-20251129105010958&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里选&lt;strong&gt;Motorola 格式&lt;/strong&gt;是因为他是&lt;strong&gt;SPI 的标准格式&lt;/strong&gt;，配置帧长度较灵活（可配置为 8/16/32 位等），TI固定16位，通用性较差。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Data Size&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751893.png&quot; alt=&quot;image-20251129111324805&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然AD9833一次传输16bits，但是&lt;em&gt;&lt;strong&gt;HAL库的SPI传输函数一次只支持传送8bits（const uint8_t格式）&lt;/strong&gt;&lt;/em&gt;，所以这里选择8bits。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751894.png&quot; alt=&quot;image-20251129111556088&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;First Bit&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751895.png&quot; alt=&quot;image-20251129112005061&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751896.png&quot; alt=&quot;image-20251129112100493&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为D15,D14决定写入哪个寄存器，所以选***MSB（Most Significant Bit）***即最高有效位先传输。为输入数据指明方向。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Prescaler (for Baud Rate)
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751897.png&quot; alt=&quot;image-20251129112725510&quot; /&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751898.png&quot; alt=&quot;image-20251129112751341&quot; /&gt;
这里的&lt;em&gt;&lt;strong&gt;预分频系数选择16&lt;/strong&gt;&lt;/em&gt;，对应波特率为2.625M（小于图十中的AD9833最大传输速率40M),太大了不稳定，太小了太慢，保险起见留有余量只要设置小于25M即可。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CPOL和CPHA&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751899.png&quot; alt=&quot;image-20251129113801218&quot; /&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751900.png&quot; alt=&quot;image-20251129113917727&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里从时序图看到AD9833的时钟一开始拉高，并且在第一个时钟下降沿开始读取数据，所以&lt;em&gt;&lt;strong&gt;CPOL选择High，CPHA选择1 Edge。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;CRC Calculation（&lt;strong&gt;CRC 校验计算&lt;/strong&gt;）
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751901.png&quot; alt=&quot;image-20251129114239570&quot; /&gt;&lt;/p&gt;
&lt;p&gt;AD9833 本身不支持 SPI 的 CRC 校验，因此需将设为&lt;strong&gt;Disabled&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NSS Signal Type&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751902.png&quot; alt=&quot;image-20251129115631793&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面选择了Hardware NSS Signal的话，这里&lt;em&gt;&lt;strong&gt;自动配置，不用管他。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;keil编译烧录&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;AD9833.c&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;AD9833.h&quot;
#include &quot;spi.h&quot;
/*
*******************************************************
向AD9833发送一个16bit的数据
*******************************************************
*/
void AD9833_Write(unsigned short TxData) //TxData是2字节
{
unsigned char data[2] ; //一个char一个字节，数组为2个字节
data[0] = (unsigned char)((TxData&amp;gt;&amp;gt;8) &amp;amp;0xff); //data[0]存储高位
data[1] = (unsigned char)(TxData&amp;amp;0xff); //data[1]存储低位
HAL_SPI_Transmit (&amp;amp;hspi2 , data, 2, 0x02) ; //用HAL库的SPI发送函数发送数据
}
/*
*******************************************************
Reset: 0为有输出，1为没输出，此位只控制有无输出，不复位寄存器
SleeppMode: 3为关闭内部DAC和时钟，0为不关闭
optionbit|modebit: 00正弦01三角10方波11保留
*******************************************************
*/
void AD9833_CtrlSet(unsigned char Reset,unsigned char SleeppMode,unsigned char optionbit,unsigned char modebit)
{
unsigned short regtemp = 0; //对输出模式的一些选择
regtemp = regtemp|(((unsigned short)Reset&amp;amp;0x01)&amp;lt;&amp;lt;8); //以下就是把每个位对应到相应的寄存器上，DIV2默认为0
regtemp = regtemp|((SleeppMode&amp;amp;0x03)&amp;lt;&amp;lt;6);
regtemp = regtemp|((optionbit&amp;amp;0x01)&amp;lt;&amp;lt;5);
regtemp = regtemp|((modebit&amp;amp;0x01)&amp;lt;&amp;lt;1);
AD9833_Write(regtemp); //写入数据，不需要先声明将要写入，可以复用指令，只要前两位是00
}
/*
*******************************************************
设置频率，设置完后暂不输出，用CtrlSet函数设置输出
频率值：0.1Hz-12.5MHz（最大值为25M晶振时钟的一半）
单位：Hz;例如，输出1M，则输入1000000
*******************************************************
*/
void AD9833_FreqSet(double Freq) 
{
    // 核心：先计算28位频率控制字
    uint32_t freq_data = (uint32_t)(Freq * 268435456.0 / 25000000.0); 
    // 25000000是AD9833参考时钟（硬件晶振为25MHz时，若实际是12.5M则改为12500000）
    // 拆分28位为两个14位（LSB和MSB）
    uint16_t frequence_LSB = (uint16_t)(freq_data &amp;amp; 0x3FFF); // 低14位
    uint16_t frequence_MSB = (uint16_t)((freq_data &amp;gt;&amp;gt; 14) &amp;amp; 0x3FFF); // 高14位
    // 标记为写入FREQ0寄存器（bit15-14=01）
    frequence_LSB |= 0x4000; 
    frequence_MSB |= 0x4000; 
    // 先写入控制字：连续写FREQ0的LSB和MSB（bit15-12=0010，bit8=0（不复位））
    AD9833_Write(0x2000); // 原代码0x2100错误：bit8=1是Reset，会关闭输出
    // 按顺序写入LSB、MSB
    AD9833_Write(frequence_LSB);
    AD9833_Write(frequence_MSB);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;AD9833.h&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;main.h&quot;
void AD9833_Write(unsigned short TxData);
void AD9833_CtrlSet(unsigned char Reset,unsigned char SleeppMode,unsigned char optionbit,unsigned char modebit);
void AD9833_FreqSet(double Freq);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;main.c&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include &quot;main.h&quot;
#include &quot;spi.h&quot;
#include &quot;gpio.h&quot;

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include &quot;AD9833.h&quot;
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SPI2_Init();
  /* USER CODE BEGIN 2 */
	//置RESET位为1，复位AD9833硬件（不清除寄存器）
  AD9833_Write(0x0100); //0001 0000 0000--D8是RESET位
  //短暂延时，确保复位完成（1ms足够）
  HAL_Delay(1);  
  AD9833_FreqSet(10000);//方波频率为设置的一半，其他两个相同
	AD9833_CtrlSet(0,0,0,0);//  00正弦|01三角|10方波|11保留

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
     HAL_Delay(100);//减少功耗，降低 MCU 占用率：若无延时，MCU 会以最高主频疯狂执行空循环
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
  RCC_OscInitStruct.PLL.PLLM = 8;
  RCC_OscInitStruct.PLL.PLLN = 168;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 4;
  if (HAL_RCC_OscConfig(&amp;amp;RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&amp;amp;RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf(&quot;Wrong parameters value: file %s on line %d\r\n&quot;, file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;软件驱动编写&lt;/h2&gt;
&lt;p&gt;AD9833有三种功能寄存器，&lt;em&gt;&lt;strong&gt;控制寄存器、频率寄存器、相位寄存器&lt;/strong&gt;&lt;/em&gt;。AD9833每次接收16bits，然后我们根据要改写的功能（频率或相位）改写16bits里的内容。&lt;/p&gt;
&lt;h3&gt;控制寄存器（16 bits）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751903.png&quot; alt=&quot;image-20251129122841931&quot; /&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751904.png&quot; alt=&quot;image-20251129123247878&quot; /&gt;&lt;/p&gt;
&lt;p&gt;控制寄存器内有16bits，即SPI传输一次可改写其全部内容（HAL_SPI_Transmit第三个参数为2，即一次传输两个字节)。&lt;em&gt;&lt;strong&gt;这16bits的功能大概分成四类&lt;/strong&gt;&lt;/em&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;帧头(D15,D14)
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751905.png&quot; alt=&quot;image-20251129124453556&quot; /&gt;
&lt;em&gt;&lt;strong&gt;D15和D14位设为0&lt;/strong&gt;&lt;/em&gt;，告知AD9833我们这16bits写入的对象是控制寄存器。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;规定数据写入格式(D13,D12)
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751906.png&quot; alt=&quot;image-20251129124850633&quot; /&gt;&lt;/p&gt;
&lt;p&gt;因为我们不需要粗调细调分开来的功能，所以选择一次输入1 word（16bits)，&lt;em&gt;&lt;strong&gt;将D13设定为1，此时D12无用，不用管他默认为0。&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设定波形输出状态(D1,D3,D5,D11,D10)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;D1,D3,D5三位共同决定输出波形为正弦波，三角波或者方波
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751907.png&quot; alt=&quot;image-20251129125459677&quot; /&gt;&lt;/li&gt;
&lt;li&gt;AD9833共有两个独立的频率寄存器FREQ0和FREQ1。&lt;em&gt;&lt;strong&gt;D11&lt;/strong&gt;&lt;/em&gt;决定输出哪个寄存器内的值。&lt;em&gt;&lt;strong&gt;为0输出FREQ0&lt;/strong&gt;&lt;/em&gt;，为1输出FREQ1。&lt;/li&gt;
&lt;li&gt;AD9833共有两个独立的相位寄存器PHASE0和PHASE1。D10决定输出哪个寄存器内的值。为0输出PHASE0，为1输出PHASE1。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设定AD9833工作状态(D6,D7,D8)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;D8&lt;/strong&gt;&lt;/em&gt;为RESET位，&lt;strong&gt;上电时RESET一次&lt;/strong&gt;（设置为1，后面在AD9833_CtrlSet函数中默认设置为0），复位硬件，不改变频率寄存器和相位寄存器的值。即&lt;strong&gt;AD9833有掉电存储功能&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751908.png&quot; alt=&quot;image-20251129133033079&quot; /&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751909.png&quot; alt=&quot;image-20251129134302173&quot; /&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751910.png&quot; alt=&quot;image-20251129134345071&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将&lt;em&gt;&lt;strong&gt;D6,D7都设定为0&lt;/strong&gt;&lt;/em&gt;，让AD9833一直保持工作状态，输出波形。
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751911.png&quot; alt=&quot;image-20251129134556816&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;预留位（D0,D2,D4,D9）
为了兼容性、扩展性和容错性有几个预留位，我们&lt;em&gt;&lt;strong&gt;不用管他们，全部都默认设置为0&lt;/strong&gt;&lt;/em&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;频率寄存器（28 bits）&lt;/h3&gt;
&lt;p&gt;AD9833一次只能接受16bits，所以要发送两次即32bits发送，每次都要2bits帧头，除去共4bits帧头，&lt;em&gt;&lt;strong&gt;剩下28bits写入频率寄存器当作频率大小&lt;/strong&gt;&lt;/em&gt;。如图所示，配置FREQ0。
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751912.png&quot; alt=&quot;image-20251129140143115&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;帧头（D14,D15）&lt;/p&gt;
&lt;p&gt;AD9833有两个频率寄存器,&lt;em&gt;&lt;strong&gt;D15|D14&lt;/strong&gt;&lt;/em&gt;分别为&lt;em&gt;&lt;strong&gt;0|1&lt;/strong&gt;&lt;/em&gt;，1|0时写入&lt;em&gt;&lt;strong&gt;FREQ0&lt;/strong&gt;&lt;/em&gt;和FREQ1。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据位（其他28bits）&lt;/p&gt;
&lt;p&gt;因为&lt;strong&gt;AD9833是先写入LSB,再写入MSB，所以发送函数也要按顺序编写&lt;/strong&gt;。
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751913.png&quot; alt=&quot;image-20251129140303339&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;相位寄存器（16 bits）&lt;/h3&gt;
&lt;p&gt;相位寄存器和频率寄存器数量相同，不同的是位数和帧头数量（3个）。
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751914.png&quot; alt=&quot;image-20251129140715241&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;频率计算公式&lt;/h3&gt;
&lt;p&gt;这里硬件晶振是25MHz，所以AD9833输出波形频率理论上限是12.5MHz（奈奎斯特采样定律）。可输出频率上限由晶振大小决定。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751915.png&quot; alt=&quot;image-20251129142857811&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751916.jpg&quot; alt=&quot;0e29c9878dfba50b63691f0bd95bc6d4&quot; /&gt;
&lt;img src=&quot;https://raw.githubusercontent.com/Etta666/AD9833/main/pic/20251129163751917.png&quot; alt=&quot;image-20251129142945058&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;使用心得&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;分模块&lt;/strong&gt;&lt;/em&gt;功能编写程序。在AD9833.c里分成AD9833_Write，AD9833_CtrlSet和AD9833_FreqSet，之所以没有AD9833_PhaseSet是因为AD9833只有一个输出口，改不改相位没影响。&lt;/li&gt;
&lt;li&gt;在使用发送数据函数的时候要注意&lt;em&gt;&lt;strong&gt;参数格式&lt;/strong&gt;&lt;/em&gt;对不对。HAL_SPI_Transmit函数里一次只能发送8bits，就不能直接填写16bits的TxData。&lt;/li&gt;
&lt;li&gt;基本&lt;em&gt;&lt;strong&gt;只用FREQ0&lt;/strong&gt;&lt;/em&gt;就够了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;遇到的问题&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;无法输出正常频率的方波。改DIV2的值不起作用。只能输出频率比设定频率小一半的方波。&lt;/li&gt;
&lt;li&gt;输出的幅度不太稳定，有时不是0.6Vpp，变成0.4Vpp。&lt;/li&gt;
&lt;li&gt;STLINK烧程序不太好使，动不动就要拔了重连。😑&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2301_76587898/article/details/139061523?spm=1001.2014.3001.5506&quot;&gt;STM笔记_AD9833的详解及其f407驱动_ad9833驱动程序详解-CSDN博客&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>一次电子设计比赛</title><link>https://yuumu.asia/posts/%E4%BC%A4%E5%AF%92%E6%9D%82%E7%97%85/%E4%B8%80%E6%AC%A1%E7%94%B5%E5%AD%90%E8%AE%BE%E8%AE%A1%E6%AF%94%E8%B5%9B/</link><guid isPermaLink="true">https://yuumu.asia/posts/%E4%BC%A4%E5%AF%92%E6%9D%82%E7%97%85/%E4%B8%80%E6%AC%A1%E7%94%B5%E5%AD%90%E8%AE%BE%E8%AE%A1%E6%AF%94%E8%B5%9B/</guid><description>一次大一期间，可能是终身难忘的电子设计比赛</description><pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;前情提要&lt;/h2&gt;
&lt;p&gt;风和日丽的一天。班里某爱玩原神的周同学联系我说光电学院B311实验室的两名同学想要参加电子设计大赛，但是缺一个人。这个时候周同学想到了我这块十分好搬的砖。我也不知道找我这个愣头青，出于想让电子实验室大佬带我赢的心情，我答应了。缘起就是我大一做过一点点关于电子设计的东西。但并未加入电子实验室。我便想要完成这么一次比赛充实自己。&lt;/p&gt;
&lt;h2&gt;过程中发生的乌龙&lt;/h2&gt;
&lt;p&gt;我拿到了题目发现事情并不简单。要求用555做一个方波，锯齿波，一次谐波和三次谐波的正弦。选做是：要用运放去做波形的失真。有：顶部失真，底部失真。交越失真。我想了想上学期好像是学过555定时器可以做施密特触发器去完成这些功能。我便加上了那两个队友。线上讨论了一周。&lt;/p&gt;
&lt;p&gt;第二周我在南部上课遇到了这俩同学。有个同学的名字比较中性，&lt;s&gt;我把他当成了两周的男生()&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;开始&lt;/h2&gt;
&lt;p&gt;从五月份就开始了。我们的进度一直为0。耍了两天我开动了。过程是先设计电路。我先找出了555的引脚和我上学期的笔记，如下&lt;/p&gt;
&lt;p&gt;我先找到了让生成标准方波的办法。555无稳态电路的输出高低电平时长由电容充电时间T1和放电时间T2决定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;充电：电源VCC经充电电阻给电容C充电，电压从1/3VCC升到2/3VCC，输出高电平；&lt;/li&gt;
&lt;li&gt;放电：电容C经放电电阻向555的7脚（放电端）放电，电压从2/3VCC降到1/3VCC，输出低电平；&lt;/li&gt;
&lt;li&gt;占空比：D = T1 / (T1 + T2) × 100%，周期 T = T1 + T2。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键设计：二极管隔离的独立充放电回路。&lt;/strong&gt; 利用二极管的单向导电性，将充电、放电回路彻底分离，再配合滑动变阻器（电位器）调节两路的电阻值，实现占空比可调：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;充电回路：Vcc → 充电电阻R1 → 二极管D1 → 电容C → GND&lt;/li&gt;
&lt;li&gt;放电回路：电容C → 二极管D2正向 → 放电电阻R2 → 555的7脚 → GND&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;电位器串联在充/放电回路中，调节其滑片即可改变T1或T2，进而调节占空比。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;555二极管隔离充放电回路的仿真电路图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我开始画了PCB，我看过很多芯片啊或者电脑主板的布线，有的六层板八层板确实能让人瞋目结舌。我画的智能保证他不断路和短路。至于通不通还得看福分。正直假期末尾。估计嘉立创也不送。估计都大家挺懒得动的。&lt;/p&gt;
&lt;p&gt;我便去自己焊接一块试试。来到B311实验室。焊半天果然还是没有通路。&lt;/p&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;等到五一过来之后，基本上全天泡在实验室里头弄。我开始去网上搜集各种方案在那尝试。电工和码农唯一对等的一句话可能：“为什么这个玩意在仿真能跑出来，实际搭出来没东西啊。”。&lt;/p&gt;
&lt;p&gt;比如后面的part是做矩形波。我第一个想到的就是积分电路，方波积分形成三角波。我们用的是LM324一个四通道运放。我们其实拿他第一个通道做了一个阻抗转换降低后一级的输入阻抗，不过后期我问学长才知道，在低频的情况下其实做不做阻抗转换意义不大。第二个通道才是积分器。调整RC电路的乘积使得他和输入的方波匹配，能跑出不失真的矩形波（其实是三角板，锯齿波应该不是这么做的，我理解的RC充放电可能叫锯齿波）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;LM324积分电路的仿真图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;……过程非常的艰辛，经常遇到晚上快关门了，搭好的电路波形消失了。快审核的前一天，我们调出了这样的波形&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;审核前一天在示波器上调出来的波形&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后我打的板子什么的也是基本没用到，都是拿洞洞板焊的，可见我画板子的能力有多菜了（哭。&lt;/p&gt;
&lt;p&gt;最令人难受的是电路最后还是在审核之前烧坏了。最终我们也只获得了方波的分数。虽然以后不知道还不会接触到电路设计的工作，但是我知道了在任何想法面前，稳定性和可用性永远都是第一位的。这和编程是一个道理。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;最后还是从中学到很多东西。认识了很重要的人的。也是我大学里难忘的经历之一。之后希望我还是会把精力放在我想从事的方向上。&lt;/p&gt;
</content:encoded></item></channel></rss>