缘起数学一直是我的弱项，最近准备研究生于是痛定思痛对以前学的数学内容做个小整理。篇幅短小是因为打latex公式太麻烦了（没错虽然笔记记录在ipad上可以导出pdf，但意识流笔记，字迹排版啥还是太抽象了，就不嫌丑发出来了QwQ）对大学数学的分类我专业比较吃数学，回顾大一到大三，基本数学学习的脉络是这样的：数学分析，高数增强版（掐人中线性代数概率论工科数学基础（复分析，场论和数学物理方法）离散数学随机过程非常惨烈的是数学知识点一般会落后于专业课，所以专业课需要超前点映一些数学的内容，这时候就会非常懵逼（啊，我吗？.JPG）当然一般专业学数学就学前三项，我一般称为“三板斧”。三板斧学好了后面学起来也不会太折磨。数分相关第一个要学的是 集合，函数 。集合是函数的操作对象，广义上说函数就是一个集合到另一个集合的对应关系（映射），当然一般我们在实分析里面默认集合就是 $R, R^2, R^3$，对应实数域，二维空间和三维空间。在此基础上我们给出 极限和连续 的定义，为高等数学提供了一个区分高中的数学抓手。函数研究的是其性态，包括 最大最小值/极大极小值，奇偶性，周期性 。在此基础上我们可以研究函数的图像，并给出几个初等函数及其经典图像。当然，基础的性态不足以完整刻画函数的特点，于是基于极限，我们进一步提出刻画的工具——导数，以及对应的三个定理：中值定理，微分等式和微分不等式。以上内容构成一元函数微分学内容。而一元函数积分学按照上面思路，给出积分的计算方法，积分等式与不等式，并顺势推广到多元函数—— 多元函数微分学和多元函数积分学 。需要注意的是， 二重积分 其实属于多元函数积分学内，但课本单独提出。而 微分方程 属于多元函数微分学和积分学的综合应用，需要按情况分类比较好（这是个坑嘛hhh）。在谈论函数时，我们应该注意到函数的离散化—— 数列 ，并通过定理将数列的求解问题转为对等价函数的求解问题。线代相关线代主要操作对象是实数域中的向量（亦即函数），通过 行列式 推出 矩阵 概念，并将矩阵概念理解为线性系统即可。理解矩阵后就可以通过矩阵运算实现线性变换，这里需要区分下 线性相关和线性无关 这两个核心概念，并基于两个概念分出后面各种衍生概念，包括极大无关组，合同矩阵，正定矩阵等。将矩阵的概念再扩大，理解矩阵向量空间，在向量空间内则包含各种函数，其几何意义需要我们关注，包括平面，线的方程，以及二次型相关曲面和曲线的标准形。对标准形变基则可以化为9种不同的基本图形。本质上，线代想要搞清的就是线性变换的问题，即给定一个函数f，如何通过线性变换让f变为g，使其简化。概率相关概率首先要明确 事件 ，然后是 概率 的内涵。注意概率对标集合，从集合的运算推出概率的运算及性质。对有限情况我们可以沿用高中学的古典概，而经典分布则为 0-1，伯努利，泊松，几何，超几何 ，这些概念是逐渐衍生推广的，对标样本点n从有限到无穷。（伯努利是0-1的n重，泊松是n趋于无穷的结果，推导要用数分知识）注意到无穷的情况，我们需要更加细密的颗粒度，即 几何概 ，其代表古典概样本趋于无穷的情况，但需要空间仍然可度量。与从一元函数到多元函数的过程类似，我们提出随机变量的概念，并将其推广到多元，那么多元如何退化为一元呢？只需要 边缘概 就好（也即消元，可以理解为条件概）几何分布的内容比离散的古典概分布丰富，最出名的当然属 高斯正态分布 ，公式放在这里了：$$ p_1(x) = \frac{1}{\sqrt{2\pi\varrho^2}}exp\{-\frac{(x-\mu)^2}{2\varrho^2}\} $$一维放了那二维还会远吗（笑），二维分布如下，注意多了 $\rho$ 表示两维的关联性。$$ p_2(x,y) = \frac{1}{2\pi\varrho_1\varrho_2\sqrt{1-\rho^2}}exp\{ -\frac{1}{2(1-\rho^2)}[(\frac{x-\mu_1}{\varrho_1})^2 + (\frac{y-\mu_2}{\varrho_2})^2 - 2\rho(\frac{x-\mu_1}{\varrho_1})(\frac{y-\mu_2}{\varrho_2})] \} $$[quote color="info"]二维公式的背诵技巧：一维乘以一维，前面系数除以 $\sqrt{1-\rho^2}$ 是为了单位化，正常乘 $\sqrt{2\pi}$ 就打开根号了。后面e指数上系数看作平方公式，打开时候系数整体多系数$1-\rho^2$表单位化，平方公式把括号打开了，有xy那项多了系数$\rho$（x平方和y平方都是自己和自己的关系，不可能存在相关系数，正常写）[/quote]考试需要会背会默。除了正态分布，还有t分布，莱斯分布，瑞利分布等，这些分布在不同的场合也有应用。再之后就是对应分布的 数字特征 了，典型的是均值，方差，矩，协方差等。最后解决实际问题，大数定理与中心极限定理，参数估计和假设检验。三大板块一般出大题，套路比较明显。提后话上面的内容不是想给出每个具体公式的推导之类，而是想梳理下知识的脉络，个人喜欢先列个大纲，再填充枝叶。数学作为工具到后面都是要用的，如果只看枝叶而不管联系，到实际问题时很可能就无法联想到解决问题的抓手，一堆公式不知道用哪个，怎么变形凑成目标形式。温故知新，希望数学下次不要再给我使绊子了 :@(献花) [DarkBText]以上内容为作者不看课本默出，如有谬误请评论走起。毕竟作者只是个被数学折磨的小白罢了（悲）[/DarkBText]
Alist代理下的Obsidian同步问题：Remotely Save 启用WebDav报错405 Method Not Allowed
起因之前写过(【指南】Obsidian+Cloudreve多端同步方案)[https://www.nickwald.top/study/obsidian-cloudreve.html]这篇文章，在试图切换为Alist作为后端代理时出现问题。那么就有看官要问了，为什么不继续使用Cloudreve呢？因为自己的服务器做了初始化，之前的环境要重新配置，二来Alist支持阿里云盘Open，恰巧自己就有阿里云盘的VIP哈哈哈哈哈问题排查天大地大，权限最大如果你哪个地方报错了，不管是什么错，一定要先检查是否是权限是否足够（背后蕴含多少眼泪.jpg）。Alist使用WebDav需要确认用户已经开启了读写权限，新版本甚至还要用户本身对文件具有操作的能力。排查结果：OK啊用户权限没问题（沧桑脸.jpg）路径问题教程里提示说明网址需要添加/dav后缀，我添加了，仍然报错。。。Bing了下，发现好像绝大部分都是到这里就OK了，虽然Obsidian仍然在报错。。。等等…………注意到WebDav网址格式为：协议://域名或IP地址:端口号/路径所以单纯的/dav后面还要加上路径！（注意力惊人.jpg）所以原本路径http://example.whereami.com/dav应该修改为http://example.whereami.com/dav/so/your/path/here解决。总结很小的一个问题，如果他报错不是405 Method Not Allowed就好了，弄得我下意识以为https协议不支持路径不存在一般不是报404 Not Found或者400 Bad Request吗。。。
事情起因本学期有大数据课设，选择了Steam评论情感预测方向。现在已知 appid ，要求根据 appid 爬取对应游戏的信息。数据文件的封装先写了个DataIO对底层数据文件的交互进行封装。class DataIO: """ DataIO类用于封装数据资源 Args: file_path (str): 数据文件路径 """ file_path: str = None encoding:str = 'utf-8' def __init__(self, file_path: str, encoding: str = 'utf-8'): self.file_path = file_path self.encoding = encoding def save(self, data: dict, update: bool = False): """ 保存数据到文件 Args: data (dict): 要保存的数据列表 update (bool): 是否覆盖文件中存在的同appid数据，默认为False """ if self.file_path is None: raise ValueError("文件路径不能为空") file_type = os.path.splitext(self.file_path)[-1][1:] match file_type: case 'csv': self.save_to_csv(data, update) case 'xlsx': self.save_to_xlsx(data, update) case _: raise ValueError(f"不支持的文件类型{file_type}") def save_to_csv(self, data: dict, update: bool = False): """ 保存数据到CSV文件 Args: data (dict): 要保存的数据 update (bool): 是否覆盖文件中存在的同appid数据，默认为False """ if self.file_path is None: raise ValueError("文件路径不能为空") try: if os.path.isfile(self.file_path) and update: df = pd.read_csv(self.file_path, encoding=self.encoding) if 'appid' in df.columns: mask = df['appid'] == data['appid'] if mask.any(): # 创建一个新的 DataFrame 用于更新 new_df = pd.DataFrame([data]) # 重新索引新的 DataFrame 以匹配原 DataFrame 的列顺序 new_df = new_df.reindex(columns=df.columns) # 更新匹配的行 df.loc[mask] = new_df.values else: df = pd.concat([df, pd.DataFrame([data])], ignore_index=True) else: df = pd.concat([df, pd.DataFrame([data])], ignore_index=True) df.to_csv(self.file_path, index=False, encoding=self.encoding) else: pd.DataFrame([data]).to_csv(self.file_path, index=False, encoding=self.encoding, mode='a', header=not os.path.isfile(self.file_path)) except Exception as e: print(f"保存数据到 CSV 文件时出错: {e}") def save_to_xlsx(self, data: dict, update: bool = False): """ 保存数据到Excel文件 Args: data (dict): 要保存的数据列表 update (bool): 是否覆盖文件中存在的同appid数据，默认为False """ if self.file_path is None: raise ValueError("文件路径不能为空") try: if os.path.isfile(self.file_path) and update: df = pd.read_excel(self.file_path, engine='openpyxl') if 'appid' in df.columns: mask = df['appid'] == data['appid'] if mask.any(): # 创建一个新的 DataFrame 用于更新 new_df = pd.DataFrame([data]) # 重新索引新的 DataFrame 以匹配原 DataFrame 的列顺序 new_df = new_df.reindex(columns=df.columns) # 更新匹配的行 df.loc[mask] = new_df.values else: df = pd.concat([df, pd.DataFrame([data])], ignore_index=True) else: df = pd.concat([df, pd.DataFrame([data])], ignore_index=True) df.to_excel(self.file_path, index=False, engine='openpyxl') else: pd.DataFrame([data]).to_excel(self.file_path, index=False, engine='openpyxl', mode='a', header=not os.path.isfile(self.file_path)) except Exception as e: print(f"保存数据到 Excel 文件时出错: {e}")对Soup对象的封装【重点】使用 BeautifulSoup 一大痛点就是需要对获取的对象进行处理。第一版直接使用函数的结果是代码的混乱。为了减少重复操作，索性对soup进行了二次封装。class SoupFindResult: """ SoupFindResult类用于封装soup.find()方法的返回值 当初始化不传入element时，会将soup作为返回结果的element Args: element (BeautifulSoup): BeautifulSoup对象 element_text (str): 元素的文本内容 """ element = None element_text: str = None def __init__(self, soup: BeautifulSoup, element: str = None, class_: str =None, id_: str=None): if soup is None: return selector = {} if class_: selector['class_'] = class_ if id_: selector['id'] = id_ if element and soup.find(element, **selector): self.element = soup.find(element, **selector) self.element_text = soup.find(element, **selector).text.strip() else: self.element = soup self.element_text = soup.text.strip() def text_number(self, ignore_signal = False, number_index: int = 0, to_int = False): """ 提取数字（仅支持单个数字） Args: ignore_signal (bool): 是否忽略全部符号，此时将拼凑所有数字（默认为False） number_index (int): 数字的索引，默认为0 to_int (bool): 是否转换为整数，默认为False Returns: float/int: 提取的数字 Raises: IndexError: 数字索引超出范围 """ if self.element_text: text = re.sub(r"[^0-9.]", "", self.element_text) if ignore_signal else self.element_text match = re.findall(r"\d+\.?\d*", text) if len(match) > number_index: return int(match[number_index]) if to_int else float(match[number_index]) raise IndexError(f"数字索引超出范围：{number_index} > {len(match)}") return None def __str__(self): return self.element_text def find_all(self, element: str, class_: str =None, id_: str=None): """ 查找所有子元素 Args: element (str): 要查找的元素 class_ (str): 元素的class属性 id_ (str): 元素的id属性 Returns: SoupFindResult: 封装后的查找结果 """ if self.element: selector = {} if class_: selector['class_'] = class_ if id_: selector['id'] = id_ return [self.__class__(item) for item in self.element.find_all(element, **selector)] return None def find(self, element: str, class_: str =None, id_: str=None): """ 查找第一个子元素 Args: element (str): 要查找的元素 class_ (str): 元素的class属性 id_ (str): 元素的id属性 Returns: SoupFindResult: 封装后的查找结果 """ return self.__class__(self.element, element, class_, id_)最后的组合import re import requests from bs4 import BeautifulSoup import csv import pandas as pd import os import urllib3 class SteamAppIDRequest: """ SteamAppIDRequest类用于请求Steam游戏信息并封装 """ class DataIO: # 略 class SoupFindResult: # 略 # 数据文件封装类 data_IO: DataIO = None # 爬虫代理设置 headers = {} soup: BeautifulSoup = None # 游戏appid appid: int = None # 游戏名称 name: str = None # 游戏价格（国区原价） price: float = None # 游戏标签 tags: list = [] # 游戏发行时间 release_date: str = None # 游戏开发商 developer: list[str] = None # 游戏发行商 publisher: list[str] = None # 游戏支持的系统 supported_systems = { 'Windows': False, 'macOS': False, 'Linux': False } # 游戏模式 modes: list[str] = [] # 游戏评分 metacritic_score: float = None # 游戏用户评分 review_total: int = None review_positive: int = None review_negative: int = None # 游戏最低配置 system_requirements: dict[str:str] = { 'processor' : None, 'memory' : None, 'graphics' : None, 'storage' : None, 'SSD' : False } # 支持的语言 supported_languages: list[str] = [] def __init__(self, appid: int, data_file_path: str = 'steam_game_info.csv'): """ 初始化SteamAppIDRequest类 Args: appid (int): 游戏的appid data_file_path (str): 数据文件路径，默认为steam_game_info.csv Raises: ValueError: appid不能为空 """ if not appid: raise ValueError("appid不能为空") self.headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", "Accept-Language": "zh-CN,zh;q=0.9" } self.appid = appid self.info(appid) self.data_IO = self.DataIO(data_file_path) def soup_find(self, element: str, class_: str =None, id_: str=None) -> SoupFindResult: """ 封装soup.find()方法 Args: element (str): 要查找的元素 class_ (str): 元素的class属性 id_ (str): 元素的id属性 Returns: SoupFindResult: 封装后的查找结果 """ res = self.SoupFindResult(self.soup, element, class_, id_) return res def soup_find_all(self, element: str, class_: str =None, id_: str=None) -> list[SoupFindResult]: """ 封装soup.find_all()方法 Args: element (str): 要查找的元素 class_ (str): 元素的class属性 id_ (str): 元素的id属性 Returns: list[SoupFindResult]: 封装后的查找结果 """ res = self.SoupFindResult(self.soup) return res.find_all(element, class_, id_) def get(self, appid): """ 获取指定appid的游戏信息 Args: appid (int): 游戏的appid Returns: SteamAppIDRequest: 当前SteamAppIDRequest对象，方便链式调用 """ # 禁用SSL证书验证警告 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # 禁用警告 url = f"https://store.steampowered.com/app/{appid}/" try: # 使用代理访问时不验证证书 response = requests.get(url, headers=self.headers, verify=False) response.raise_for_status() self.soup = BeautifulSoup(response.text, 'html.parser') return self except requests.RequestException as e: print(f"请求出错: {e}") except Exception as e: print(f"发生错误: {e}") def info(self, appid): """ 获取游戏信息并返回 Args: appid (str): 游戏的appid Returns: SteamAppIDRequest: 当前SteamAppIDRequest对象，方便链式调用 """ self.get(appid) if not self.soup: return self # 游戏名 self.name = self.soup_find('div', 'apphub_AppName').element_text # 游戏价格 purchase_div = self.soup_find('div', 'game_purchase_action') if purchase_div.element: price = purchase_div.find('div', 'game_purchase_price').element_text if price: # 处理免费游戏 if '免费' in price: self.price = 0.00 else: self.price = purchase_div.find('div', 'game_purchase_price').text_number() # 游戏标签 genre = [] genre_links = self.soup_find('div', None, 'genresAndManufacturer').find_all('a') if genre_links: genre = [link.element_text for link in genre_links if '/genre/' in link.element.get('href', '')] glance_tags = [] glance_tags_links = self.soup_find('div', 'popular_tags').find_all('a') if glance_tags_links: glance_tags = [link.element_text for link in glance_tags_links] self.tags = list(set(genre + glance_tags)) # 游戏开发商&发行商 manufacturer_div = self.soup_find('div', None, 'genresAndManufacturer') if manufacturer_div: links = manufacturer_div.find_all('a') self.developer = [link.element_text for link in links if 'developer' in link.element.get('href', '')] self.publisher = [link.element_text for link in links if 'publisher' in link.element.get('href', '')] # 游戏发行时间 self.release_date = self.soup_find('div', 'release_date').element_text.split("\n")[-1].strip() # 支持的系统 for icon in self.soup.find_all('span', class_='platform_img'): classes = icon.get('class', []) if 'win' in classes: self.supported_systems['Windows'] = True elif 'mac' in classes: self.supported_systems['macOS'] = True elif 'linux' in classes: self.supported_systems['Linux'] = True # 游戏模式 for mode in self.soup_find_all('a', 'game_area_details_specs_ctn'): label_div = mode.find('div', 'label') if label_div: self.modes.append(label_div.element_text) # Metacritic评分 self.metacritic_score = self.soup_find('div', 'score').text_number() # 系统要求（最低配置） system_requirements_div = self.soup_find('div', 'game_area_sys_req') if not system_requirements_div.element: system_requirements_div = self.soup_find('div', 'game_area_sys_req_leftCol') if system_requirements_div: system_requirements_ul = system_requirements_div.find('ul').find('ul') if system_requirements_ul: for li in system_requirements_ul.find_all('li'): if '处理器' in li.element_text: self.system_requirements['processor'] = li.element_text.split(':')[-1].strip() elif '内存' in li.element_text: self.system_requirements['memory'] = li.element_text.split(':')[-1].strip() elif '显卡' in li.element_text: self.system_requirements['graphics'] = li.element_text.split(':')[-1].strip() elif '存储' in li.element_text: self.system_requirements['storage'] = li.element_text.split(':')[-1].strip() elif 'SSD' in li.element_text: self.system_requirements['SSD'] = True # 游戏评论 self.review_total = self.SoupFindResult(self.soup.find('label', {'for': 'review_type_all'}), 'span', 'user_reviews_count').text_number(ignore_signal=True, to_int=True) self.review_positive = self.SoupFindResult(self.soup.find('label', {'for':'review_type_positive'}), 'span', 'user_reviews_count').text_number(ignore_signal=True, to_int=True) self.review_negative = self.SoupFindResult(self.soup.find('label', {'for':'review_type_negative'}), 'span', {'class':'user_reviews_count'}).text_number(ignore_signal=True, to_int=True) # 游戏支持的语言 language_rows = self.soup_find('table', 'game_language_options').find_all('tr')[1:] # 跳过表头 for row in language_rows: if 'unsupported' in row.element['class']: continue # 跳过不支持的语言 language_cell = row.find('td', 'ellipsis') if language_cell: self.supported_languages.append(language_cell.element_text) return self def format(self): """ 格式化游戏信息并返回 Returns: dict: 格式化后的游戏信息 Raises: ValueError: 游戏信息不存在或无法访问 """ if self.soup is None: raise ValueError("游戏信息不存在或无法访问") if self.name is None: raise f"游戏ID {self.appid} 名称不存在或无法访问。" if self.price is None: raise f"游戏ID {self.appid} 价格不存在或无法访问。" if len(self.tags) == 0: raise f"游戏ID {self.appid} 类型不存在或无法访问。" if self.review_total is None: raise f"游戏ID {self.appid} 评论数不存在或无法访问。" if self.system_requirements['memory'] is None: raise f"游戏ID {self.appid} 系统要求不存在或无法访问。" if self.system_requirements['storage'] is None: raise f"游戏ID {self.appid} 存储要求不存在或无法访问。" if self.supported_languages is None: raise f"游戏ID {self.appid} 支持的语言不存在或无法访问。" if self.modes is None: raise f"游戏ID {self.appid} 游戏模式不存在或无法访问。" def format_system_requirements(system_requirements): """ 处理系统要求中的内存和存储信息，提取数字部分，并转换为GB单位 """ requirements_digit = re.search(r'(\d+)', system_requirements) requirements_digit = requirements_digit.group(0) if requirements_digit else None requirements_digit = int(requirements_digit) if requirements_digit else None if not 'GB' in system_requirements and requirements_digit is not None: requirements_digit = requirements_digit / 1024 return requirements_digit memory_requirement = format_system_requirements(self.system_requirements['memory']) storage_requirement = format_system_requirements(self.system_requirements['storage']) # 游戏模式识别 mutiplayer = False singleplayer = False achievements = False steam_craft = False mutiplayer_substrings = ['多人', '在线', '线上', '对战', '局域网'] for mode in self.modes: if any(substring in mode for substring in mutiplayer_substrings): mutiplayer = True if '单人' in mode or '单机' in mode: singleplayer = True if '成就' in mode: achievements = True if '创意工坊' in mode: steam_craft = True game_formatted_info = { "appid": self.appid, "game_name": self.name, "price": self.price, "tags": self.tags, "tags_count": len(self.tags), "supported_languages_count": len(self.supported_languages), "review_total_count": self.review_total, "review_positive_rate": float(self.review_positive) / self.review_total, "memory_requirement": memory_requirement, "storage_requirement": storage_requirement, "mutiplayer": mutiplayer, "singleplayer": singleplayer, "achievements": achievements, "steam_craft": steam_craft, } return game_formatted_info def __str__(self): if self.soup is None: raise ValueError("游戏信息不存在或无法访问") return str({ "appid": self.appid, "game_name": self.name, "price": self.price, "tags": self.tags, "memory_requirement": self.system_requirements['memory'], "storage_requirement": self.system_requirements['storage'], "release_date": self.release_date, "developer": self.developer, "publisher": self.publisher, "supported_systems": self.supported_systems, "modes": self.modes, "metacritic_score": self.metacritic_score, "review_total": self.review_total, "review_positive": self.review_positive, "review_negative": self.review_negative, "review_positive_rate": float(self.review_positive) / self.review_total, "system_requirements": self.system_requirements, "supported_languages": self.supported_languages }) def __eq__(self, value): if self.soup is None: raise ValueError("游戏信息不存在或无法访问") if not isinstance(value, SteamAppIDRequest): return False return self.appid == value.appid def save(self, update: bool = False, file_path: str = None): """ 将游戏信息保存到文件中 Args: update (bool): 是否覆盖已有数据而非直接追加，默认为False file_path (str): 数据文件路径，为None时使用默认路径 Returns: SteamAppIDRequest: 返回当前对象，方便链式调用 """ format_data = self.format() if file_path: self.data_IO.file_path = file_path self.data_IO.save(format_data, update) return self爬取结果爬取结果正常，收工~
摘要小时候可能玩过宠物孵化玩具，可以通过按键交互实现宠物的孵化，喂养等有趣的功能。本次实验尝试基于可编程器件实验板DTCE-EDA_V设计了电子宠物孵化器的系统构架与并用代码实现宠物孵化功能。基于Verilog实现了包含顶层模块EPEt，LCD1602，img，music，seg，rand，button在内的7大模块。关键词关键词 ：电子宠物孵化器，Verilog，FPGA，多模块。项目要求在数字实验板上的 8×8 点阵上模拟一个电子宠物“孵化器”的工作过程。基本要求SW7 为总开关，SW7=‘0’时，所有器件不显示，SW7 拨到‘1’时，8×8 点阵显示一只 黄色的“蛋”；SW0 为温度选择键，SW0=‘1’时，LD2（黄灯）亮，表示孵化器温度适合孵化， SW0=‘0’时，LD0（蓝灯）亮，表示孵化器温度偏低，无法孵化；BTN0 为孵化启动键，按下后如果温度合适（SW0=‘1’），8×8 点阵显示“蛋”的形状 每 2 秒钟发生一次变化，变化顺序如图 1~图 6；变化过程中，如果温度偏低 （SW0=‘1’）则变化停止，等到温度合适后继续变化，如果温度偏低持续时间超过 5 秒，则“蛋”变成绿色，表示孵化失败；当“蛋”的形状变化到图 6 后，孵化过程不再受温度影响，每 2 秒依次显示图 7、图 8 和图 9 动物图案，表示孵化成功；孵化出的动物有蛇、小鸡、海龟、恐龙 4 种，具体孵化结果为四种动物中的随机一 种，4 种动物图案如下：按下 BTN0 后开始计时，记录整个孵化时间，孵化成功或失败后计时停止，同时 用 DISP7~DISP6 两个数码管显示计时；孵化成功或失败后按动 BTN0 即可开始新的孵化过程。提高要求为相关状态和过程配加合适的音效或音乐；自拟其他功能。系统设计从整体上而言，整个系统解耦为七大模块。设计思路首先对整个系统进行初步构想：将系统视为黑盒，其输入输出应该包含：控制类电源开关“孵化”启动/重启按钮温度选择开关显示类声蜂鸣器光8*8双色LED点阵（孵化图案）数码管（总计孵化时间）发光LED阵列（左半部分提示进入下一阶段的时间，右半部分提示温度状态）LCD1602（文字提示）考虑到状态的切换问题，使用状态机来管理孵化进程是一个看起来很不错的解决方案。于是我们可以考虑设计一个寄存器变量pet_state，使用数字代替表示不同的孵化状态。另外，为了方便代码的调试管理，并且为未来可能的进一步开发提供灵活性和便捷性，应该将整个系统的功能代码解耦分离，分解为多个功能各异的模块。本次实验内，我将整个系统大致功能代码分类如下：从面向对象编程的设计原理来说，应该把显示逻辑从主逻辑中分离。于是我们应该分离出LCD1602，img，seg三大模块。这三者的显示都有扫描，译码，显示三大步骤。蜂鸣器的声波生成应该也要单独分离。随机数生成器可以作为一个单独的模块，当业务拓展时，仅需改变接口位宽即可满足发展的需求。按键消抖功能应该单独分离出一个模块。这样子就可以实现多按键时代码轻松复用。整个系统存在需要高频率的部分（8*8点阵，LCD1602，蜂鸣器），所以时钟频率选择10MHz。总体框图分块设计按键消抖模块button按键消抖模块的设计灵感来源于00-19计数器的实验。我们的目的是通过延迟计时的方式滤除不稳定的抖动，这种抖动一般在30ms之内。我们可以设计模块端口如下：module button( input wire btn, // 按钮 input wire CLK, // 时钟信号 input wire RST, // 复位信号 output reg btn_deb // 去抖后的按钮 );寄存器变量cnt负责CLK计数，产生30ms计时。寄存器变量btn_deb是消抖后的稳定输出寄存器变量stable_flag是按键稳定的标志，这个变量可以排除稳定后的长按信号，使得btn_deb的输出为长一个时钟的瞬时脉冲。在顶层模块EPet相关代码整合如下：wire btn0_deb; // 按钮0去抖后输出 /* 按钮去抖 */ button u0( .btn(btn0), .CLK(clk), .RST(rst), .btn_deb(btn0_deb) );button模块综合代码如下：/* 按钮去抖模块解读 */ module button( input wire btn, // 按钮 input wire CLK, // 时钟信号 input wire RST, // 复位信号 output reg btn_deb // 去抖后的按钮 ); parameter TIME_30MS = 'd300000; //30ms消抖 reg [18:0] cnt; // 计数器 reg stable_flag; // 稳定标志 always @(posedge CLK or posedge RST) begin if (RST == 1'b1) begin cnt <= 'd0; end else if (cnt == TIME_30MS) begin cnt <= 'd0; end else if (btn == 1'b1) begin cnt <= cnt + 'd1; end else begin cnt <= 'd0; end end always @(posedge CLK or posedge RST) begin if (RST == 1'b1) begin stable_flag <= 1'b0; end else if (btn == 1'b1 && cnt == TIME_30MS) begin stable_flag <= 1'b1; end else if (btn == 1'b0) begin stable_flag <= 1'b0; end end always @(posedge CLK or posedge RST) begin if (RST == 1'b1) begin btn_deb <= 1'b0; end else if (stable_flag == 1'b0 && cnt == TIME_30MS) begin btn_deb <= 1'b1; end else begin btn_deb <= 1'b0; end end endmodule数码管显示模块seg本次实验用实验板的数码管是并联在一起的，所以我们需要通过扫描的方式输出数字。设计思路同样遵循00-19计数器，不同的是待显示数字并不是先输入后分离的，而是在顶层模块EPet中就已经实现的分离（因为计时时候就是按照个位和十位分开累加的）。具体可看模块例化部分：/* 数码管显示时间 */ seg u4( .power(power), .CLK(clk), .num({seg_display_2[3:0], seg_display_1[3:0]}), //是个位和十位的拼接 .seg(seg[6:0]), .cat(seg_cat[7:0]) );综上，seg模块代码如下：/* 数码管模块解读附录 num: 四位宽一个数字，一共二位数字，8位宽 */ module seg( input wire power, // 电源开关 input wire [7:0] num, // 待显示的数字 input wire CLK, // 时钟信号 output reg [6:0] seg, // 数码管 output reg [7:0] cat // 位选 ); reg select = 'd0; // 当前显示的数码管编号 reg [3:0] bin; // 待显示的数字的二进制表示 // 扫描 always @ (posedge CLK or negedge power) begin if (power == 1'b0) begin select <= 1'b0; end else begin select <= ~select; end end always @ (*) begin if (power == 1'b0) begin // 电源关闭时，数码管全灭 cat <= 8'b1111_1111; end else begin case (select) 1'b0: cat <= 8'b1111_1110; 1'b1: cat <= 8'b1111_1101; default: cat <= 8'b1111_1111; endcase end end // 译码 always @ (*) begin // 分离显示数字 case (select) 2'b00: bin = num[3:0]; // 个位 2'b01: bin = num[7:4]; // 十位 default: bin = 4'b0000; endcase end always @ (*) begin case (bin) 4'd0: seg = 7'b1111110; // 共阴极，低电平有效 4'd1: seg = 7'b0110000; 4'd2: seg = 7'b1101101; 4'd3: seg = 7'b1111001; 4'd4: seg = 7'b0110011; 4'd5: seg = 7'b1011011; 4'd6: seg = 7'b1011111; 4'd7: seg = 7'b1110000; 4'd8: seg = 7'b1111111; 4'd9: seg = 7'b1111011; default: seg = 7'b0000000; endcase end endmodule随机数生成模块rand这里的随机数并不是严格意义上的随机数，而是伪随机数。在数电课上我们已经学过线性伪随机数序列（M序列），于是我们可以使用代码实现这一线性反馈移位寄存器（LFSR）序列。这里我们设定M序列位宽为8，反馈多项式可以任意选取，本次实验我选择的反馈函数为：$$rand_0=rand_7⊕rand_2⊕rand_1$$我们设定每个时钟刻LFSR更新一次，并调试好随机种子（RAND_SEED本次实验选用8'd0100_0010）这样我们就可以实现随机了。（虽然我们的随机种子是固定设好的，但是每次我们启动孵化的时间，孵化过程的长短都不可能严格一致，这样就导致从上电到截取随机数的这段时间内所含时钟刻随机，进而造成结果随机）综上，模块代码如下：/* 随机数生成器模块解读附录 线性反馈移位寄存器（LFSR） M序列：反馈多项式取7，2，1 */ module rand( input wire RST, // 复位信号 input wire CLK, // 时钟信号 output reg [7:0] rand // 随机数 ); parameter RAND_SEED = 8'd0100_0010; // 随机数种子 initial begin rand = RAND_SEED; end always @ (posedge CLK or posedge RST) begin if (RST == 1'b1) begin rand <= 3'd0; end else begin rand[0] <= rand[7] ^ rand[2] ^ rand[1]; rand[1] <= rand[0]; rand[2] <= rand[1]; rand[3] <= rand[2]; rand[4] <= rand[3]; rand[5] <= rand[4]; rand[6] <= rand[5]; rand[7] <= rand[6]; end end endmodule需要注意的是，rand输出是一直在随着时钟变化的。为了获得可用的随机数，我们可以任意截取某一时刻的随机数作为输出结果，虽然输出位宽为7，但是我们并不需要这么多位宽的随机数。我们可以仅仅截取8位中的2位即可（因为整个序列随机并且每个位数的01分布几率均匀，数学推导可知截取的子序列也是均匀随机的）。这一代码逻辑在顶层模块EPet中实现：// some codes were ignored rand_num[1:0] <= rand_num_current[7:6]; // 保存随机数 // ……又是一些代码 case (rand_num[1:0]) // 随机宠物 2'b00: img_index <= 'd8; // 蛇 2'b01: img_index <= 'd9; // 鸡 2'b10: img_index <= 'd10; // 龟 2'b11: img_index <= 'd11; // 龙 endcase蜂鸣器发声模块music实验板为无源蜂鸣器，所以需要我们手动进行 PWM调制 。我们以音阶C2为基准，参照表格设定中音DO-高音DO频率如下（位于顶层模块EPet中）：// 音阶定义 parameter DO = 12'd524; parameter RE = 12'd587; parameter MI = 12'd659; parameter FA = 12'd698; parameter SO = 12'd784; parameter LA = 12'd880; parameter TI = 12'd988; parameter DO_H = 12'd1047;为了使得蜂鸣器发声，我们仅需要产生相应频率的矩形波即可。由于 $T=\frac{1}{f}$ ，有计数上限：$$n_{\frac{1}{2}}=\frac{T_{note}}{2T_{CP}}=\frac{f_{CP}}{2f_{note}}$$使用计数器即可很简单地产生（不大于时钟频率的）任意频率波形（除以2是为了使得占空比为50%）。综合模块代码如下：/* 无源蜂鸣器模块解读附录 */ module music( input wire power, // 电源开关 input wire CLK, // 时钟信号 input wire RST, // 复位信号 input wire PLAY, // 播放信号 input wire [11:0] not_freq, // 目标频率 output reg beep // 蜂鸣器 ); reg [23:0] cnt = 'd0; // 分频计时器 parameter clk_freq = 'd10_000_000; always @ (posedge CLK) begin if (cnt > clk_freq / not_freq / 2) begin if (power == 1'b1 && PLAY == 1'b1) begin beep <= ~beep; end else begin beep <= 1'b0; end cnt <= 'd0; end else begin cnt <= cnt + 'd1; end end endmodulemusic模块仅仅是为了发声，其控制信号主要有两个：播放控制信号play和目标音阶频率not_freq。实际控制发声的功能逻辑写在了EPet里：/* 音乐 */ reg play = 1'd0; // 播放标志 reg [11:0] not_freq = 12'd524; // 声音频率 // 音阶定义 parameter DO = 12'd524; parameter RE = 12'd587; parameter MI = 12'd659; parameter FA = 12'd698; parameter SO = 12'd784; parameter LA = 12'd880; parameter TI = 12'd988; parameter DO_H = 12'd1047; music u5( .CLK(clk), .RST(rst), .beep(beep), .PLAY(play), .not_freq(not_freq[11:0]), .power(power) ); always @ (posedge clk) begin if (power == 1'b1) begin case (pet_state) 4'b0001: begin // 孵化中 if (cnt1s == TIME_1S) begin if (temp_sw) begin // 适宜温度 not_freq <= DO; end else begin // 冷 not_freq <= RE; end play <= ~play; end end 4'b0010: begin // 孵化完成 if (img_index < 'd8) begin // 孵化完成音乐 case (img_index) 'd5: not_freq <= LA; 'd6: not_freq <= TI; 'd7: not_freq <= DO_H; default: ; endcase if (cnt1s == TIME_1S) begin play <= ~play; end end else begin play <= 1'b0; end end 4'b0011: begin play <= 1'b0; end default: play <= 1'b0; endcase end else begin play <= 1'b0; end end8*8点阵显示模块img类似数码管，也是通过扫描的方式显示。设计亮点：使用数组的方式储存图像数据，容易修改，简单直观暴露显示接口img_index，只需要提供图片序号即可直接显示，与主逻辑解耦实际实验板红色分量远远大于绿色分量，若简单粗暴地直接设定黄色（红绿同时显示），实际效果是偏向红色有些难以区分的。为了让黄色更黄，有设置红绿分量调节常量COLOR_YELLOW_DIV，经过实践测定设置为5比较合适（绿色5份，红色1份）巧妙设置了“仅显示绿色”模式，很方便的完成了失败时的显示而不必修改原图像数据。每个图像仅需准备一份双色版本即可，大大减少了存储使用量综合模块代码如下：/* 8x8LED点阵模块解读附录 类似数码管，也是通过扫描的方式显示图像 */ module img( input wire power, // 电源开关 input wire [3:0] img_index, // 图像编号 input wire green_mode, // 仅显示绿色模式 input wire CLK, // 时钟信号 input wire RST, // 复位信号 output reg [7:0] img_row, // 图像行 output reg [7:0] img_col_red, // 图像列(红色) output reg [7:0] img_col_green // 图像列(绿色) ); reg [2:0] cnt = 'd0; // 位选计数器 reg [5:0] cnt_red = 'd0; // 绿色分量计数器 parameter COLOR_YELLOW_DIV = 'd5; // 黄色分量中绿色分量占比（绿色n份，红色1份） /* 预先设定好的图像集 第一个 [63:0] 数据宽度 8*8*2 = 128 第二个 [1:0] 图像编号 */ reg [127:0] image [0:11]; // 初始化 initial begin image [0] <= { // 蛋0 { { //绿灯 8'b0000_0000, 8'b0000_0000, 8'b0001_1000, 8'b0011_1100, 8'b0011_1100, 8'b0001_1000, 8'b0000_0000, 8'b0000_0000 }, { // 红灯 8'b0000_0000, 8'b0000_0000, 8'b0001_1000, 8'b0011_1100, 8'b0011_1100, 8'b0001_1000, 8'b0000_0000, 8'b0000_0000 } } }; // 这里省略了其他图像的数据…… end // 扫描 always @ (posedge CLK or posedge RST) begin if (RST == 1'b1) begin cnt <= 3'b000; end else begin if (power == 1'b1) begin if (cnt == 3'b111) begin cnt <= 3'b000; // 黄色占空比调节 if (cnt_red == COLOR_YELLOW_DIV) begin cnt_red <= 'd0; end else begin cnt_red <= cnt_red + 'd1; end end else begin cnt <= cnt + 3'b001; end end else begin cnt <= 3'b000; end end end // 显示（行的控制） always @ (posedge CLK) begin if (power == 1'b1) begin case (cnt) 3'b000: img_row <= 8'b1111_1110; 3'b001: img_row <= 8'b1111_1101; 3'b010: img_row <= 8'b1111_1011; 3'b011: img_row <= 8'b1111_0111; 3'b100: img_row <= 8'b1110_1111; 3'b101: img_row <= 8'b1101_1111; 3'b110: img_row <= 8'b1011_1111; 3'b111: img_row <= 8'b0111_1111; endcase end else begin img_row <= 8'b1111_1111; end end // 显示（列的控制） always @ (posedge CLK) begin case (cnt) 3'b000: begin img_col_red <= image[img_index][7:0]; img_col_green <= image[img_index][71:64]; end 3'b001: begin img_col_red <= image[img_index][15:8]; img_col_green <= image[img_index][79:72]; end 3'b010: begin img_col_red <= image[img_index][23:16]; img_col_green <= image[img_index][87:80]; end 3'b011: begin img_col_red <= image[img_index][31:24]; img_col_green <= image[img_index][95:88]; end 3'b100: begin img_col_red <= image[img_index][39:32]; img_col_green <= image[img_index][103:96]; end 3'b101: begin img_col_red <= image[img_index][47:40]; img_col_green <= image[img_index][111:104]; end 3'b110: begin img_col_red <= image[img_index][55:48]; img_col_green <= image[img_index][119:112]; end 3'b111: begin img_col_red <= image[img_index][63:56]; img_col_green <= image[img_index][127:120]; end endcase // 仅显示绿色模式 if (green_mode == 1'b1) begin img_col_red <= 8'b0000_0000; end else begin // 黄色占空比调节 if (cnt_red != COLOR_YELLOW_DIV) begin img_col_red <= 8'b0000_0000; end else begin img_col_green <= 8'b0000_0000; end end end endmoduleLCD1602显示模块LCD1602管脚如下：其中我们仅仅需要注意数据脚，RS脚和R/W脚（本次实验我们并不需要从LCD读，所以R/W可以直接固定为0）。LCD1602储存有以下三种：DDRAM:显示数据RAM（可写入）CGRAM:用户可自定义的字模（可写入，00H-0FH）CGROM:内置的常用字模（不可写入,ASCII: 20H-7FH, 日文、希腊:A0H-FFH）上面是LCD1602的指令集。可见，这里也可以通过状态机的方式控制LCD1602.为了适配任意时钟，将周期设置为参数TIME_20MS,TIME_500HZ方便例化。综合代码如下：/* LCD1602显示屏模块解读附录 DDRAM:显示数据RAM（可写入） CGRAM:用户可自定义的字模（可写入，00H-0FH） CGROM:内置的常用字模（不可写入,ASCII: 20H-7FH, 日文、希腊:A0H-FFH） */ module LCD1602( // 1602液晶显示屏模块 input wire [127:0] row_1, // row_1[127:0] 为第一行显示内容 input wire [127:0] row_2, // row_2[127:0] 为第二行显示内容 input wire CLK, // 时钟信号 input wire RST, // 复位信号 output reg LCD_E, // 使能端,当其为下降沿时执行命令 output reg LCD_RS, // 数据/指令选择端,为0时输入指令,为1时输入数据 output reg [7:0] LCD_DATA, // 数据输出端(DB7-DB0) output reg LCD_RW // 读写选择端,为0时写入,为1时读取 ); // 初始化 initial begin LCD_RW <= 1'b0; //读写选择端设置为始终写入 end // 时钟CLK频率：10MHz = 10^-7s = 10^-4ms parameter TIME_20MS= 'd20_0000; //需要20ms上电稳定(初始化) reg [17:0] cnt_20ms; reg delay_done = 1'b0; parameter TIME_500HZ = 'd20000; //工作周期 reg [14:0] cnt_500hz; reg write_flag; //状态机有40种状态,此处用了格雷码,一次只有一位变化(在二进制下) parameter IDLE=8'h00; parameter SET_FUNCTION=8'h01; parameter DISP_OFF=8'h03; parameter DISP_CLEAR=8'h02; parameter ENTRY_MODE=8'h06; parameter DISP_ON=8'h07; parameter ROW1_ADDR=8'h05; parameter ROW1_0=8'h04; parameter ROW1_1=8'h0C; parameter ROW1_2=8'h0D; parameter ROW1_3=8'h0F; parameter ROW1_4=8'h0E; parameter ROW1_5=8'h0A; parameter ROW1_6=8'h0B; parameter ROW1_7=8'h09; parameter ROW1_8=8'h08; parameter ROW1_9=8'h18; parameter ROW1_A=8'h19; parameter ROW1_B=8'h1B; parameter ROW1_C=8'h1A; parameter ROW1_D=8'h1E; parameter ROW1_E=8'h1F; parameter ROW1_F=8'h1D; parameter ROW2_ADDR=8'h1C; parameter ROW2_0=8'h14; parameter ROW2_1=8'h15; parameter ROW2_2=8'h17; parameter ROW2_3=8'h16; parameter ROW2_4=8'h12; parameter ROW2_5=8'h13; parameter ROW2_6=8'h11; parameter ROW2_7=8'h10; parameter ROW2_8=8'h30; parameter ROW2_9=8'h31; parameter ROW2_A=8'h33; parameter ROW2_B=8'h32; parameter ROW2_C=8'h36; parameter ROW2_D=8'h37; parameter ROW2_E=8'h35; parameter ROW2_F=8'h34; reg [5:0] c_state; //当前状态 reg [5:0] n_state; //下一状态 // 初始化，上电稳定 always @ (posedge CLK or posedge RST) begin if (RST == 1'b1) begin cnt_20ms <= 1'b0; //复位 end else if (cnt_20ms == TIME_20MS) begin delay_done <= 1'b1; //到达20ms时置1 end else begin cnt_20ms <= cnt_20ms + 1'b1; //未到达20ms时加1 end end // 工作周期分频（LCD1602工作周期500Hz） always @ (posedge CLK or posedge RST) begin if (RST == 1'b1) begin cnt_500hz <= 1'b0; end else if( delay_done == 1'b1 ) begin if(cnt_500hz == TIME_500HZ) begin cnt_500hz <= 1'b0; end else begin cnt_500hz <= cnt_500hz + 1'b1; end end else begin cnt_500hz <= 1'b0; end end always @ (posedge CLK or posedge RST) begin //使能端,每个工作周期一次下降沿,执行一次命令（/2是为了避免在数据写入未完成时就执行指令） if (RST == 1'b1) begin LCD_E <= 1'b0; end else if (cnt_500hz < TIME_500HZ/2) begin LCD_E <= 1'b1; end else begin LCD_E <= 1'b0; end end always @ (posedge CLK or posedge RST) begin //每到一个工作周期,write_flag置高一周期 if (RST == 1'b1) begin write_flag <= 1'b0; end else if (cnt_500hz == TIME_500HZ) begin write_flag <= 1'b1; end else begin write_flag <= 1'b0; end end // 状态机 //state 指令集对应：RS, R/W, DB7, DB6, DB5, DB4, DB3, DB2, DB1, DB0 always @ (posedge CLK or posedge RST) begin if(RST == 1'b1) begin c_state <= IDLE; end else if(write_flag) begin //每一个工作周期改变一次状态 c_state <= n_state; end end always @ (*) begin case (c_state) //循环进行扫描显示 IDLE:n_state=SET_FUNCTION; SET_FUNCTION:n_state=DISP_OFF; DISP_OFF:n_state=DISP_CLEAR; DISP_CLEAR:n_state=ENTRY_MODE; ENTRY_MODE:n_state=DISP_ON; DISP_ON:n_state=ROW1_ADDR; ROW1_ADDR:n_state=ROW1_0; ROW1_0:n_state=ROW1_1; ROW1_1:n_state=ROW1_2; ROW1_2:n_state=ROW1_3; ROW1_3:n_state=ROW1_4; ROW1_4:n_state=ROW1_5; ROW1_5:n_state=ROW1_6; ROW1_6:n_state=ROW1_7; ROW1_7:n_state=ROW1_8; ROW1_8:n_state=ROW1_9; ROW1_9:n_state=ROW1_A; ROW1_A:n_state=ROW1_B; ROW1_B:n_state=ROW1_C; ROW1_C:n_state=ROW1_D; ROW1_D:n_state=ROW1_E; ROW1_E:n_state=ROW1_F; ROW1_F:n_state=ROW2_ADDR; ROW2_ADDR:n_state=ROW2_0; ROW2_0:n_state=ROW2_1; ROW2_1:n_state=ROW2_2; ROW2_2:n_state=ROW2_3; ROW2_3:n_state=ROW2_4; ROW2_4:n_state=ROW2_5; ROW2_5:n_state=ROW2_6; ROW2_6:n_state=ROW2_7; ROW2_7:n_state=ROW2_8; ROW2_8:n_state=ROW2_9; ROW2_9:n_state=ROW2_A; ROW2_A:n_state=ROW2_B; ROW2_B:n_state=ROW2_C; ROW2_C:n_state=ROW2_D; ROW2_D:n_state=ROW2_E; ROW2_E:n_state=ROW2_F; ROW2_F:n_state=ROW1_ADDR; default:; endcase end // RS端控制 always @ (posedge CLK or posedge RST) begin if(RST == 1'b1) begin LCD_RS <= 1'b0; end else if(write_flag == 1'b1) begin //当状态为七个指令任意一个,将RS置为指令输入状态 if( (n_state == SET_FUNCTION) || (n_state == DISP_OFF) || (n_state == DISP_CLEAR) || (n_state == ENTRY_MODE) || (n_state == DISP_ON) || (n_state == ROW1_ADDR) || (n_state == ROW2_ADDR) ) begin LCD_RS<=1'b0; //为0时输入指令,为1时输入数据 end else begin LCD_RS<=1'b1; end end end // 显示控制 always @ (posedge CLK or posedge RST) begin if(RST == 1'b1) begin LCD_DATA <= 1'b0; end else if(write_flag == 1'b1) begin case (n_state) // x态：信号数值不确定 // z态：高阻态 IDLE:LCD_DATA<=8'hxx; //8'b0011_1000,工作方式设置:DL=1(DB4,8位数据接口),N=1(DB3,两行显示),L=0(DB2,5x8点阵显示). SET_FUNCTION:LCD_DATA<=8'h38; //8'b0000_1000,显示开关设置:D=0(DB2,显示关),C=0(DB1,光标不显示),D=0(DB0,光标不闪烁) DISP_OFF:LCD_DATA<=8'h08; //8'b0000_0001,清屏 DISP_CLEAR:LCD_DATA<=8'h01; //8'b0000_0110,进入模式设置:I/D=1(DB1,写入新数据光标右移),S=0(DB0,显示不移动) ENTRY_MODE:LCD_DATA<=8'h06; //8'b0000_1100,显示开关设置:D=1(DB2,显示开),C=0(DB1,光标不显示),D=0(DB0,光标不闪烁) DISP_ON:LCD_DATA<=8'h0c; //8'b1000_0000,设置DDRAM地址:00H->1-1,第一行第一位 ROW1_ADDR:LCD_DATA<=8'h80; //将输入的row以每8-bit拆分,分配给对应的显示位 ROW1_0:LCD_DATA<=row_1[127:120]; ROW1_1:LCD_DATA<=row_1[119:112]; ROW1_2:LCD_DATA<=row_1[111:104]; ROW1_3:LCD_DATA<=row_1[103: 96]; ROW1_4:LCD_DATA<=row_1[ 95: 88]; ROW1_5:LCD_DATA<=row_1[ 87: 80]; ROW1_6:LCD_DATA<=row_1[ 79: 72]; ROW1_7:LCD_DATA<=row_1[ 71: 64]; ROW1_8:LCD_DATA<=row_1[ 63: 56]; ROW1_9:LCD_DATA<=row_1[ 55: 48]; ROW1_A:LCD_DATA<=row_1[ 47: 40]; ROW1_B:LCD_DATA<=row_1[ 39: 32]; ROW1_C:LCD_DATA<=row_1[ 31: 24]; ROW1_D:LCD_DATA<=row_1[ 23: 16]; ROW1_E:LCD_DATA<=row_1[ 15: 8]; ROW1_F:LCD_DATA<=row_1[ 7: 0]; ROW2_ADDR:LCD_DATA<=8'hc0;//8'b1100_0000,设置DDRAM地址:40H->2-1,第二行第一位 ROW2_0:LCD_DATA<=row_2[127:120]; ROW2_1:LCD_DATA<=row_2[119:112]; ROW2_2:LCD_DATA<=row_2[111:104]; ROW2_3:LCD_DATA<=row_2[103: 96]; ROW2_4:LCD_DATA<=row_2[ 95: 88]; ROW2_5:LCD_DATA<=row_2[ 87: 80]; ROW2_6:LCD_DATA<=row_2[ 79: 72]; ROW2_7:LCD_DATA<=row_2[ 71: 64]; ROW2_8:LCD_DATA<=row_2[ 63: 56]; ROW2_9:LCD_DATA<=row_2[ 55: 48]; ROW2_A:LCD_DATA<=row_2[ 47: 40]; ROW2_B:LCD_DATA<=row_2[ 39: 32]; ROW2_C:LCD_DATA<=row_2[ 31: 24]; ROW2_D:LCD_DATA<=row_2[ 23: 16]; ROW2_E:LCD_DATA<=row_2[ 15: 8]; ROW2_F:LCD_DATA<=row_2[ 7: 0]; endcase end end endmodule经过封装后很方便使用，在EPet中简单设置一个状态机即可更新显示内容：/* LCD1602显示 */ reg [127:0] row_1; // 第一行显示内容 reg [127:0] row_2; // 第二行显示内容 reg lcd_rst; // LCD1602刷新信号 LCD1602 u1( .row_1(row_1[127:0]), .row_2(row_2[127:0]), .CLK(clk), .RST(lcd_rst), .LCD_E(LCD_E), .LCD_RS(LCD_RS), .LCD_DATA(LCD_DATA), .LCD_RW(LCD_RW) ); // LCD1602内容刷新 always @ (posedge clk) begin if (power == 1'b0) begin row_1 <= "*- POWER OFF -*"; row_2 <= "-=> Switch 7 <=-"; end else begin case (pet_state) 4'b0000: begin // 孵化未开始 row_1 <= "*- POWER ON -*"; row_2 <= "-=> Button 0 <=-"; end 4'b0001: begin // 孵化中 if (temp_sw) begin // 正常温度 row_1 <= ">- Hatching -<"; row_2 <= "-=> WARM <=-"; end else begin // 异常温度 row_1 <= ">- Hatching -<"; row_2 <= "-=> FROZEN <=-"; end end 4'b0010: begin // 孵化完成 row_1 <= ">- Hatching -<"; row_2 <= "-=> Success! <=-"; end 4'b0011: begin // 孵化失败 row_1 <= ">- Hatching -<"; row_2 <= "-=> Failed <=-"; end default: ; endcase end end顶层模块EPet整个顶层模块的核心在于状态机的设置。首先是设置了一个全局状态变量：reg [3:0] pet_state; // 宠物状态 /* 宠物状态说明 0: 孵化未开始 1: 孵化中 2: 孵化完成 3: 孵化失败 */另一个重点的状态转移：/* 宠物状态机 */ always @ (posedge clk) begin if (power == 1'b1) begin case (pet_state) 4'b0000: begin // 孵化未开始 if (btn0_deb == 1'b1) begin pet_state <= 4'b0001; // 孵化中 end end 4'b0001: begin // 孵化中 if (hatch_state_flag == 1'b1) begin hatch_state_flag <= 1'b0; // 重置孵化结束标志 end img_green_mode <= 1'b0; // 显示彩色模式 if (img_index == 'd5) begin pet_state <= 4'b0010; // 孵化完成 end else if (temp_sw == 1'b0 && hatch_cnt == TIME_FROZEN_DEAD) begin pet_state <= 4'b0011; // 孵化失败 end end 4'b0010: begin // 孵化完成（包含蛋裂动画） if (btn0_deb == 1'b1 && img_index > 'd7) begin hatch_state_flag <= 1'b1; pet_state <= 4'b0001; // 孵化中 end end 4'b0011: begin // 孵化失败 img_green_mode <= 1'b1; // 仅显示绿色模式 if (btn0_deb == 1'b1) begin hatch_state_flag <= 1'b1; pet_state <= 4'b0001; // 孵化中 end end default: ; endcase end else begin pet_state <= 4'b0000; // 孵化未开始 hatch_state_flag <= 1'b1; end end孵化中的计时为了避免多驱动的错误，和img显示写在一起：// 孵化中计时 parameter TIME_NEXT_IMG = 'd2; parameter TIME_FROZEN_DEAD = 'd5; reg [23:0] cnt1s = 'd0; // 1s计数器 reg [2:0] hatch_cnt = 'd0; // 孵化计数器 reg [3:0] seg_display_1 = 'd0; // 显示计数器（个位） reg [3:0] seg_display_2 = 'd0; // 显示计数器（十位） reg temp_sw_flag = 1'b0; // 温度切换标志（1：正常温度/0：异常温度） reg hatch_state_flag = 1'b0; // 孵化状态切换标志 always @ (posedge clk) begin // 1s分频 if (power == 1'b1) begin if (hatch_state_flag == 1'b1) begin cnt1s <= 'd0; // 重置1s计数器 end if (cnt1s == TIME_1S) begin cnt1s <= 'd0; end else begin cnt1s <= cnt1s + 'd1; end end else begin cnt1s <= 'd0; // 重置1s计数器 end end always @ (posedge clk) begin // 显示计时 if (power == 1'b1 && pet_state == 4'b0001) begin // 电源 + 孵化中 if (hatch_state_flag == 1'b1) begin seg_display_1 <= 'd0; // 重置显示计时器 seg_display_2 <= 'd0; end if (cnt1s == TIME_1S) begin if (seg_display_1 == 'd9) begin seg_display_1 <= 'd0; seg_display_2 <= seg_display_2 + 'd1; end else begin seg_display_1 <= seg_display_1 + 'd1; end end end else if (power == 1'b0) begin // 关机 seg_display_1 <= 'd0; // 重置显示计时器 seg_display_2 <= 'd0; end end always @ (posedge clk) begin // 8*8LED点阵显示 if (power == 1'b1) begin // 开机 case (pet_state) 4'b0001: begin // 孵化中 if (hatch_state_flag == 1'b1) begin hatch_cnt <= 'd0; // 重置孵化计时器 img_index <= 'd0; // 重置图像编号 end if (temp_sw) begin // 切换温度时重置计时器 if (temp_sw_flag == 1'b0) begin temp_sw_flag <= 1'b1; hatch_cnt <= 'd0; end end else begin if (temp_sw_flag == 1'b1) begin temp_sw_flag <= 1'b0; hatch_cnt <= 'd0; end end if (cnt1s == TIME_1S) begin if (temp_sw) begin // 正常温度的处理 if (hatch_cnt == TIME_NEXT_IMG - 'd1) begin hatch_cnt <= 'd0; img_index <= img_index + 'd1; end else begin hatch_cnt <= hatch_cnt + 'd1; end end else begin // 异常温度的处理 if (hatch_cnt < TIME_FROZEN_DEAD) begin hatch_cnt <= hatch_cnt + 'd1; end end end end 4'b0010: begin // 孵化完成 if (img_index < 'd8) begin // 蛋裂动画播放 if (cnt1s == TIME_1S) begin if (hatch_cnt == TIME_NEXT_IMG - 'd1) begin hatch_cnt <= 'd0; img_index <= img_index + 'd1; end else begin hatch_cnt <= hatch_cnt + 'd1; end end rand_num[1:0] <= rand_num_current[7:6]; // 保存随机数 end else begin case (rand_num[1:0]) // 随机宠物 2'b00: img_index <= 'd8; // 蛇 2'b01: img_index <= 'd9; // 鸡 2'b10: img_index <= 'd10; // 龟 2'b11: img_index <= 'd11; // 龙 endcase end end 4'b0011: begin // 孵化失败 hatch_cnt <= 'd0; // 重置孵化计时器 end endcase end else begin // 关机 hatch_cnt <= 'd0; // 重置孵化计时器 img_index <= 'd0; // 重置图像编号 end end温度/时间提示以及完整的代码详见附录。仿真波形及波形分析[quote color="info"]原始报告含各个仿真波形的分析，这里偷个懒就不放了。[/quote]故障及问题分析[quote color="info"]太长了，偷懒不放了OwO[/quote]总结和结论本次实验丰富了我的硬件设计经历。了解了很多器件的原理并学会了读说明书/使用手册。这次实验让我运用了很多编程设计的技巧和思想，并且让我意识到了很多以前没有意识到的知识漏洞，提升了知行合一的学科素养。此外，本次实验让我进一步掌握了Quartus的使用方法，了解了ModelSim的仿真文件编写方式。而由于Quartus本身的可视化做的很糟糕，我还学习了如何在vscode下比较优雅地编辑verilog，安装了CTags并排除了ModelSim原先的安装授权问题。这些经历也锻炼了我学习并接收新事物的能力。附录完整代码[quote color="info"]太长了，偷懒不放了OwO[/quote]参考资料[quote color="info"]原报告有，此略[/quote]
前言最近在北邮沉迷学习数电，第一个验收的实验是实现循环自动计数的计数器。就这次实验写一个小结回顾，也希望能给后来人启发。实验要求完成一个计数值为00~19的计数器的设计和仿真，并下载到实验板上验证。要求如下：计数值每秒加1，加到19后回0BTN0 为暂停键，按一下计时停止，再按一下计时继续，要求为 BTN0 设计防抖电路在数码管 DISP1 和 DISP0 上显示计数结果BTN7 为复位键，无论何时按下计数值都回到 00实验板上时钟选择 100Hz系统分析输入输出端口设计首先，我们先设计系统的输入和输出。输入显然有两个：暂停按钮 btn0 和复位按钮 reset。输出是数码管，但是我们发现数码管共用一个输入端口，所以为了实现多位数字显示，我们需要用 扫描 的方式显示。于是我们定义输出为 seg ，位选为 cat。我们可以定义整个顶层模块如下：module segCounter ( input wire clk, input wire btn0, reset, output reg [6:0] seg, output reg [7:0] cat );系统模块设计按键消抖模块由于按键动作的时刻和按下时间长短随机，并且存在开关簧片反弹导致电平抖动，我们需要设计一个按键消抖模块。一般抖动时间小于 20ms ，所以我们只需要检测高电平持续的长度大于20ms（实验里取30ms为检测时间）时再置高电平即可。可见，btn0 是原始的按键输入波形， cnt_3 的作用是基于 clk 实现计数，每当 clk 到上升沿时，若高电平则+1，计数到3后则 stable_flag 置高电平，同时 press 产生一个周期的高电平。cnt_3 为什么要计数到3呢？因为 clk 频率是 100Hz ，频率是周期的倒数，所以简单换算如下：$$ f=\frac{1}{T} \Rightarrow T=\frac{1}{f}=\frac{1}{100}s=10ms $$每隔10ms clk 来一个上升沿， cnt_3 +1，则当 cnt_3 为3时显然时间就过去了30ms。消抖模块对应代码如下： // 消抖 always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin cnt_3 <= 3'b000; end else if (cnt_3 == 3'b011) begin cnt_3 <= 3'b000; end else if (btn0 == 1'b1) begin cnt_3 <= cnt_3 + 3'b001; end else begin cnt_3 <= 3'b000; end end always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin stable_flag <= 1'b0; end else if (btn0 == 1'b1 && cnt_3 == 'd3) begin stable_flag <= 1'b1; end else if (btn0 == 1'b0) begin stable_flag <= 1'b0; end end always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin press <= 1'b0; end else if (stable_flag == 1'b0 && cnt_3 == 'd3) begin press <= 1'b1; end else begin press <= 1'b0; end end复位功能设计我们采用异步复位（ posedge clk or posedge reset 里面用 or ），当reset按下时就立刻执行复位操作。因为复位功能显然优先级最高，所以所有 always 块内我们都先验证 reset 的状态。数码管的显示如图，要使得共阴极数码管点亮，需要改变的是阳极的状态。而阳极是多个数码管共用的，所以我们借助人 视觉暂留 的特点，用扫描的方式显示所有数字（类似显示屏的原理）。可见，我们使用 select 区分当前显示的是十位还是个位。译码器负责转换数字到数码管阳极状态的映射关系，扫描控制电路负责控制不同位数数码管的点亮与否。代码实现最后，整个代码如下：module segCounter ( input wire clk, input wire btn0, reset, output reg [6:0] seg, output reg [7:0] cat ); reg [2:0] cnt_3; reg [6:0] cnt_100; reg stable_flag, press, ctr, select; reg [3:0] cnt1; reg [3:0] cnt2; reg [3:0] bin; // 消抖 always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin cnt_3 <= 3'b000; end else if (cnt_3 == 3'b011) begin cnt_3 <= 3'b000; end else if (btn0 == 1'b1) begin cnt_3 <= cnt_3 + 3'b001; end else begin cnt_3 <= 3'b000; end end always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin stable_flag <= 1'b0; end else if (btn0 == 1'b1 && cnt_3 == 'd3) begin stable_flag <= 1'b1; end else if (btn0 == 1'b0) begin stable_flag <= 1'b0; end end always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin press <= 1'b0; end else if (stable_flag == 1'b0 && cnt_3 == 'd3) begin press <= 1'b1; end else begin press <= 1'b0; end end // 暂停标志 always @(posedge press or posedge reset) begin if (reset == 1'b1) begin ctr <= 1'b0; end else begin ctr <= ~ctr; end end // 时钟倍频（cnt_100 clk计数） always @(posedge clk or posedge reset) begin if (reset == 1'b1) begin cnt_100 <= 7'b000_0000; end else if (cnt_100 == 7'b110_0100 && ctr == 1'b0) begin cnt_100 <= 7'b000_0000; end else if (ctr == 1'b0) begin cnt_100 <= cnt_100 + 7'b000_0001; end end // 显示（cnt1 个位，cnt2 十位） always @(posedge clk, posedge reset) begin if (reset == 1'b1) begin cnt1 <= 0; cnt2 <= 0; end else if (cnt_100 == 7'b110_0100 && ctr == 1'b0) begin if (cnt1 == 4'b1001) begin cnt1 <= 0; if (cnt2 == 0) cnt2 <= cnt2 + 1; else cnt2 <= 0; end else begin cnt1 <= cnt1 + 1; end end end // 扫描 always @(posedge clk, posedge reset) begin if (reset == 1'b1) begin cat <= 8'b1111_1111; bin <= 0; end else begin select <= ~select; if (select == 1'b0) begin cat <= 8'b1111_1110; bin <= cnt1; end else begin cat <= 8'b1111_1101; bin <= cnt2; end end end // 译码 always @(bin) begin case (bin) 4'd0: seg = 7'b1111110; 4'd1: seg = 7'b0110000; 4'd2: seg = 7'b1101101; 4'd3: seg = 7'b1111001; 4'd4: seg = 7'b0110011; 4'd5: seg = 7'b1011011; 4'd6: seg = 7'b1011111; 4'd7: seg = 7'b1110000; 4'd8: seg = 7'b1111111; 4'd9: seg = 7'b1111011; default: seg = 7'b0000000; endcase end endmodule 实验用板如图，引脚对应如下。
前言最近正在尝试使用 Obsidian 这款开源免费的笔记应用。优点在于Wiki化管理和可视化的知识联系图。但是，他本身没有联网同步功能！！！ :@(无奈) 每次在ipad上写笔记，要是都要复制一份到电脑上，那也太麻烦了。不过搜索发现，Obsidian本身丰富的第三方社区插件能帮助解决这个问题。啊那很激动哇，正好自己有服务器，就动动手就能实现多端同步了。前期准备Obsidian服务器（非必须，只要支持 WebDav 读写服务即可）重点在于 WebDav 服务，看情况可能要GitHub加速器，因为很多社区插件都托管在GitHub上，网络问题可能导致下载安装插件失败。搭建WevDav服务这里以 Cloudreve 演示。在服务器安装并启用Cloudreve让我埋个坑，后面开篇文章写XwX在Cloudreve启用WebDav首先，首页左侧菜单点击 “连接” 。接下来，在云盘创建单独的文件夹存放笔记仓库，并点击 “创建新账号” 。设置Obsidian安装插件Remotely Save打开Obsidian，新建仓库，左下角齿轮图标点击进入设置界面。[tip type="warning"]设置是仓库隔离的。若想继承其他仓库的设置，可以将其他仓库根文件夹下的 .obsidian文件夹复制到该仓库。 [/tip]左侧菜单选择 “第三方插件” ，关闭 安全模式 。打开 社区插件市场 ，搜索 Remotely Save 。安装。设置插件Remotely Save安装完成后，启用插件，并转到插件设置页。设置WebDav登录信息。[tip type="info"]强烈建议设置 自动运行 和 启动后运行 。 [/tip]大功告成！ :@(得意)
[tip type="warning"]本指南不保证时效性，实际操作可能因相关软件升级而更新，请以官方文档为准。[/tip]前言当项目体量变大，从个人到团体的开发模式势必会有极大的更改，而Git作为代码开发的管理工具，目前已被许多大厂和开发者采用。下面让我们从白嫖到入门，了解如何使用Git，并充分利用GitHub平台管理我们的代码吧。要点Git工具的安装GitHub注册与教育优惠基本的Git指令仓库管理与基本操作GitHub Page：来点开发文档吧GitHub Action：CI自动化与人类解放剩下的东西……Section 1: Git工具的安装（仅以Windows为例）官方参考：https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git进入Git官网，点选右侧Download for Windows点击Click here to download获取安装包后，（以管理员方式）运行，选择安装路径，安装即可。安装完成后检查 Git Bash 是否已安装。接下来打开cmd（Win键+R键），输入指令git -v，若出现版本信息即为安装成功。Section 2: GitHub注册GitHub Docs（中文）：https://docs.github.com/zh[DSUCCESS title="提示" font="darkgreen" bg="r" border="darkgreen"]由于地理位置，网络环境等复杂原因，GitHub经常出现掉线问题。请选择合适的加速器或者使用魔法访问。[/DSUCCESS]首先，我们需要打开GitHub官网【左Ctrl按住点选可以自动在新建页面打开哦】右上角有两个选项，若已有账户请选择Sign in，否则请选择Sign up创建账户。按照提示完善个人信息（邮箱，用户名，密码等等）之后，我们会来到个人界面。右上角点击头像，弹出右菜单。其中，Your profile是个人首页。但是我们先不打开这个，而是选择SettingsSection 3: 白嫖教育优惠（以北京邮电大学为例）北邮关于邮箱注册的说明：https://nic.bupt.edu.cn/info/1013/1482.htm首先申请学校邮箱，你会得到以@bupt.edu.cn结尾的邮箱继续回到GitHub的Settings界面，点选Emails添加你的学校邮箱，通过验证码激活。接下来打开教务系统，导出在读证明。你的邮箱会收到一份pdf，打开，查看自己的英文名。在设置里修改自己的姓名【不能少一个字母，也不能变换大小写】GitHub打开申请页面，这时候要【关闭】加速器或者魔法（他会获取你的位置，点同意），选择【Student】，填写表单。[tip type="error"]有些电脑无法获取位置的解决办法：https://zhuanlan.zhihu.com/p/643398611?utm_id=0，北邮的地理位置：东经:116.358103、北纬:39.961554[/tip]接下来会要求你提供证明，要求拍照，这时候你用手机打开那个PDF文件，让拍摄到的画面大致是这样子。提交，等待审核通过。【通过会收到邮件提示，而且会有pro的权限在Highlights】Section 4: Git的基本入门菜鸟教程-Git入门：https://www.runoob.com/git/git-tutorial.html更简明的教程：https://www.runoob.com/manual/git-guide/
视频效果[video src="https://www.nickwald.top/usr/uploads/2023/02/3165716385.mp4" loop="loop" poster="https://www.nickwald.top/usr/uploads/2023/02/607553714.png" height="" width=""]代码一览from manim import * class Beginning(Scene): def construct(self): title = Text("胡说数学",font="ipix_12px",font_size=DEFAULT_FONT_SIZE*2) subtitle_vol = Text("第一集",font="ipix_12px") subtitle = Text("从数羊开始的数字",font="ipix_12px") author = Text("By Nickwald",font="ipix_12px") l = Line(UP,DOWN) subtitle_vol.next_to(title,RIGHT+DOWN*0.8,buff=-2) subtitle.next_to(title,RIGHT+UP*0.7,buff=-2) l.next_to(title,RIGHT,buff=-2.5) author.next_to(l,DOWN,buff=LARGE_BUFF) titleWriteIn = [ Write(subtitle_vol), Write(subtitle) ] titleFadeOut = [ FadeOut(title), FadeOut(subtitle_vol), FadeOut(subtitle), FadeOut(l), FadeOut(author) ] self.play(Write(title)) self.play(title.animate.shift(LEFT*3)) self.play(Create(l)) self.play(AnimationGroup(*titleWriteIn)) self.play(Write(author)) self.wait() self.play(AnimationGroup(*titleFadeOut)) self.wait()代码讲解固定格式首先，使用manim制作视频时，都需要在开头添加manim库：from manim import *接下来，我们便需要创建一个场景：class YourSceneName(Scene): def construct(self):需要注意的是，一个代码文件中可以存在多个场景，看起来可能是这样的：class SceneName1(Scene): def construct(self): //Your code here... class SceneName2(Scene): def construct(self): //Your code here...如果有多个场景，在使用manimCE渲染时，需要你选择要渲染的场景。一般文字(Text对象)[tip type='info']manimCE中的Text对象说明：说明文档 [/tip]注意到之前的代码中有这样一行：title = Text("Section 1:从数羊开始……",font="ipix_12px",font_size=30).to_corner(UL)这行代码做了两件事：创建Text对象设置Text位置我们逐个分析。[tabs][item title="创建Text对象"]首先，我们创建了Text对象，并将其赋给 title变量。下面是一些对Text对象的说明。# 一般用法 varName = Text("Some text here...")事实上，manim提供了极高的定义属性支持，完整声明如下：class Text(text, fill_opacity=1.0, stroke_width=0, *, color='#FFFFFF', font_size=48, line_spacing=- 1, font='', slant='NORMAL', weight='NORMAL', t2c=None, t2f=None, t2g=None, t2s=None, t2w=None, gradient=None, tab_width=4, height=None, width=None, should_center=True, disable_ligatures=False, **kwargs)我们仅仅就其中几个最常用的属性进行说明。color：颜色，支持诸如RED，YELLOW，GREEN等内置颜色变量，也支持#号开头的颜色代码（需要单引号）。font：字体，需要查看自己系统已安装的字体font_size：字号- weight=BOLD：加粗- slant=ITALIC：斜体更高级的玩法：事实上，我们可以对一串文本中的部分文字做自定义。t2c/t2s/t2w：部分设置颜色/斜体/字重，我们用字典的方式对多个部分自定义。比如：text = Text("Hello world", t2c={'o':YELLOW, 'H':BLUE}) text = Text("Hello world", t2w={'world':BOLD}) text = Text("Hello world", t2s={'Hello':ITALIC})Text对象使用Pango渲染，支持除了中英文之外的其他语言（包括日语，泰语，蒙古语等等）Text对象也同时支持制表符，比如要实现多行文本，不必定义多个Text对象，仅仅需要插入换行符\n即可：text = Text("这是第一行 \n 第二行在这")[/item][item title="设置Text位置"]事实上，几乎所有的对象都可以通过各种不同的方法来改变自己的位置。一般使用方法的模式为Object.method1.method2.……，在对象后面用.连接多个方法。一个具体的例子为：text1 = Text("1") # 你可以这样子用： text2 = Text("2").to_corner(UR) # 也可以这样做： text2.next_to(text1, DOWN) # 还可以同时使用多个方法： text1.to_corner(UL).shift(DOWN*3)包括缩放scale()等等方法都是类似的使用方式。[/item][/tabs]线l = Line(UP,DOWN)这里创建了一个线的对象，但是，为什么是UP和DOWN呢？[tip type='info']如果有一定线性代数基础，那么我们容易知道，变换和坐标都可以通过数学上的矩阵去描述。[/tip]事实上，manim中的UP，DOWN等常量本质上就是一个矩阵，所以上文中的DOWN*3也就不难理解了。另一种使用Line的方式为：dot1 = Dot(point = UL) dot2 = Dot(point = DR) l = Line(dot1, dot2)这段代码实现的是连线dot1和dot2。动画播放前面都是初始化的步骤，接下来，我们将决定对象将以怎样的方式去展现。很庆幸的是，manim已经帮我们内置了极多的动画方法，我们可以直接使用，就可以实现丰富高级的动画。基本的代码格式是：self.play(Animations)实现"书写"文字Write(Object)[tip type='info']manim所有的动画效果都必须放在self.play()的括号中，不然会出现"闪现"的情况。这是因为self.play()会自动插值，使得坐标变换等移动变得顺滑。[/tip]Write()默认从左往右"书写"文字，如果想从右往左，仅仅需要将reverse属性设置为True。下面是一个具体的例子self.play( Write(textObject, reverse=True) )动画组一般动画是逐个播放的，比如先是"画一条线"，然后再"移动这条线"。但是，有时候我们想让几个动画同时进行，比如"在向左移动的同时将自身的透明度从0变为1"。这个时候，我们便需要引入动画组的概念。# 定义一个动画组 titleFadeOut = [ FadeOut(title), FadeOut(subtitle_vol), FadeOut(subtitle), FadeOut(l), FadeOut(author) ] # 播放动画组 self.play(AnimationGroup(*titleFadeOut))类似数组一样，我们仅仅需要将要同时播放的动画放进去，然后使用AnimationGroup()方法即可。唯一需要注意的是，括号内动画组名字的开头需要添加星号。
manim是什么如果你没有看过B站UP主 3Blue1Brown 的视频，那么，你可以先在b站上观看他的视频。[BiliVideo][/BiliVideo]manim就是其为了做视频而开发的基于Python的可视化引擎。manimCE和manim的区别manimCE是manim的社区版本。作为社区版，manimCE更加活跃，开发文档也更完善。事实上，你可以从manimCE的 官方网站 上更深入地了解它。更方便的是，manimCE完善了依赖的安装方式，安装流程上自动化更高，出错率也更少。因为manimCE目前还没有中文版本支持，所以这里简单的对manimCE安装过程做个教程，方便更多人使用上这个强大的视频制作引擎。本教程仅仅说明Windows系统，其他系统的安装方式可以自行阅读开发文档。本次安装所用设备参数，仅供参考（manimCE对设备要求不高）：系统版本：Windows11 x64cpu：11代i7内存：16G安装安装ChocolateyChocolatey是Windows平台上的包管理器，通过它可以集中安装、管理、更新各种各样的软件。类似apt-get，可以一条命令自动抓取更新，省心省力自动化高。Chocolatey安装文档：戳我首先，可以打开Windows的开始菜单，在搜索栏中搜索 powershell ，右键，以管理员身份运行。运行 Set-ExecutionPolicy AllSigned ，升级权限。再运行Get-ExecutionPolicy，如果显示Restricted，则说明权限无效，需要：检查是否以管理员身份运行了powershell多次运行Set-ExecutionPolicy AllSigned，重复检查复制粘贴以下代码，然后运行：Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))安装结束后，运行choco，如果最后显示消息如下，则安装Chocolatey成功。 由于国内网络环境原因，如果安装失败，可以按上面的步骤多试几次，或者通过网络代理，更换安装源等方式改善网络。安装manimCE到这一步，安装就简单了。对于Chocolatey，我们仅仅需要一条命令：choco install manimceChocolatey就会自动安装包括manimCE在内的所有依赖。同Chocolatey一样，因为国内网络环境，可能会出现安装失败的情况。多次尝试即可。安装完成后，运行manim，应该同样会弹出版本信息。安装VSCode（可选）VSCode官网：戳我直接点击首页的Download for Windows即可。因为网络原因，下载不了的可以网上寻找安装包。安装VSCode拓展（可选）安装完VSCode后，可以从拓展中搜索Manim SideView拓展，安装。之后即可直接在VSCode中使用manimCE。 manimCE升级仅仅需要一行命令：choco upgrade manimce
楔子最近学校学到了面向对象，觉得挺有意思的。以前编程觉得单纯看变量名就好，学习后才发现自己以前是面向过程的编程思维，小程序没问题，一旦工程变大，弊端就显露无疑。恰好学校雏燕计划也开始了，用上了面向对象的编程方法。觉得有必要留下点笔墨，记录下自己学习的过程，以供参考。一切的开始从第一次接触编程，变量永远是绕不开的话题。这个变量在哪里改变了值，在哪里被引用……编程绕着变量转，一切似乎都已经完美。这就是上一代程序员的感觉，他们把这种围绕变量，函数转的流程称为 面向过程 。早期软件开发面临的问题较为简单，认清问题问题到代码实现并不是太困难的事情。随着计算机的普及和应用领域的发展，计算机处理的事情越来越复杂，规模和复杂度空前扩大。人们忽然发现面向过程不太好使了。面向对象？面向对象！20世纪60年代，一场“软件危机”席卷了整个编程界。事实上，随着计算机发展，原本通常是一个人完成的工作需要更多人合力完成。虽然“人多力量大”听起来很美好，但是出于编程习惯的差异，代码版本控制缺失等问题，软件开发人员失望的发现，他们已经无法控制住软件的错误了……很要命，程序员想。可能很难理解，一个自己亲手开发的程序，为什么会落得无法控制错误的地步。可以想象下，两个无助的人漂流到荒岛，做一个简陋的茅草屋可能绰绰有余，然而，当问题变成如何建造一个100层的高楼，程序员就蚌埠住了。复杂到一定程度，面对上千上万行的庞大代码，也只能有心无力。虽然随之而来的Git代码管理，驼峰命名法等大杀器成功缓解了“有码难编”的困境，但是程序员最终发现，真正的问题出于面向过程本身。面向对象 出现了，程序员看到了新的救星。类，数据成员，成员函数让我们从生活中寻找灵感。糖果可以作为一个庞大的类（Class），我们发现，首先，糖果是有很多颜色的，其次，糖果还可以有不同的口味。我们可以定义一个新的类了。class Candy{ private: string color; string taste; };但是，结构体（Struct）依然可以替代类：struct Candy{ string color; string taste; };但是，类可以看做是结构体的一次大升级：class Candy{ public: void Set(string, string); void AskTaste(); void AskColor(); private: string color; string taste; }; void Candy::Set(string Color, string Taste){ color = Color; taste = Taste; } void Candy::AskTaste(){ cout<<"The Candy taste like "<<taste; } void Candy::AskColor(){ cout<<"The Candy is "<<color; }类不仅包括变量（数据成员 Data Members），还可以包括函数（成员函数 Member Functions）！面向对象更加符合人的认知规律，将数据和操作封装在一起。类相当于一种新的自定义数据类型，地位等同int，float，struct等等。因此在C++中，可以通过类名 对象名的方式创建一个属于类的对象。下面是一个创建对象的具体代码片段：class Candy{ //Define }; int main(void){ class Candy c1;//class可以省略 //Your Code }我们将Define部分称之为 类的声明 ，而其中的成员有三种访问属性：public为 公有性成员 ，外部可访问，作为接口protected为 保护性成员 ，外部不可访问，主要在类的继承和派生中起作用。private为 私有成员 ，外部不可访问[tip type="note"]若未声明访问属性，默认为private类型。[/tip]面向对象的优点事实上，面向对象真正促进了软件体系的重构。我们知道，需求是不断变化的，我们无法预知需求的变化，但是可以预测需求变化的位置。这样就给我们可乘之机，能够通过封装的方式将新代码和旧代码隔离，减少维护成本。面向对象通过继承和映射关系，把接口，包等等概念的实现变得简单，这也为后面设计模式的推行奠定了至关重要的基础。