2024年1月

大家好,我是沙漠尽头的狼。在朋友圈看到桂素伟大佬发的喜讯截图,站长赶紧翻译向大家报喜,确定了-C#是2023年度的编程语言!

.NET与C#

在TIOBE指数的历史上,C#首次获得了年度编程语言的奖项。祝贺!二十多年来,C#一直是前10名的选手,现在它正在追赶四大语言,它以一年内最大的涨幅(+1.43%)赢得了这一当之无愧的奖项。紧随其后的是Scratch(+0.83%)和Fortran(+0.64%)。C#正在蚕食Java的市场份额,并且在诸如Web应用程序后端和游戏(感谢Unity)等领域中越来越受欢迎。C#可以免费使用(站长注:.NET是MIT协议开源,免费跨平台,信创没问题),并以稳定的速度发展,使该语言在每个新版本中都更具表现力。C#将继续存在,甚至可能很快超越Java。

除了C#之外,去年TIOBE指数中还有许多有趣的变化。Fortran和Kotlin成为了前20名的永久选手,取代了长期受欢迎的R和Perl。Fortran非常适合使用优秀的库来处理数字,并且在许多领域仍然是大学的宠儿。Kotlin是Java的易于学习/编写的竞争对手。一个有趣的问题是:2024年哪些语言将进入TIOBE指数前20名?这很难预测。Julia在2023年短暂触及了TIOBE指数,但未能保持该位置。Julia语言和社区的成熟需要获得第二次机会。我会把赌注押在Dart(与Flutter一起)和TypeScript上。后者已经在工业中得到了大量使用,但由于某种原因,它尚未突破TIOBE指数。让我们看看2024年有什么。 -- TIOBE软件首席执行官Paul Jansen

TIOBE编程社区指数是编程语言受欢迎程度的指标。该指数每月更新一次。评级基于全球范围内的熟练工程师、课程和第三方供应商的数量。诸如Google、Bing、Yahoo!、Wikipedia、Amazon、YouTube和Baidu等流行搜索引擎被用于计算评级。值得注意的是,TIOBE指数并不是关于最好的编程语言或编写最多代码行的语言。

该指数可用于检查您的编程技能是否仍然是最新的,或者就构建新软件系统时应采用哪种编程语言做出战略决策。TIOBE指数的定义可以
在这里
找到。

2024年1月 2023年1月 变化 编程语言Logo 编程语言 评级 变化
1 1 Python page Python 13.97% -2.39%
2 2 C page C 11.44% -4.81%
3 3 C++ page C++ 9.96% -2.95%
4 4 Java page Java 7.87% -4.34%
5 5 C# page C# 7.16% +1.43%
6 7 change JavaScript page JavaScript 2.77% -0.11%
7 10 change PHP page PHP 1.79% +0.40%
8 6 change Visual Basic page Visual Basic 1.60% -3.04%
9 8 change SQL page SQL 1.46% -1.04%
10 20 change Scratch page Scratch 1.44% +0.86%
11 12 change Go page Go 1.38% +0.23%
12 27 change Fortran page Fortran 1.09% +0.64%
13 17 change Delphi/Object Pascal page Delphi/Object Pascal 1.09% +0.36%
14 15 change MATLAB page MATLAB 0.97% +0.06%
15 9 change Assembly language page Assembly language 0.92% -0.68%
16 11 change Swift page Swift 0.89% -0.31%
17 25 change Kotlin page Kotlin 0.85% +0.37%
18 16 change Ruby page Ruby 0.80% +0.01%
19 18 change Rust page Rust 0.79% +0.18%
20 31 change COBOL page COBOL 0.78% +0.45%

TIOBE编程语言索引


其他编程语言

下面是完整的前50名编程语言列表。此概述是非官方发布的,因为可能存在我们错过的语言。如果您觉得缺少某种编程语言,请通知我们
tpci@tiobe.com
。另请查看我们监视
的所有编程语言的概述

排名 编程语言 评级
21 D 0.77%
22 F# 0.77%
23 R 0.74%
24 SAS 0.70%
25 (Visual) FoxPro 0.67%
26 Ada 0.62%
27 Classic Visual Basic 0.60%
28 Prolog 0.56%
29 VBScript 0.55%
30 Perl 0.52%
31 Objective-C 0.46%
32 Dart 0.43%
33 Julia 0.40%
34 X++ 0.39%
35 TypeScript 0.39%
36 Lua 0.37%
37 Scala 0.34%
38 GAMS 0.33%
39 Transact-SQL 0.30%
40 Logo 0.30%
41 ABAP 0.29%
42 Lisp 0.28%
43 CFML 0.28%
44 Haskell 0.28%
45 PL/SQL 0.27%
46 Awk 0.27%
47 Eiffel 0.24%
48 Smalltalk 0.23%
49 ML 0.23%
50 ActionScript 0.23%


接下来的 50 种编程语言

以下列表表示第51到第100的语言。由于差异相对较小,编程语言仅按字母顺序列出。

  • Algol、AutoLISP、Avenue、Bash、bc、Boo、CIL、CL (OS/400)、CLIPS、Clojure、CLU、Curl、DiBOL、Erlang、Forth、Hack、Icon、Io、J、J#、JScript、LabVIEW、Ladder Logic、Lingo、LiveCode、M4、Maple、MQL5、NATURAL、Nim、OpenEdge ABL、PL/I、PostScript、PowerShell、Pure Data、Q、Racket、REXX、Ring、RPG、Scheme、Snap!、Solidity、SPARK、SPSS、Squirrel、Stata、Wolfram、Xojo、XQuery


相当长的历史

要了解更大的图景,请查找下面多年来前10名编程语言的位置。请注意,这些都是平均值。

编程语言 2024 2019 2014 2009 2004 1999 1994 1989
Python 1 4 8 6 11 22 22 -
C 2 2 1 2 2 1 1 1
C++ 3 3 4 3 3 2 2 3
Java 4 1 2 1 1 16 - -
C# 5 6 5 8 9 32 - -
JavaScript 6 8 9 9 8 21 - -
Visual Basic 7 19 - - - - - -
PHP 8 7 6 5 6 - - -
SQL 9 9 - - 7 - - -
Assembly language 10 13 - - - - - -
Objective-C 27 11 3 42 48 - - -
Lisp 30 28 14 17 15 10 7 2
(Visual) Basic - - 7 4 5 3 3 7

这里有两点重要评论:

  • 上表中的“Visual Basic”和“(Visual) Basic”是有区别的。直到 2010 年,“(Visual) Basic”指的是 Basic 的所有可能的子语言,包括 Visual Basic。经过一番讨论,决定将“(Visual) Basic”拆分为所有子语言,仅举几例,例如 Visual Basic .NET、Classic Visual Basic、PureBasic 和 Small Basic。由于 Visual Basic .NET 已成为 Visual Basic 的主要实现,因此它现在被称为“Visual Basic”。
  • 2018 年,在有人指出 SQL 是图灵完备之后,编程语言 SQL 被添加到 TIOBE 索引中。因此,尽管这种语言非常古老,但它在索引中的历史很短。


编程语言名人堂

如下所示,名人堂列出了所有“年度编程语言”奖获得者。该奖项颁发给一年内收视率上升最高的编程语言。

年份 年度编程语言获得者
2023 medal
C#
2022 medal
C++
2021 medal
Python
2020 medal
Python
2019 medal
C
2018 medal
Python
2017 medal
C
2016 medal
Go
2015 medal
Java
2014 medal
JavaScript
2013 medal
Transact-SQL
2012 medal
Objective-C
2011 medal
Objective-C
2010 medal
Python
2009 medal
Go
2008 medal
C
2007 medal
Python
2006 medal
Ruby
2005 medal
Java
2004 medal
PHP
2003 medal
C++

感谢桂素伟大佬朋友圈相告,这是原文链接:
TIOBE 指数
,由沙漠尽头的狼提供翻译,大家可以欢呼了!

背景

之前做的小工具
一个jsqlparse+git做的小工具帮我节省时间摸鱼
昨天突然停止工作,看了下jvm并没有退出,但是看日志确实有不少Error输出,虽说是一个普通的NPE,但是分析了一下却疑点重重,所以花点时间来一探究竟,最终又掌握一个jvm知识点,还是比较有意思。

错误现场

以下是示例代码,为了说明问题做了简化,大概意思是使用CCJSqlParserUtil去解析一段sql语句,如果解析出错了以后从JSQLParserException.getMessage()中利用正则提取出具体的行和列。

Statements statements = null;
Set<Integer> sqlSet = new HashSet<>();
String sql ="alter table test add column varchra(4)";
try {
    statements = CCJSqlParserUtil.parseStatements(sql);
} catch (JSQLParserException e) {
        Pattern pattern = Pattern.compile("line (\\d+), column (\\d+)");
        String message = e.getMessage();
        Matcher m = pattern.matcher(message);
        int line = -1;
        int column = -1;
        while(m.find()){
            int groupCount = m.groupCount();
            if(groupCount > 0){
                line = Integer.parseInt(m.group(1));
                column = Integer.parseInt(m.group(2));
                break;
            }
        }
}

上面那个错误sql解析出错了以后的异常信息如下:

Encountered unexpected token: "varchra" <S_IDENTIFIER>
    at line 1, column 29.

Was expecting:

    "COMMENT"

那个诡异的NPE 栈如下:

java.lang.NullPointerException: null
        at java.util.regex.Matcher.getTextLength(Matcher.java:1283)
        at java.util.regex.Matcher.reset(Matcher.java:309)
        at java.util.regex.Matcher.<init>(Matcher.java:229)
        at java.util.regex.Pattern.matcher(Pattern.java:1093)
        at xxx.ScriptUtil.sqlParse(ScriptUtil.java:41)

很显然是e.getMessage()返回了null导致pattern.matcher(message)失败,但是e.getMessage()理论上来讲不会是null,有点玄学的味道,一般解决玄学的首要方法是重启大法(
个人观点,欢迎来喷,哈哈
)。果然,重启了以后竟然好了,好奇心一下就被激发了。

错误原因

网上一通搜索确实类似的案例不少,大概的意思是jvm对异常处理这块做了优化,如果频繁抛出某种异常jvm会对这些异常做一些处理,使用JVM初始化的时候创建的那些异常对象来替代本应该新建的异常对象,因此这些异常栈和Message是空的,这一特性受OmitStackTraceInFastThrow参数的管控,可以通过-XX:+OmitStackTraceInFastThrow开启,或者-XX:-OmitStackTraceInFastThrow关闭,看完确实恍然大悟,但是并没有找到官方的一些说明,还是心有不甘,决定在openjdk源码中找找答案,全局在openjdk8的源码中搜索OmitStackTraceInFastThrow关键字,确实得到了想要的答案,一起来看下。

结合网上的一些结论和源码来看只有以下几类异常才会触发OmitStackTraceInFastThrow,分别是NullPointerException、ArithmeticException、ArrayIndexOutOfBoundsException、ArrayStoreException、ClassCastException,最终发现是有一个脚本文件的内容为空,会触发jsqlparse发生ArrayIndexOutOfBoundsException,进而触发了OmitStackTraceInFastThrow特性,导致工具代码中e.getMessage()返回null而触发NPE造成工具停止运行的假象。

修复办法

  1. 使用-XX:-OmitStackTraceInFastThrow关闭这一特性;

  2. 对执行逻辑优化,如果发现脚本文件内容为空就直接返回,不再继续执行;

推荐阅读

https://opts.console.heapdump.cn/result/query/Ex13k

https://heapdump.cn/topic/OmitStackTraceInFastThrow

一个jsqlparse+git做的小工具帮我节省时间摸鱼

CMU DLSys 课程笔记 2 - ML Refresher / Softmax Regression

本节 Slides
|
本节课程视频

这一节课是对机器学习内容的一个复习,以 Softmax Regression 为例讲解一个典型的有监督机器学习案例的整个流程以及其中的各种概念。预期读者应当对机器学习的基本概念有一定的了解。

目录

机器学习基础

针对于手写数字识别这一问题,传统的图像识别算法可能是首先找到每个数字的特征,然后手写规则来识别每个数字。这种方式的问题在于,当我们想要识别的对象的种类很多时,我们需要手动设计的规则就会变得非常复杂,而且这些规则很难设计得很好,因为我们很难找到一个完美的特征来区分所有的对象。

而机器学习方法则是让计算机自己学习如何区分这些对象,我们只需要给计算机一些数据,让它自己学习如何区分这些数据,这样的方法就可以很好地解决这个问题。

具体到有监督机器学习方法,我们需要给计算机一些数据,这些数据包含了我们想要识别的对象的一些特征,以及这些对象的标签,计算机需要从这些数据中学习到如何区分这些对象,如下图

有监督机器学习

图里中间部分即为我们需要建立的机器学习模型,通常由以下内容组成:

  1. 模型假设:描述我们如何将输入(例如数字的图像)映射到输出(例如类别标签或不同类别标签的概率)的“程序结构”,通过一组参数进行参数化。
  2. 损失函数:指定给定假设(即参数选择)在所关注任务上的表现“好坏”的函数。
  3. 优化方法:确定一组参数(近似)最小化训练集上损失总和的过程。

Softmax Regression 案例

问题定义

让我们考虑一个 k 类分类问题,其中我们有:

  • 训练数据:
    \(x^{(i)} \in \R^n\)
    ,
    \(y^{(i)} \in {1,\dots, k}\)
    for
    \(i = 1, … , m\)
  • 其中
    \(n\)
    为输入数据的维度,
    \(m\)
    为训练数据的数量,
    \(k\)
    为分类类别的数量
  • 针对 28x28 的 MNIST 数字进行分类,
    \(n = 28 \cdot 28 = 784\)
    ,
    \(k = 10\)
    ,
    \(m = 60,000\)

模型假设

我们的模型假设是一个线性模型,即

\[h_\theta(x) = \theta^T x
\]

其中
\(\theta \in \R^{n\times k}\)
是我们的模型参数,
\(x \in \R^n\)
是输入数据。

机器学习中,经常使用的形式是多个输入叠加在一起的形式,即

\[X \in \R^{m\times n}= \begin{bmatrix} {x^{(1)}}^T \\ \vdots \\ {x^{(m)}}^T \end{bmatrix}, \quad y = \begin{bmatrix} y^{(1)} \\ \vdots \\ y^{(m)} \end{bmatrix}
\]

然后线性模型假设可以写为

\[h_\theta(X) = \begin{bmatrix} {x^{(1)}}^T\theta \\ \vdots \\ {x^{(m)}}^T\theta \end{bmatrix} = X\theta
\]

损失函数

最简单的损失函数就是根据是否预测正确,如

\[\ell_{e r r}(h(x), y)=\left\{\begin{array}{ll}
0 & \text { if } \operatorname{argmax}_{i} h_{i}(x)=y \\
1 & \text { otherwise }
\end{array}\right.
\]

我们经常用这个函数来评价分类器的质量。但是这个函数有一个重大的缺陷是非连续,因此我们无法使用梯度下降等优化方法来优化这个函数。

取而代之,我们会用一个连续的损失函数,即交叉熵损失函数

\[z_{i}=p(\text { label }=i)=\frac{\exp \left(h_{i}(x)\right)}{\sum_{j=1}^{k} \exp \left(h_{j}(x)\right)} \Longleftrightarrow z \equiv \operatorname{softmax}(h(x))
\]

\[\ell_{ce}(h(x), y) = -\log p(\text { label }=y) = -h_y(x) + \log \sum_{j=1}^k \exp(h_j(x))
\]

这个损失函数是连续的,而且是凸的,因此我们可以使用梯度下降等优化方法来优化这个损失函数。

优化方法

我们的目标是最小化损失函数,即

\[\min_{\theta} \frac{1}{m} \sum_{i=1}^m \ell_{ce}(h_\theta(x^{(i)}), y^{(i)})
\]

我们使用梯度下降法来优化这个损失函数,针对函数
\(f:\R^{n\times k} \rightarrow \R\)
,其梯度为

\[\nabla_\theta f(\theta) = \begin{bmatrix} \frac{\partial f}{\partial \theta_{11}} & \dots & \frac{\partial f}{\partial \theta_{1k}} \\ \vdots & \ddots & \vdots \\ \frac{\partial f}{\partial \theta_{n1}} & \dots & \frac{\partial f}{\partial \theta_{nk}} \end{bmatrix}
\]

梯度的几何含义为函数在某一点的梯度是函数在该点上升最快的方向,如下图

梯度的几何含义

我们可以使用梯度下降法来优化这个损失函数,即

\[\theta \leftarrow \theta - \alpha \nabla_\theta f(\theta)
\]

其中
\(\alpha \gt 0\)
为学习率,即每次更新的步长。学习率过大会导致无法收敛,学习率过小会导致收敛速度过慢。

学习率大小

我们不需要针对每个样本都计算一次梯度,而是可以使用一个 batch 的样本来计算梯度,这样可以减少计算量,同时也可以减少梯度的方差,从而加快收敛速度,这种方法被称为随机梯度下降法(Stochastic Gradient Descent, SGD)。该方法的算法描述如下

\[\left.
\begin{array}{l}
\text { Repeat:} \\
\text { \quad Sample a batch of data } X \in \R^{B\times n}, y \in \{1, \dots, k\}^B \\
\text { \quad Update parameters } \theta \leftarrow \theta-\alpha \nabla_{\theta} \frac{1}{B} \sum_{i=1}^{B} \ell_{ce}\left(h_{\theta}\left(x^{(i)}\right), y^{(i)}\right)
\end{array}
\right.
\]

前面都是针对 SGD 的描述,但是损失函数的梯度还没有给出,我们一般使用链式法则进行计算,首先计算 softmax 函数本身的梯度

\[\frac{\partial \ell(h, y)}{\partial h_i} = \frac{\partial}{\partial h_i} \left( -h_y + \log \sum_{j=1}^k \exp(h_j) \right) = -e_y + \frac{\exp(h_i)}{\sum_{j=1}^k \exp(h_j)}
\]

写成矩阵形式即为

\[\nabla_h \ell(h, y) = -e_y + \operatorname{softmax}(h)
\]

然后计算损失函数对模型参数的梯度

\[\frac{\partial \ell(h, y)}{\partial \theta} = \frac{\partial \ell(\theta^T x, y)}{\partial \theta} = \frac{\partial \ell(h, y)}{\partial h} \frac{\partial h}{\partial \theta} = x(\operatorname{softmax}(h) - e_y)^T
\]

写成矩阵形式即为

\[\nabla_\theta \ell(h, y) = X^T (\operatorname{softmax}(X\theta) - \mathbb{I}_y)
\]

完整算法描述

最终算法描述为

\[\left.
\begin{array}{l}
\text { Repeat:} \\
\text { \quad Sample a batch of data } X \in \R^{B\times n}, y \in \{1, \dots, k\}^B \\
\text { \quad Update parameters } \theta \leftarrow \theta-\alpha X^T (\operatorname{softmax}(X\theta) - \mathbb{I}_y)
\end{array}
\right.
\]

以上就是完整的 Softmax Regression 的算法描述,最终在 hw0 中我们会实现这个算法,其分类错误率将低于 8 %。

前言

路由(routers)是应用的重要组成部分。所谓路由,有多种定义,对于应用层的单页应用程序而言,路由是一个决定 URL 如何呈现的库,在服务层实现 API 时,路由是解析请求并将请求定向到处理程序的组件。简单的来说,在 Next.js 中,路由决定了一个页面如何渲染或者一个请求该如何返回。

Next.js 目前有两套路由解决方案,之前的方案称之为“Pages Router”,目前的方案称之为“App Router”,两套方案是兼容的,都可以在 Next.js 中使用。本篇我们会重点讲解 App Router,并学习 App Router 下路由的定义方式和常见的文件约定,学习完本篇,你将学会如何创建一个页面。

1. 文件系统(file-system)

Next.js 的路由基于的是文件系统,也就是说,一个文件就可以是一个路由。举个例子,你在
app/pages
目录下创建一个
index.js
文件,它会直接映射到
/
路由地址:

// app/pages/index.js
import React from 'react'
export default () => <h1>Hello world</h1>


app/pages
目录下创建一个
about.js
文件,它会直接映射到
/about
路由地址:

// app/pages/about.js
import React from 'react'
export default () => <h1>About us</h1>

2. 从 Pages Router 到 App Router

现在你打开使用
create-next-app
创建的项目,你会发现默认并没有
pages
这个目录。查看
packages.json
中的 Next.js 版本,如果版本号大于
13.4
,那就对了!

Next.js 从 v13 起就使用了新的路由模式 —— App Router。之前的路由模式我们称之为“Pages Router”,为保持渐进式更新,依然存在。从 v13.4 起,App Router 正式进入稳定化阶段,App Router 功能更强、性能更好、代码组织更灵活,以后就让我们使用新的路由模式吧!

可是这俩到底有啥区别呢?Next.js 又为什么升级到 App Router 呢?知其然知其所以然,让我们简单追溯一下。以前我们声明一个路由,只用在
pages
目录下创建一个文件就可以了,以前的目录结构类似于:

└── pages
├── index.js
├── about.js
└── more.js

这种方式有一个弊端,那就是
pages
目录的所有 js 文件都会被当成路由文件,这就导致比如组件不能写在
pages
目录下,这就不符合开发者的使用习惯。(当然 Pages Router 还有很多其他的问题,只不过目前我们介绍的内容还太少,为了不增加大家的理解成本,就不多说了)

升级为新的 App Router 后,现在的目录结构类似于:

src/
└── app
├── page.js
├── layout.js
├── template.js
├── loading.js
├── error.js
└── not-found.js
├── about
│ └── page.js
└── more
└── page.js

使用新的模式后,你会发现
app
下多了很多文件。这些文件的名字并不是我乱起的,而是 Next.js 约定的一些特殊文件。从这些文件的名称中你也可以了解文件实现的功能,比如布局(layout.js)、模板(template.js)、加载状态(loading.js)、错误处理(error.js)、404(not-found.js)等。

简单的来说,App Router 制定了更加完善的规范,使代码更好被组织和管理。至于这些文件具体的功能和介绍,不要着急,本篇我们会慢慢展开。

3. 使用 Pages Router

当然你也可以继续使用 Pages Router,如果你想使用 Pages Router,只需要在
src
目录下创建一个
pages
文件夹或者在根目录下创建一个
pages
文件夹。其中的 JS 文件会被视为 Pages Router 进行处理。

image.png

但是要注意,虽然两者可以共存,但 App Router 的优先级要高于 Pages Router。而且如果两者解析为同一个 URL,会导致构建错误。

你在 Next.js 官方文档进行搜索的时候,左上角会有 App 和 Pages 选项,这对应的就是 App Router 和 Pages Router:

image.png

因为两种路由模式的使用方式有很大不同,所以搜索的时候注意选择正确的的路由模式。

4. 使用 App Router

4.1. 定义路由

现在让我们开始正式的学习 App Router 吧。

首先是定义路由,文件夹被用来定义路由。每个文件夹都代表一个对应到 URL 片段的路由片段。创建嵌套的路由,只需要创建嵌套的文件夹。举个例子,下图的
app/dashboard/settings
目录对应的路由地址就是
/dashboard/settings

image.png

4.2. 定义页面(Pages)

那如何保证这个路由可以被访问呢?你需要创建一个特殊的名为
page.js
的文件。至于为什么叫
page.js
呢?除了
page
有“页面”这个含义之外,你可以理解为这是一种约定或者规范。(如果你是 Next.js 的开发者,你也可以约定为
index.js
甚至
yayu.js
!)

image.png

在上图这个例子中:

  • app/page.js
    对应路由
    /
  • app/dashboard/page.js
    对应路由
    /dashboard
  • app/dashboard/settings/page.js
    对应路由
    /dashboard/settings
  • analytics
    目录下因为没有
    page.js
    文件,所以没有对应的路由。这个文件可以被用于存放组件、样式表、图片或者其他文件。

当然不止
.js
文件,Next.js 默认是支持 React、TypeScript 的,所以
.js

.jsx

.tsx
都是可以的。

那这个
page.js
代码如何写呢?最常见的是展示 UI,比如:

// app/page.js
export default function Page() {
return <h1>Hello, Next.js!</h1>
}

访问
http://localhost:3000/
,效果如下:

image.png

4.3. 定义布局(Layouts)

布局是指多个页面共享的 UI。在导航的时候,布局会保留状态,保持可交互性并且不会重新渲染,比如用来实现后台管理系统的侧边导航栏。

定义一个布局,你需要新建一个名为
layout.js
的文件,该文件默认导出一个 React 组件,该组件应接收一个
children
prop,
chidren
表示子布局(如果有的话)或者子页面。

举个例子,我们新建目录和文件如下图所示:

image.png

相关代码如下:

// app/dashboard/layout.js
export default function DashboardLayout({
children,
}) {
return (
<section>
<nav>nav</nav>
{children}
</section>
)
}
// app/dashboard/page.js
export default function Page() {
return <h1>Hello, Dashboard!</h1>
}

当访问
/dashboard
的时候,效果如下:

image.png

其中,
nav
来自于
app/dashboard/layout.js

Hello, Dashboard!
来自于
app/dashboard/page.js

你可以发现:同一文件夹下如果有 layout.js 和 page.js,page 会作为 children 参数传入 layout。换句话说,layout 会包裹同层级的 page。

app/dashboard/settings/page.js
代码如下:

// app/dashboard/settings/page.js
export default function Page() {
return <h1>Hello, Settings!</h1>
}

当访问
/dashboard/settings
的时候,效果如下:

image.png

其中,
nav
来自于
app/dashboard/layout.js

Hello, Settings!
来自于
app/dashboard/settings/page.js

你可以发现:布局是支持嵌套的

app/dashboard/settings/page.js
会使用
app/layout.js

app/dashboard/layout.js
两个布局中的内容,不过因为我们没有在
app/layout.js
写入可以展示的内容,所以图中没有体现出来。

根布局(Root Layout)

布局支持嵌套,最顶层的布局我们称之为根布局(Root Layout),也就是
app/layout.js
。它会应用于所有的路由。除此之外,这个布局还有点特殊。

使用
create-next-app
默认创建的
layout.js
代码如下:

// app/layout.js
import './globals.css'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}

其中:

  1. app
    目录必须包含根布局,也就是
    app/layout.js
    这个文件是必需的。
  2. 根布局必须包含
    html

    body
    标签,其他布局不能包含这些标签。但如果你要更改这些标签,不推荐直接修改,Next.js 提供
    内置工具
    帮助你管理诸如
    <title />
    这样的 HTML 元素。
  3. 你可以使用
    路由组
    创建多个根布局。
  4. 默认根布局是
    服务端组件
    ,且不能设置为客户端组件。

4.4. 定义模板(Templates)

模板类似于布局,它也会传入每个子布局或者页面。但不会像布局那样维持状态。

模板在路由切换时会为每一个 children 创建一个实例。这就意味着当用户在共享一个模板的路由间跳转的时候,将会重新挂载组件实例,重新创建 DOM 元素,不保留状态。这听起来有点抽象,没有关系,我们先看看模板的写法,再写个 demo 你就明白了。

定义一个模板,你需要新建一个名为
template.js
的文件,该文件默认导出一个 React 组件,该组件接收一个
children
prop。我们写个示例代码。在
app
目录下新建一个
template.js
文件。

image.png

template.js
代码如下:

// app/template.js
export default function Template({ children }) {
return <div>{children}</div>
}

你会发现,这用法跟布局一模一样。它们最大的区别就是状态的保持。如果同一目录下既有
template.js
也有
layout.js
,最后的输出效果如下:

<Layout>
{/* 模板需要给一个唯一的 key */}
<Template key={routeParam}>{children}</Template>
</Layout>

也就是说
layout
会包裹
template

template
又会包裹
page

某些情况下,模板会比布局更适合:

  • 依赖于 useEffect 和 useState 的功能,比如记录页面访问数(维持状态就不会在路由切换时记录访问数了)、用户反馈表单(每次重新填写)等

  • 更改框架的默认行为,举个例子,布局内的 Suspense 只会在布局加载的时候展示一次 fallback UI,当切换页面的时候不会展示。但是使用模板,fallback 会在每次路由切换的时候展示。

布局 VS 模板

为了帮助大家更好的理解布局和模板,我们写一个 demo,展示下两者的特性。

项目目录如下:

app
└─ dashboard
├─ layout.js
├─ page.js
├─ template.js
├─ about
│ └─ page.js
└─ settings
└─ page.js

其中
dashboard/layout.js
代码如下:

'use client'
import { useState } from 'react'
import Link from 'next/link'
export default function Layout({ children }) {
const [count, setCount] = useState(0)
return (
<>
<div>
<Link href="/dashboard/about">About</Link>
<br/>
<Link href="/dashboard/settings">Settings</Link>
</div>
<h1>Layout {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
{children}
</>
)
}

dashboard/template.js
代码如下:

'use client'
import { useState } from 'react'
export default function Template({ children }) {
const [count, setCount] = useState(0)
return (
<>
<h1>Template {count}</h1>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
{children}
</>
)
}

dashboard/page.js
代码如下:

export default function Page() {
return <h1>Hello, Dashboard!</h1>
}

dashboard/about/page.js
代码如下:

export default function Page() {
return <h1>Hello, About!</h1>
}

dashboard/settings/page.js
代码如下:

export default function Page() {
return <h1>Hello, Settings!</h1>
}

最终展示效果如下(为了方便区分,做了部分样式处理):

image.png

现在点击两个
Increment
按钮,会开始计数。随便点击下数字,然后再点击
About
或者
Settings
切换路由,你会发现,Layout 后的数字没有发生变化,Template 后的数字重置为 0。这就是所谓的状态保持。

10.gif

注:当然如果刷新页面,Layout 和 Template 后的数字肯定都重置为 0。

4.5. 定义加载界面(Loading UI)

现在我们已经了解了
page.js

layout.js

template.js
的功能,然而特殊文件还不止这些。App Router 提供了用于展示加载界面的
loading.js

这个功能的实现借助了 React 的
Suspense
API。关于 Suspense 的用法,可以查看
《React 之 Suspense》
。它实现的效果就是当发生路由变化的时候,立刻展示 fallback UI,等加载完成后,展示数据。

// 在 ProfilePage 组件处于加载阶段时显示 Spinner
<Suspense fallback={<Spinner />}>
<ProfilePage />
</Suspense>

初次接触 Suspense 这个概念的时候,往往会有一个疑惑,那就是——“在哪里控制关闭 fallback UI 的呢?”

哪怕在 React 官网中,对背后的实现逻辑并无过多提及。但其实实现的逻辑很简单,简单的来说,ProfilePage 会 throw 一个数据加载的 promise,Suspense 会捕获这个 promise,追加一个 then 函数,then 函数中实现替换 fallback UI 。当数据加载完毕,promise 进入 resolve 状态,then 函数执行,于是更新替换 fallback UI。

了解了原理,那我们来看看如何写这个
loading.js
吧。
dashboard
目录下我们新建一个
loading.js

image.png

loading.js
的代码如下:

// app/dashboard/loading.js
export default function DashboardLoading() {
return <>Loading dashboard...</>
}

同级的
page.js
代码如下:

// app/dashboard/page.js
async function getData() {
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
message: 'Hello, Dashboard!',
}
}
export default async function DashboardPage(props) {
const { message } = await getData()
return <h1>{message}</h1>
}

不再需要其他的代码,loading 的效果就实现了:

11.gif

就是这么简单。其关键在于
page.js
导出了一个 async 函数。

loading.js
的实现原理是将
page.js
和下面的 children 用
<Suspense>
包裹。因为
page.js
导出一个 async 函数,Suspense 得以捕获数据加载的 promise,借此实现了 loading 组件的关闭。

image.png

当然实现 loading 效果,不一定非导出一个 async 函数。也可以借助 React 的
use
函数。现在我们在
dashboard
下新建一个
about
目录,在其中新建
page.js
文件。

/dashboard/about/page.js
代码如下:

// /dashboard/about/page.js
import { use } from 'react'
async function getData() {
await new Promise((resolve) => setTimeout(resolve, 5000))
return {
message: 'Hello, About!',
}
}
export default function Page() {
const {message} = use(getData())
return <h1>{message}</h1>
}

同样实现了 loading 效果:

12.gif

如果你想针对
/dashboard/about
单独实现一个 loading 效果,那就在
about
目录下再写一个
loading.js
即可。

如果同一文件夹既有
layout.js
又有
template.js
又有
loading.js
,那它们的层级关系是怎样呢?

对于这些特殊文件的层级问题,直接一张图搞定:

image.png

4.6. 定义错误处理(Error Handling)

再讲讲特殊文件
error.js
。顾名思义,用来创建发生错误时的展示 UI。

其实现借助了 React 的
Error Boundary
功能。简单来说,就是给 page.js 和 children 包了一层
ErrorBoundary

image.png

我们写一个 demo 演示一下
error.js
的效果。
dashboard
目录下新建一个
error.js
,目录效果如下:

image.png

dashboard/error.js
代码如下:

'use client' // 错误组件必须是客户端组件
// dashboard/error.js
import { useEffect } from 'react'
export default function Error({ error, reset }) {
useEffect(() => {
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// 尝试恢复
() => reset()
}
>
Try again
</button>
</div>
)
}

为触发 Error 错误,同级
page.js
的代码如下:

"use client";
// dashboard/page.js
import React from "react";
export default function Page() {
const [error, setError] = React.useState(false);
const handleGetError = () => {
setError(true);
};
return (
<>{error ? Error() : <button onClick={handleGetError}>Get Error</button>}</>
);
}

效果如下:

13.gif

有时错误是暂时的,只需要重试就可以解决问题。所以 Next.js 会在
error.js
导出的组件中,传入
reset
函数,帮助尝试从错误中恢复。该函数会触发重新渲染错误边界里的内容。如果成功,会替换展示重新渲染的内容。

还记得上节讲过的层级问题吗?让我们回顾一下:

image.png

从这张图里你会发现一个问题,因为
Layout

Template

ErrorBoundary
外面,这说明错误边界不能捕获同级的
layout.js
或者
template.js
中的错误。如果你想捕获特定布局或者模板中的错误,那就在父级的
error.js
里进行捕获。

那问题来了,如果已经到了顶层,就比如根布局中的错误如何捕获呢?为了解决这个问题,Next.js 提供了
global-error.js
文件,使用它时,将其放在
app
目录下。

global-error.js
会包裹整个应用,而且当它触发的时候,它会替换掉根布局的内容。所以,
global-error.js
中也要定义
<html>

<body>
标签。

global-error.js
示例代码如下:

'use client'
// app/global-error.js
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}

注:
global-error.js
用来处理根布局和根模板中的错误,
app/error.js
建议还是要写的

4.7. 定义 404 页面

最后再讲一个特殊文件 ——
not-found.js
。顾名思义,当该路由不存在的时候展示的内容。

Next.js 项目默认的 not-found 效果如下:

image.png

如果你要替换这个效果,只需要在
app
目录下新建一个
not-found.js
,代码示例如下:

import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
<Link href="/">Return Home</Link>
</div>
)
}

not-found 的效果就会更改为:

image.png

考虑到
layout.js

template.js
的使用效果,如果我把
not-found.js
添加到一个子目录里,比如
/dashboard/blog
下:

dashboard
└─ blog
├─ page.js
└─ not-found.js

当访问
/dashboard/blog/yayu
(该路由没有声明)时,它的 not-found 效果会是自定义的吗?

答案是不会。访问
/dashboard/blog/yayu
依然会走到默认 not-found 路由效果。这是因为
not-found.js
被用于当
notFound
函数被抛出的时候。(
notFound
函数是
next/navigation
这个包提供的一个方法)

写个示例代码:

/dashboard/blog
下新建一个
page.js

not-found.js

page.js
代码如下:

// /dashboard/blog/page.js
import { notFound } from 'next/navigation'
export default function Page() {
notFound()
return <></>
}

not-found.js
代码则直接复制上面的
not-found.js

当访问
/dashboard/blog
时,因为
page.js
丢出了
notFound
函数,所以会触发
not-found.js
的执行。

image.png

但当访问
/dashboard/blog/yayu
时,因为没有对应的路由处理程序,依然是默认的 not-found 效果

image.png

如果我们添加对应的处理程序,在
app/dashboard/blog/yayu/page.js
中也执行 notFound 函数,就会渲染
/dashboard/blog/not-found.js
的 UI 内容。

对应到实际开发,当我们请求一个用户的数据时或是请求一篇文章的数据时,如果该数据不存在,就可以直接丢出
notFound
函数,渲染自定义的
not-found
界面。一个示例代码如下:

// app/dashboard/blog/[id]/page.js
import { notFound } from 'next/navigation'
async function fetchUser(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }) {
const user = await fetchUser(params.id)
if (!user) {
notFound()
}
// ...
}

5. 链接和导航(Linking and Navigating)

知道了如何定义路由,最后我们再讲讲如何在 Next.js 中实现链接和导航。Next.js 提供了两种方式:

  1. 使用
    <Link>
    组件
  2. 使用
    useRouter
    Hook

5.1.
<Link>

<Link>
是一个拓展了 HTML
<a>
标签的内置组件,用来实现预获取(prefetching) 和客户端路由导航。这是 Next.js 中路由导航的主要方式。

5.1.1. 基础使用

使用示例如下:

import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">Dashboard</Link>
}

支持 hash 值用于实现跳转到页面的某个位置:

<Link href="/dashboard#settings">Settings</Link>
// Output
<a href="/dashboard#settings">Settings</a>

5.1.2. 动态渲染链接

支持路由链接动态渲染:

import Link from 'next/link'
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}

5.1.3. 获取当前路径名

如果需要对当前链接进行判断,你可以使用
usePathname()
这个方法,它会读取当前 URL 的路径名(pathname),这是一段示例代码:

'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function Navigation({ navLinks }) {
const pathname = usePathname()
return (
<>
{navLinks.map((link) => {
const isActive = pathname === link.href
return (
<Link
className={isActive ? 'text-blue' : 'text-black'}
href={link.href}
key={link.name}
>
{link.name}
</Link>
)
})}
</>
)
}

5.1.4. 跳转行为设置

App Router 的默认行为是滚动到新路由的顶部,或者在前进后退导航时维持之前的滚动距离。

如果你想要禁用这个行为,你可以给
<Link>
组件传递一个
scroll={false}
,或者在使用
router.push

router.replace
的时候,设置
scroll: false

示例代码如下:

// next/link
<Link href="/dashboard" scroll={false}>
Dashboard
</Link>
// useRouter
import { useRouter } from 'next/navigation'
const router = useRouter()
router.push('/dashboard', { scroll: false })

注:关于
<Link>
组件的用法,我们还会在
《组件篇 | Link 和 Script》
中详细介绍。

5.2. useRouter

第二种方式是使用 useRouter。示例代码如下:

'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}

注意使用该 hook 需要在客户端组件中。(顶层的
'use client'
就是声明这是客户端组件)

注:关于 useRouter,我们还会在
《API 篇 | 请求相关的常用函数与方法》
篇中详细介绍。

小结

恭喜你,完成了本节内容的学习!

这一节我们重点讲解了 Next.js 基于文件系统的路由解决方案 App Router,介绍了用于定义页面的
page.js
、定义布局的
layout.js
、定义模板的
template.js
、定义加载界面的
loading.js
、定义错误处理的
error.js
、定义 404 页面的
not-found.js
。现在你再看 App Router 的这个目录结构:

src/
└── app
├── page.js
├── layout.js
├── template.js
├── loading.js
├── error.js
└── not-found.js
├── about
│ └── page.js
└── more
└── page.js

简单的来说,App Router 制定了更加完善的规范,使代码更好被组织和管理。

对此是不是有了更加深刻的理解呢?然而这还只有 Next.js 强大的路由功能的一小部分。下篇我们开始介绍 Next.js 的高级路由功能。

知识星球【Next.js开发指南】(已更新至第33章)

  1. 初始篇 | Next.js CLI
  2. 路由篇 | App Router
  3. 路由篇 | 动态路由、路由组、平行路由和拦截路由
  4. 路由篇 | 路由处理程序和中间件
  5. 路由篇 | 国际化
  6. 数据获取篇 | 数据获取、缓存与重新验证
  7. 数据获取篇 | Server Actions 与表单
  8. 渲染篇 | 从 CSR、SSR、SSG、ISR 开始说起
  9. 渲染篇 | 服务端组件和客户端组件
  10. 渲染篇 | Streaming 和 Edge Runtime
  11. 缓存篇 | Caching
  12. 样式篇 | Tailwind CSS、CSS-in-JS 与 Sass
  13. 组件篇 | Images
  14. 组件篇 | Font
  15. 组件篇 | Link 和 Script
  16. 优化篇 | 懒加载
  17. 配置篇 | TypeScript 和 ESLint
  18. 配置篇 | 环境变量、路径别名与 src 目录
  19. 配置篇 | MDX
  20. 配置篇 | 草稿模式和内容安全策略
  21. 配置篇 | 路由段配置项
  22. 部署篇 | 静态导出
  23. Metadata 篇 | 基于配置
  24. Metadata 篇 | 基于文件
  25. API 篇 | next.config.js(上)
  26. API 篇 | next.config.js(下)
  27. API 篇 | 请求相关的常用函数与方法
  28. API 篇 | 常用函数与方法
  29. 实战篇 | React Notes | 项目介绍与创建
  30. 实战篇 | React Notes | 侧边栏笔记列表
  31. 实战篇 | React Notes | 笔记预览界面
  32. 实战篇 | React Notes | 笔记编辑界面
  33. 实战篇 | React Notes | 笔记搜索
  34. 实战篇 | React Notes | 国际化
  35. 实战篇 | React Notes | Auth
  36. 实战篇 | React Notes | 文件上传
  37. 实战篇 | React Notes | 部署(一)
  38. 实战篇 | React Notes | 部署(二)
  39. 实战篇 | 博客 | 项目创建
  40. 实战篇 | 博客 | 博客后台
  41. 实战篇 | 博客 | MDX
  42. 实战篇 | 博客 | Server Actions
  43. 实战篇 | 博客 | 渲染原理
  44. 实战篇 | App | 需求分析
  45. 实战篇 | App | 数据库设计
  46. 实战篇 | App | 项目创建
  47. 实战篇 | App | 移动端处理
  48. 实战篇 | App | 接口开发
  49. 实战篇 | App | 数据请求
  50. 实战篇 | App | 构建部署
  51. 源码篇 | 源码架构
  52. 源码篇 | 调试代码
  53. 源码篇 | 路由实现
  54. 源码篇 | 渲染原理
  55. 源码篇 | 手写 SSR
  56. 源码篇 | mini-next
  57. 源码篇 | mini-next
  58. 源码篇 | mini-next
  59. 源码篇 | mini-next
  60. 面试篇 | 常见面试题及解析
  61. 面试篇 | 常见面试题及解析
  62. 面试篇 | 常见面试题及解析

1、准备材料

开发板(
STM32F407G-DISC1

ST-LINK/V2驱动
STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

逻辑分析仪
nanoDLA

2、实验目标

使用STM32CubeMX软件配置STM32F407
通用定时器的输出比较通道,并将其输出到四个LED灯引脚实现LED灯流水灯效果

3、实验流程

3.0、前提知识

STM32F407的定时器通道均可以实现输出比较功能,输出比较功能是利用当前计数值CNT与捕获/比较寄存器CRR的值作比较,如果值相等就会产生输出比较结果,此时也会产生输出比较完成中断或DMA请求

定时器产生的输出比较结果可以输出到比较通道的具体引脚上,也可以直接内部使用不输出到引脚,具体输出的电平由比较模式和输出极性共同决定

输出比较模式有①冻结:保持当前电平、②匹配时输出有效电平、③匹配时输出无效电平、④匹配时翻转输出电平、⑤强制输出有效电平和⑥强制输出无效电平六种模式

有效电平可以手动设置为高/低电平

3.1、CubeMX相关配置

请先阅读“
STM32CubeMX教程1 工程建立
”实验3.4.1小节配置RCC和SYS

3.1.1、时钟树配置

系统时钟树配置与上一实验一致,均设置为STM32F407总线能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

在Pinout & Configuration页面右边芯片引脚预览Pinout view中找到LED灯的四个控制引脚PD12、PD13、PD14和PD15,依次左键单击并配置其功能为TIM4_CHx

然后在页面左侧功能分类栏目中点开Timers栏目,单击栏目下的TIM4,并将其Channel1~4全部配置为Output Compare CHx

具体配置如下图所示

然后对启用的TIM4定时器的四个通道参数进行设置,下面对重要参数介绍

①首先对计数器参数的设置与之前讲解的一致,这里不再赘述,周期设为500ms,不分频不预装载

②四个通道的输出比较模式均选择为了Toggle on match,即当比较寄存器CCR的值和当前定时器计数值CNT匹配时翻转通道引脚输出状态,四个通道从上到下其比较寄存器的值CCR依次为999、1999、2999和3999,这表示在一个时钟500ms周期内,通道1、2、3和4分别在100ms、200ms、300ms和400ms的时候翻转其对应通道引脚的值

③输出比较四个通达均不启用预装载,通道输出有效状态为高电平,其他的模式如其字面意思较为好理解,这里不再过多阐述

具体的参数配置如下图所示

3.1.3、外设中断配置

在Pinout & Configuration页面左边System Core/NVIC中勾选TIM4全局中断,然后选择合适的中断优先级即可

3.2、生成代码

请先阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节配置Project Manager

单击页面右上角GENERATE CODE生成工程

3.2.1、外设初始化函数调用流程

在生成的工程代码主函数main()中调用了MX_TIM4_Init()函数完成了对TIM4基本定时器参数,输出比较通道1/2/3/4参数的配置

然后在HAL_TIM_OC_Init()函数中调用了HAL_TIM_OC_MspInit()函数对TIM4时钟和中断进行了使能,并对中断优先级进行了配置

最后在MX_TIM4_Init()函数末尾调用了HAL_TIM_MspPostInit()函数对TIM4的四个输出比较通道引脚进行了复用设置,从而完成了整个初始化过程

如下图所示为TIM4输出比较初始化的具体函数调用流程

3.2.2、外设中断函数调用流程

再来看看输出比较中断回调函数流程,使能定时器全局中断后在stm32f4xx_it.c中自动生成了TIM4的中断处理函数TIM4_IRQHandler

TIM4_IRQHandler调用了HAL库的定时器中断处理函数HAL_TIM_IRQHandler,这个函数负责处理所有的定时器相关中断

通过判断中断来源及相关寄存器,最终输出比较事件完成的回调函数为HAL_TIM_OC_DelayElapsedCallback(或者是HAL_TIM_PWM_PulseFinishedCallback)
(注释1)
,该函数为虚函数,需要用户重新实现

如下图所示为TIM4输出比较中断回调的具体函数调用流程

该实验目标不需要动态修改输出比较的参数,因此笔者这里没有重新在tim.c中重新实现该回调函数,需要提醒的是,输出比较事件完成时会同时进入HAL_TIM_OC_DelayElapsedCallback和HAL_TIM_PWM_PulseFinishedCallback两个回调函数中,因此请注意此实验确保注释掉PWM输出实验中HAL_TIM_PWM_PulseFinishedCallback函数体内动态修改占空比的代码,否则将达不到预期效果

3.2.3、添加其他必要代码

最后只需要在主函数中启动定时器
(注释2)
,启动输出比较通道就可以实现本实验目标,启动代码如下图所示

源代码如下

HAL_TIM_Base_Start(&htim4);
HAL_TIM_OC_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_OC_Start(&htim4, TIM_CHANNEL_2);
HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_3);
HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_4);

4、常用函数

/*启动输出比较通道*/
HAL_StatusTypeDef HAL_TIM_OC_Start(TIM_HandleTypeDef *htim, uint32_t Channel)

/*停止输出比较通道*/
HAL_StatusTypeDef HAL_TIM_OC_Stop(TIM_HandleTypeDef *htim, uint32_t Channel)
 
/*以中断方式启动输出比较通道*/
HAL_StatusTypeDef HAL_TIM_OC_Start_IT(TIM_HandleTypeDef *htim, uint32_t Channel)
 
/*停止以中断方式启动的输出比较通道*/
HAL_StatusTypeDef HAL_TIM_OC_Stop_IT(TIM_HandleTypeDef *htim, uint32_t Channel)
 
/*输出比较通道完成回调函数*/
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)

5、烧录验证

5.1、具体步骤

“设置TIM4的4个通道为输出比较到通道 -> 配置TIM4基本参数及4个输出比较通道参数 -> NVIC中勾选TIM4全局中断并设置合适中断优先级 -> 在主函数中使用HAL_TIM_Base_Start(&htim4)启动定时器TIM4 -> 然后使用HAL_TIM_OC_Start_IT(&htim4, TIM_CHANNEL_x)函数开启四个通道的输出比较”

5.2、实验现象

烧录程序,上电单片机启动后会发现开发板上的四个LED灯,按照绿、橙、红和蓝的顺序依次点亮,然后不断循环,实际现象如下图所示

使用逻辑分析仪监测TIM4的四个输出通道引脚,可以看出TIM4的四个通道输出的电平翻转周期均为500ms,并且4个通道间每个通道均间隔100ms相继翻转状态

6、注释详解

注释1
:在生成PWM波的实验中提到其使用的中断回调函数HAL_TIM_PWM_PulseFinishedCallback也可以换成输出比较完成的回调函数HAL_TIM_OC_DelayElapsedCallback,因为笔者发现在HAL库的定时器中断处理函数HAL_TIM_IRQHandler中,这两个函数是同时调用的,也就是说调用函数1,函数2也会跟着调用,所以笔者认为两个都可以使用

注释2
:笔者发现在启动输出比较/输入捕获/PWM输出时,可以不调用定时器启动函数,执行HAL_TIM_xx_Start/HAL_TIM_xx_Start_IT函数时会自动启动定时器;另外HAL_TIM_OC_Start/HAL_TIM_OC_Start_IT两个输出比较启动函数和HAL_TIM_PWM_Start/HAL_TIM_PWM_Start_IT两个PWM输出启动函数,除了函数名和一些注释外其函数体内的代码一模一样;

更多内容请浏览
OSnotes的CSDN博客