<p>打卡第十集-数码管的动态显示</p>
<h2>数码管动态刷新原理</h2>
<ul>
<li><strong>同一时刻只点亮一位数码管</strong>,其余位处于熄灭状态。</li>
<li>依次循环点亮每一位,每位的点亮时间极短(毫秒级)。</li>
<li>利用<strong>人眼视觉暂留效应</strong>(图像在视网膜上停留约1/16秒,即62.5ms以上),只要循环速度足够快,人眼就会感觉所有位<strong>同时稳定显示</strong>。</li>
</ul>
<hr />
<h2><strong>数码管动态控制原理</strong></h2>
<p><img src="data/attachment/forum/202603/23/114621o656gpbejjuhpaug.png" alt="image.png" title="image.png" /></p>
<p><strong>在动态刷新中,每位点亮时间不宜过短,通常设为1ms左右;同时需确保一个完整的扫描周期不超过20ms,即刷新率高于50Hz,以利用人眼视觉暂留效应避免闪烁。</strong></p>
<p>打卡第十一集-定时器的使用</p>
<h3>一、核心作用</h3>
<p>定时器本质上是一个<strong>自动累加的寄存器</strong>,在时钟脉冲驱动下自增。通过配置,它可以实现以下几种核心功能:</p>
<ol>
<li><strong>精准定时</strong><br />
它可以产生精确的时间间隔。例如,设定一个1毫秒的定时,时间到后,单片机就会收到标志或触发中断。这避免了使用 <code>delay</code>函数时CPU“空等”造成的资源浪费。</li>
<li><strong>精准计数</strong><br />
它可以统计外部引脚上脉冲的个数。例如,用来统计电机编码器反馈的脉冲数,从而计算电机转速,或者统计生产线上通过的产品数量。</li>
<li><strong>输入捕获</strong><br />
它可以“捕获”外部信号跳变的精确时刻。例如,用来测量遥控器红外信号的脉冲宽度,或超声波传感器的回波高电平持续时间,从而计算出距离。</li>
<li><strong>输出比较(生成波形)</strong><br />
它可以精确地在某个时刻翻转电平,最典型的就是生成 <strong>PWM波</strong>(脉宽调制)。通过调节PWM的占空比,可以实现<strong>控制LED亮度、调节直流电机转速、控制舵机角度</strong>等功能。</li>
</ol>
<h3>二、存在的意义</h3>
<p>为什么单片机需要配置独立的定时器硬件,而不是完全依赖软件循环?</p>
<ol>
<li><strong>解放CPU,提高效率</strong><br />
这是最重要的意义。如果是软件延时(如 <code>for</code>循环),CPU在执行延时期间<strong>什么都做不了</strong>,只能空转。而使用定时器中断时,CPU可以正常处理其他任务(如扫描按键、刷新屏幕),定时器时间到后会“打断”当前任务去处理定时事件,处理完再回去继续干活。这种机制是<strong>多任务操作系统</strong>能够运行在单片机上的基础。</li>
<li><strong>时间的精确性</strong><br />
软件延时受编译器优化、中断干扰、流水线状态影响,误差较大。而定时器的计时来源是<strong>晶振</strong>,是确定且稳定的,能做到微秒级甚至纳秒级的精确度。这在需要严格时序的通信(如DS18B20单总线、红外解码)或电机控制中至关重要。</li>
<li><strong>实时性</strong><br />
对于需要严格按时处理的事件,比如电机PID控制(通常需在1ms内完成一次计算),如果依赖主循环的轮询,无法保证响应时间。定时器中断可以保证“每隔1ms,无论程序执行到哪里,都能立刻执行控制算法”,从而保证系统的<strong>实时响应</strong>。</li>
<li><strong>硬件PWM输出</strong><br />
生成PWM波如果由软件模拟,CPU需要不停地在中断里翻转IO口,频率稍高就会导致CPU负载率100%。而使用定时器的PWM功能,<strong>一旦配置好,不需要CPU干预,硬件自己就能持续输出稳定的方波</strong>。CPU只需要在需要改变占空比时修改一下寄存器即可。</li>
</ol>
<h3>三、典型应用场景</h3>
<ul>
<li><strong>LED呼吸灯/电机调速</strong>:利用定时器的PWM输出功能。</li>
<li><strong>电子时钟/万年历</strong>:利用定时器产生1秒的基准中断。</li>
<li><strong>舵机控制</strong>:利用定时器产生周期20ms,高电平0.5ms~2.5ms的精准PWM。</li>
<li><strong>旋转编码器读取</strong>:利用定时器的编码器模式或外部时钟计数模式。</li>
<li><strong>红外遥控解码</strong>:利用定时器的输入捕获功能测量高低电平的时间长度。</li>
</ul>
<h3>四、总结</h3>
<p>如果把单片机比作一个<strong>人</strong>:</p>
<ul>
<li>没有定时器,就像一个人<strong>数着拍子干活</strong>(软件延时),数拍子的时候手脚不能动,效率低且不准。</li>
<li>有了定时器,就像这个人<strong>买了一个秒表和闹钟</strong>。</li>
</ul>
<p>他可以设置闹钟(定时中断):“每过1毫秒提醒我一下,我来检查传感器”;同时让秒表在后台自动计时(输入捕获):“我看一下这个脉冲持续了多久”;甚至雇了一个<strong>助手</strong>(硬件PWM)专门在那里以固定频率推拉开关,不需要自己操心。</p>
<p><strong>简而言之,定时器是单片机从“简单的顺序执行器”迈向“高效、实时、多任务嵌入式系统”的关键硬件资源。</strong></p>
<hr />
<p>定时器常见的工作模式有以下几种:</p>
<h3>1. 定时模式</h3>
<ul>
<li>内部时钟驱动,计数器从0累加,达到设定值后产生溢出中断或标志。</li>
<li>用于产生精确时间间隔,如1ms系统心跳。</li>
</ul>
<h3>2. 计数模式</h3>
<ul>
<li>外部引脚输入的脉冲驱动计数器累加。</li>
<li>用于统计脉冲个数,如编码器转速测量、流水线产品计数。</li>
</ul>
<h3>3. 输入捕获模式</h3>
<ul>
<li>检测外部信号(上升沿/下降沿),记录当前计数器的值。</li>
<li>用于测量脉冲宽度、频率、占空比,如红外解码、超声波测距。</li>
</ul>
<h3>4. 输出比较模式</h3>
<ul>
<li>计数器值与预设比较值相等时,自动翻转/置位/清零输出引脚,并可产生中断。</li>
<li>用于生成精确时序信号,或触发特定时刻的动作。</li>
</ul>
<h3>5. PWM模式(脉宽调制)</h3>
<ul>
<li>输出比较的特殊形式,硬件自动产生周期固定、占空比可调的方波。</li>
<li>用于LED调光、电机调速、舵机控制,不占用CPU资源。</li>
</ul>
<h3>6. 单脉冲模式 / 一次性模式</h3>
<ul>
<li>定时器只运行一个周期,产生一次中断或输出一个脉冲后自动停止。</li>
<li>用于需要精准单次延时的场景。</li>
</ul>
<h3>7. 级联模式</h3>
<ul>
<li>多个定时器串联,一个作为另一个的预分频或时钟源,实现更长计时或更复杂波形。</li>
</ul>
<hr />
<p><strong>一句话总结</strong>:定时器通过内部时钟做<strong>定时</strong>,通过外部脉冲做<strong>计数</strong>,通过比较/捕获做<strong>测量与波形生成</strong>。</p>
<p>打卡第十二集-计数器的使用</p>
<p><strong>计数器的用途</strong></p>
<p><strong>计数器本质上就是统计“高低电平变化次数”的硬件。</strong></p>
<p>只要外部信号能产生脉冲(上升沿或下降沿),就可以用计数器的功能来统计。常见的典型用途包括:</p>
<h3>1. 旋转测速</h3>
<p>电机或轮子上安装<strong>编码器</strong>或<strong>霍尔传感器</strong>,每转一圈输出N个脉冲。计数器统计单位时间内的脉冲数,即可算出<strong>转速</strong>。</p>
<h3>2. 流量计量</h3>
<p>水表、燃气表中的<strong>流量传感器</strong>,液体流过时带动叶轮旋转,输出对应频率的脉冲。计数器统计脉冲数,即可得到<strong>累计流量</strong>。</p>
<h3>3. 流水线计数</h3>
<p>工业产线上用<strong>光电传感器</strong>检测产品经过,物品遮挡时输出一个脉冲。计数器统计脉冲数,即可完成<strong>产品计数</strong>。</p>
<h3>4. 按键/开关状态检测</h3>
<p>机械按键按下和释放时会产生电平变化,计数器可以统计<strong>按键次数</strong>,或配合消抖电路实现可靠计数。</p>
<h3>5. 高频脉冲测量</h3>
<p>对于比CPU处理速度更快的脉冲信号(如几十kHz以上),用软件轮询容易丢脉冲,而<strong>硬件计数器</strong>可以直接捕获,确保不遗漏。</p>
<hr />
<p><strong>核心优势</strong>:计数器由<strong>硬件独立完成</strong>统计,CPU只需在需要时读取计数值,整个过程不占用CPU资源。相比用 <code>if</code>语句在软件里轮询电平变化,硬件计数器<strong>更精准、不丢脉冲、不拖慢程序</strong>。</p>
<hr />
<p><strong>自动重装载模式</strong></p>
<p><img src="data/attachment/forum/202603/23/173116zsjpndq9g2un29q2.png" alt="image.png" title="image.png" /></p>
<p>打卡第十三集-简易多任务处理</p>
<p><code>extern</code> 是 C 语言中用于<strong>声明外部变量或函数</strong>的关键字,告诉编译器:“这个变量/函数在其他文件中定义,请链接时去找。”</p>
<hr />
<h3>主要用途</h3>
<p><strong>1. 跨文件共享全局变量</strong></p>
<p>假设有两个文件:<code>main.c</code> 和 <code>counter.c</code>,想在 <code>main.c</code> 中使用 <code>counter.c</code> 中定义的全局变量 <code>count</code>。</p>
<ul>
<li>
<p><code>counter.c</code> 中定义:<br />
<strong>c</strong></p>
<pre><code>int count = 0; // 全局变量定义(分配内存)
</code></pre>
</li>
<li>
<p><code>main.c</code> 中声明:<br />
<strong>c</strong></p>
<pre><code>extern int count; // 声明,不是定义,不分配内存
</code></pre>
</li>
</ul>
<p>这样 <code>main.c</code> 就可以访问 <code>count</code> 了。</p>
<p><strong>2. 在头文件中声明全局变量</strong></p>
<p>通常做法:在头文件 <code>.h</code> 中用 <code>extern</code> 声明变量,在某个 <code>.c</code> 文件中实际定义。</p>
<ul>
<li>
<p><code>counter.h</code>:<br />
<strong>c</strong></p>
<pre><code>extern int count; // 声明
void increment(void);
</code></pre>
</li>
<li>
<p><code>counter.c</code>:<br />
<strong>c</strong></p>
<pre><code>int count = 0; // 定义
void increment(void) {
count++;
}
</code></pre>
</li>
<li>
<p><code>main.c</code> 包含头文件即可:<br />
<strong>c</strong></p>
<pre><code>#include "counter.h"
count = 10; // 可以直接访问
</code></pre>
</li>
</ul>
<p><strong>3. 函数声明</strong></p>
<p>函数默认是 <code>extern</code> 的,即可以跨文件调用。显式写 <code>extern</code> 不是必须的,但可增强可读性。</p>
<p><strong>c</strong></p>
<pre><code>// 这两种写法等价
void func(void);
extern void func(void);
</code></pre>
<hr />
<h3>重要原则</h3>
<ul>
<li><strong>声明 vs 定义</strong>:<code>extern</code> 只做声明,不分配内存;定义才分配内存。一个变量只能定义一次,但可以声明多次。</li>
<li><strong>作用域</strong>:<code>extern</code> 声明的变量具有文件作用域,从声明处开始到文件末尾有效。</li>
<li><strong>与 <code>static</code> 对立</strong>:<code>static</code> 修饰的全局变量只能在本文件内使用,<code>extern</code> 用于跨文件访问。</li>
</ul>
<hr />
<h3>常见误区</h3>
<ul>
<li>在头文件中直接定义变量(不加 <code>extern</code>)会导致多个源文件包含该头文件时出现重复定义错误。</li>
<li><code>extern</code> 不能用于局部变量(函数内部),只能用于全局变量和函数。</li>
</ul>
<hr />
<p><strong>一句话总结</strong>:<code>extern</code> 是 C 语言中实现<strong>跨文件共享全局变量</strong>的关键手段,遵循“头文件声明,源文件定义”的原则。</p>
<hr />
<hr />
<p><code>static</code> 在 C 语言中根据修饰对象不同,有<strong>三种作用</strong>:</p>
<hr />
<h3>1. 修饰局部变量(静态局部变量)</h3>
<ul>
<li><strong>生命周期</strong>:从程序启动到结束,不随函数调用结束而销毁。</li>
<li><strong>作用域</strong>:仍局限在函数内部,外部无法访问。</li>
<li><strong>初始化</strong>:只执行一次,默认初始化为 0。</li>
</ul>
<p><strong>c</strong></p>
<pre><code>void func() {
static int count = 0;// 只初始化一次
count++;
printf("%d", count); // 每次调用递增
}
</code></pre>
<h3>2. 修饰全局变量(静态全局变量)</h3>
<ul>
<li><strong>作用域</strong>:限制在当前文件内,其他文件无法通过 <code>extern</code> 访问。</li>
<li>用于实现<strong>文件内部私有变量</strong>,防止命名冲突。</li>
</ul>
<p><strong>c</strong></p>
<pre><code>// file1.c
static int secret = 100; // 仅 file1.c 可见
</code></pre>
<h3>3. 修饰函数(静态函数)</h3>
<ul>
<li><strong>作用域</strong>:限制在当前文件内,其他文件无法调用。</li>
<li>用于实现<strong>文件内部私有函数</strong>,仅被本文件的其他函数使用。</li>
</ul>
<p><strong>c</strong></p>
<pre><code>// file1.c
static void helper() { // 仅 file1.c 可调用
// ...
}
</code></pre>
<hr />
<p>模块化编程就是将程序拆分成<strong>独立、可复用、职责清晰</strong>的模块,每个模块负责一个特定的功能。</p>
<hr />
<h3>一、模块化的核心思想</h3>
<ul>
<li><strong>接口与实现分离</strong>:头文件(<code>.h</code>)公开接口(函数声明、宏、类型),源文件(<code>.c</code>)隐藏实现细节(函数定义、私有变量)。</li>
<li><strong>信息隐藏</strong>:模块内部的变量和辅助函数对外不可见,通过 <code>static</code> 实现。</li>
<li><strong>依赖清晰</strong>:模块之间通过头文件建立依赖,避免全局变量直接共享。</li>
</ul>
<hr />
<h3>二、典型模块结构</h3>
<p>以 <code>timer</code> 模块为例:</p>
<p><strong>timer.h</strong>(对外接口)</p>
<p><strong>c</strong></p>
<pre><code>#ifndef TIMER_H
#define TIMER_H
// 公共函数声明
void timer_init(void);
void timer_start(void);
unsigned int timer_get_ms(void);
#endif
</code></pre>
<p><strong>timer.c</strong>(内部实现)</p>
<p><strong>c</strong></p>
<pre><code>#include "timer.h"
// 静态全局变量(仅本文件可见)
static unsigned int ms_counter = 0;
static void (*callback)(void) = NULL;// 私有数据
// 静态函数(内部辅助)
static void timer_isr(void) {
ms_counter++;
if (callback) callback();
}
// 公共函数定义
void timer_init(void) {
// 配置硬件定时器
// 注册中断,调用 timer_isr
}
void timer_start(void) {
// 启动定时器
}
unsigned int timer_get_ms(void) {
return ms_counter;
}
</code></pre>
<p><strong>main.c</strong>(使用模块)</p>
<p><strong>c</strong></p>
<pre><code>#include "timer.h"
int main(void) {
timer_init();
timer_start();
while(1) {
unsigned int t = timer_get_ms();
// ...
}
}
</code></pre>
<hr />
<h3>三、模块化中的关键修饰符</h3>
<table>
<thead>
<tr>
<th>修饰符</th>
<th>在模块化中的作用</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>static</code></td>
<td>修饰全局变量或函数,将其作用域<strong>限制在本文件</strong>,实现私有成员。</td>
</tr>
<tr>
<td><code>extern</code></td>
<td>在头文件中声明变量/函数,供其他模块使用。通常与 <code>#include</code> 配合,无需显式写 <code>extern</code>。</td>
</tr>
</tbody>
</table>
<hr />
<h3>四、模块化的好处</h3>
<ol>
<li><strong>可维护性</strong>:修改一个模块不影响其他模块。</li>
<li><strong>可复用性</strong>:模块可以在不同项目中直接复用。</li>
<li><strong>可测试性</strong>:可以单独编译和测试每个模块。</li>
<li><strong>协作开发</strong>:不同开发者负责不同模块,通过接口约定协作。</li>
</ol>
<hr />
<p>在模块化编程中,为重要函数添加清晰的说明注释,是<strong>保证代码可读性、可维护性</strong>的关键。通常采用 <strong>Doxygen 风格</strong>的注释,放在函数声明(头文件)或定义(源文件)之前。</p>
<hr />
<h3>标准函数说明应包含的内容</h3>
<ul>
<li><strong>功能简述</strong>:函数是做什么的</li>
<li><strong>参数</strong>:每个参数的含义、取值范围、输入/输出方向</li>
<li><strong>返回值</strong>:返回值的含义</li>
<li><strong>注意事项</strong>:调用前提、副作用、线程/中断安全等</li>
</ul>
<hr />
<h3>示例(头文件中)</h3>
<p><strong>c</strong></p>
<pre><code>/**
* @brief 初始化定时器模块
* @param freq 定时器工作频率,单位 Hz,范围 1000~100000
* @param mode 工作模式:0=单次模式,1=循环模式
* @return0: 成功, -1: 参数错误, -2: 硬件初始化失败
* @note 必须在调用 timer_start() 之前调用本函数
* @warning 若 freq 超出范围,函数会返回 -1 并保持原有配置不变
*/
int timer_init(unsigned int freq, int mode);
</code></pre>
<hr />
<h3>常用标签</h3>
<table>
<thead>
<tr>
<th>标签</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>@brief</code></td>
<td>简要描述</td>
</tr>
<tr>
<td><code>@param</code></td>
<td>参数说明(可标注 /)</td>
</tr>
<tr>
<td><code>@return</code> 或 <code>@retval</code></td>
<td>返回值说明</td>
</tr>
<tr>
<td><code>@note</code></td>
<td>补充说明</td>
</tr>
<tr>
<td><code>@warning</code></td>
<td>警告信息</td>
</tr>
<tr>
<td><code>@see</code></td>
<td>关联其他函数</td>
</tr>
</tbody>
</table>
<hr />
<h3>为什么要加这些说明?</h3>
<ul>
<li><strong>方便他人理解</strong>:不需要阅读函数实现就知道怎么用</li>
<li><strong>自动生成文档</strong>:配合 Doxygen 等工具可直接生成 API 文档</li>
<li><strong>减少使用错误</strong>:明确参数范围、注意事项,降低误用风险</li>
<li><strong>方便代码审查</strong>:注释清晰,审查者能快速判断接口设计是否合理</li>
</ul>
<p>打卡第十四集-矩阵按键</p>
<h3>一、什么是矩阵按键?</h3>
<p>当单片机需要连接很多按键时,如果每个按键都独立占用一个I/O口,会非常浪费资源。<br />
矩阵按键采用 <strong>行线</strong> 和 <strong>列线</strong> 交叉的方式,将按键排列成矩阵。</p>
<ul>
<li>如果有 <strong>m 行 × n 列</strong>,就可以实现 <strong>m × n</strong> 个按键。</li>
<li>占用的I/O口数量仅为 <strong>m + n</strong>,而不是 m×n。</li>
</ul>
<p>例如:4×4 矩阵键盘,占用 8 个 I/O 口,却能实现 16 个按键,大大节省了引脚资源。</p>
<hr />
<h3>二、矩阵按键的结构与原理</h3>
<h4>1. 硬件连接</h4>
<ul>
<li><strong>行线</strong>:通常连接到单片机的输出引脚(或输入引脚,带内部上拉)。</li>
<li><strong>列线</strong>:通常连接到单片机的输入引脚(带内部上拉)。</li>
<li>每个按键位于某一行和某一列的交叉点,按下时将该行和该列连通。</li>
</ul>
<h4>2. 工作原理(扫描法)</h4>
<p>以 <strong>行扫描法</strong> 为例,步骤如下:</p>
<ol>
<li><strong>初始化</strong>:所有行线输出低电平,所有列线设置为输入且上拉有效。</li>
<li><strong>检测是否有按键按下</strong>:读取所有列线,如果某列变为低电平,说明该列有按键按下。</li>
<li><strong>逐行扫描确定具体按键</strong>:
<ul>
<li>依次将每一行输出低电平,其他行输出高电平(或高阻态)。</li>
<li>读取所有列线状态。</li>
<li>如果某列读回低电平,则说明当前扫描的行与该列交叉处的按键被按下。</li>
</ul>
</li>
<li><strong>消除抖动</strong>:通常加入延时(如10-20ms)后再次确认状态,避免误判。</li>
</ol>
<p>除了行扫描法,还有 <strong>反转法</strong>(先输出行扫描码,再交换方向读取列值)等,原理类似。</p>
<hr />
<h3>三、矩阵按键的优缺点</h3>
<table>
<thead>
<tr>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody>
<tr>
<td>节省I/O口资源,适合按键数量多的场景</td>
<td>不能直接检测多个按键同时按下(存在组合键识别问题,但可通过软件优化部分支持)</td>
</tr>
<tr>
<td>电路结构简单,成本低</td>
<td>扫描过程需要软件配合,占用一定的CPU时间</td>
</tr>
<tr>
<td>易于扩展,可以轻松组成4×4、8×8等</td>
<td>布线稍复杂,尤其是在PCB设计时</td>
</tr>
</tbody>
</table>
<hr />
<h3>四、实际应用中的注意事项</h3>
<ol>
<li><strong>按键消抖</strong><br />
机械按键在按下和释放瞬间会产生抖动,软件中一般通过延时(10~20ms)或定时器采样去抖。</li>
<li><strong>组合键与多键冲突</strong><br />
在矩阵电路中,如果同时按下多个按键,可能会出现“鬼键”现象(检测到实际未按下的键)。<br />
解决方法:增加二极管(每个按键串联一个二极管)隔离,或通过软件逻辑识别无效组合。</li>
<li><strong>行列复用</strong><br />
在一些低引脚数的单片机上,行线和列线可以复用其他功能,只要在按键扫描时切换引脚方向即可。</li>
</ol>
<hr />
<h3>五、与独立按键的对比</h3>
<ul>
<li><strong>独立按键</strong>:每个按键占用一个I/O口,程序简单,适合按键数量少(≤4个)的场景。</li>
<li><strong>矩阵按键</strong>:程序相对复杂,但能在有限引脚下支持更多按键,是嵌入式设备、键盘、控制面板等常用方案。</li>
</ul>
<hr />
<h3>总结</h3>
<p>矩阵按键是一种通过行列交叉结构,用较少的I/O口实现多个按键的硬件设计方法。其核心在于利用单片机的引脚方向控制和扫描算法来识别具体按键。你理解的“减少I/O口占用”正是它的主要优势,在实际开发中,掌握矩阵按键的扫描原理是嵌入式入门的重要一环。</p>
<hr />
<p><strong>矩阵按键的控制原理</strong></p>
<p><img src="data/attachment/forum/202603/23/215338m90y99weqjfjwvaf.png" alt="image.png" title="image.png" /></p>
<p>按键识别原理:端口默认为高电平,当检测到引脚为低电平时,表示按键按下。</p>
<p>具体识别步骤如下:</p>
<ol>
<li><strong>列检测</strong>:将 P0.0–P0.3 设为低电平,P0.6–P0.7 设为高电平。若有按键按下,对应列的 I/O 引脚会被拉低,从而确定按下按键所在的列。</li>
<li><strong>行检测</strong>:将 P0.0–P0.3 设为高电平,P0.6–P0.7 设为低电平。若有按键按下,对应行的 I/O 引脚会被拉低,从而确定按下按键所在的行。</li>
<li><strong>按键定位</strong>:结合行与列的信息,即可唯一确定被按下的按键。</li>
</ol>
<p>打卡第十五集-外部中断</p>
<p><strong>中断和中断系统</strong></p>
<h2>一、什么是中断?</h2>
<p><strong>中断</strong> 是指CPU在正常运行程序时,由于内部或外部事件的触发,暂时停止当前程序的执行,转而去执行一个专门处理该事件的程序(称为<strong>中断服务程序</strong>),处理完毕后再返回原程序继续执行的过程。</p>
<p><strong>形象比喻</strong>:<br />
你正在看书(主程序),突然电话响了(中断请求),你放下书去接电话(中断响应),接完电话后回到刚才的地方继续看书(中断返回)。</p>
<hr />
<h2>二、为什么需要中断?</h2>
<p>如果没有中断,CPU只能通过<strong>轮询</strong>(不断查询)的方式检测外部事件,这会占用大量CPU时间,尤其在事件发生频率不高时效率极低。</p>
<p>中断的优势:</p>
<ul>
<li><strong>实时性</strong>:事件发生时能立即得到响应。</li>
<li><strong>高效性</strong>:CPU平时可以专注于主任务,只在必要时处理突发事件。</li>
<li><strong>低功耗</strong>:空闲时CPU可以进入休眠模式,通过中断唤醒。</li>
</ul>
<hr />
<h2>三、中断系统的组成</h2>
<p>一个完整的中断系统通常包含以下几个部分:</p>
<h3>1. 中断源</h3>
<p>能产生中断请求的来源。常见的中断源:</p>
<ul>
<li><strong>外部中断</strong>:由外部引脚电平变化触发(如按键、传感器)。</li>
<li><strong>定时器中断</strong>:由内部定时器溢出或匹配触发。</li>
<li><strong>通信中断</strong>:如串口接收/发送完成、I²C、SPI等。</li>
<li><strong>ADC转换完成中断</strong>等。</li>
</ul>
<h3>2. 中断控制器</h3>
<p>负责管理多个中断请求,包括:</p>
<ul>
<li><strong>使能控制</strong>:允许或禁止某个中断。</li>
<li><strong>优先级管理</strong>:多个中断同时发生时,决定先响应哪一个。</li>
<li><strong>中断标志</strong>:记录哪个中断发生了。</li>
</ul>
<p>在简单的单片机(如8051、AVR、STM32)中,中断控制器的功能集成在CPU内部或外设中。</p>
<h3>3. 中断向量表</h3>
<p>每个中断源对应一个固定的<strong>中断向量地址</strong>。当中断发生时,CPU会自动跳转到该地址执行对应的中断服务程序。</p>
<h3>4. 中断服务程序(ISR)</h3>
<p>是专门处理中断事件的函数。需要满足:</p>
<ul>
<li>尽量短小精悍,避免执行耗时操作。</li>
<li>保护现场(保存被中断程序的寄存器状态)。</li>
<li>清除中断标志,防止重复进入。</li>
</ul>
<hr />
<h2>四、中断处理流程</h2>
<ol>
<li><strong>中断请求</strong>:中断源产生有效中断信号。</li>
<li><strong>中断判优</strong>:若多个中断同时请求,中断控制器根据优先级选择最高优先级的中断。</li>
<li><strong>中断响应</strong>:CPU执行完当前指令后,保护断点(下一条指令的地址)和现场(关键寄存器),然后跳转到对应的中断服务程序。</li>
<li><strong>中断处理</strong>:执行ISR中的代码。</li>
<li><strong>中断返回</strong>:恢复现场和断点,继续执行原程序。</li>
</ol>
<hr />
<h2>五、中断优先级与嵌套</h2>
<ul>
<li><strong>优先级</strong>:每个中断源可以设定一个优先级。高优先级的中断可以打断低优先级的中断,形成<strong>中断嵌套</strong>。</li>
<li>在单片机中,通常支持多个优先级级别,例如STM32的NVIC支持4~16级可编程优先级。</li>
<li>优先级相同的多个中断,若同时发生,会按照硬件指定的“自然优先级”顺序响应。</li>
</ul>
<hr />
<h2>六、中断与矩阵按键的结合</h2>
<p>在矩阵按键检测中,如果采用轮询扫描,CPU需要不断地去读取行、列状态,即便没有按键按下也会空耗资源。</p>
<p>使用中断可以优化:</p>
<ul>
<li>将矩阵按键的<strong>某一行或某一列</strong>连接到一个外部中断引脚(比如所有列线通过“与”逻辑接到一个外部中断引脚)。</li>
<li>当有按键按下时,该引脚电平变化触发中断,在中断服务程序中再去执行矩阵扫描(此时只进行一次完整的行列扫描),从而确定具体哪个按键被按下。</li>
<li>这样CPU平时可以处理其他任务,只在有按键时才介入,既节省了资源,又保证了按键响应的实时性。</li>
</ul>
<hr />
<h2>七、单片机中的典型中断应用</h2>
<table>
<thead>
<tr>
<th>中断类型</th>
<th>典型用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>外部中断</td>
<td>按键、传感器信号、紧急停机信号</td>
</tr>
<tr>
<td>定时器中断</td>
<td>产生精确的时间基准、PWM、周期性任务调度</td>
</tr>
<tr>
<td>串口中断</td>
<td>接收/发送数据,实现异步通信</td>
</tr>
<tr>
<td>ADC中断</td>
<td>完成一次模拟信号采集后及时读取结果</td>
</tr>
<tr>
<td>DMA中断</td>
<td>数据传输完成通知</td>
</tr>
</tbody>
</table>
<hr />
<h2>八、中断使用注意事项</h2>
<ul>
<li><strong>临界区保护</strong>:在中断中修改的全局变量,在主循环中访问时可能需要关闭中断或使用原子操作,防止数据不一致。</li>
<li><strong>中断服务程序不能阻塞</strong>:不要在ISR中使用 <code>delay</code>或等待标志等耗时操作。</li>
<li><strong>中断优先级设置要合理</strong>:高优先级中断不宜过多,否则可能导致低优先级中断长时间得不到响应。</li>
<li><strong>清除中断标志</strong>:大多数中断需要软件清除标志位,否则会重复进入。</li>
</ul>
<hr />
<p><strong>什么是外部中断</strong></p>
<p>外部中断就是在单片机的一个引脚上,由于外部因素导致了一个电平的变化(比如由高变低),而通过捕获这个变化,单片机内部自主运行的程序就会被暂时打断,转而去执行相应的中断处理程序,执行完后又回到原来中断的地方继续执行原来的程序。</p>
<hr />
<p>打卡第十六集-IO中断</p>
<p><strong>基本概念</strong></p>
<p>单片机的IO引脚除了作为普通的输入输出外,往往可以复用为中断输入引脚。当该引脚上出现预设的电平变化(如上升沿、下降沿或低电平)时,单片机硬件会自动暂停当前正在执行的主程序,保存现场,并跳转到对应的中断服务程序执行。处理完中断后,再返回到主程序继续执行。</p>
<hr />
<h3>触发方式</h3>
<p>单片机IO中断通常支持多种触发方式,可根据需要选择:</p>
<ul>
<li>
<p><strong>边沿触发</strong></p>
<ul>
<li>上升沿触发:引脚电平从低到高变化时产生中断。</li>
<li>下降沿触发:引脚电平从高到低变化时产生中断。</li>
<li>双边沿触发:上升沿和下降沿均产生中断(部分单片机支持)。</li>
</ul>
</li>
<li>
<p><strong>电平触发</strong></p>
<ul>
<li>低电平触发:引脚为低电平时持续产生中断(需注意中断服务程序需清除中断标志或改变电平,否则可能反复进入中断)。</li>
<li>高电平触发:引脚为高电平时产生中断(较少见)。</li>
</ul>
</li>
</ul>
<hr />
<p><img src="data/attachment/forum/202603/30/170547aatrrk2keygak1kk.png" alt="image.png" title="image.png" /></p>
<p><img src="data/attachment/forum/202603/30/170647nnz5jb3h0gdaqzd4.png" alt="image.png" title="image.png" /></p>
<p>打卡第十七集-模数转换器ADC</p>
<p><strong>模数转换器</strong>即A/D转换器,或简称ADC(Analog-to-digital converter),通常是指一个将模拟信号转变为数字信号的电子元件。</p>
<p>模拟信号->电压</p>
<p>数字信号->0和1组成的二进制数</p>
<hr />
<h2>模数转换的核心过程</h2>
<p>ADC将连续的模拟电压转换为离散的数字量,一般经过三个步骤:</p>
<h3>1. 采样与保持</h3>
<ul>
<li><strong>采样</strong>:以固定的时间间隔对模拟信号进行“快照”,获取瞬间的电压值。</li>
<li><strong>保持</strong>:由于转换需要一定时间,采样后的电压必须稳定不变,直到转换完成。采样保持电路会“冻结”该电压。</li>
</ul>
<h3>2. 量化</h3>
<p>将采样后的电压与参考电压进行比较,将其归入有限个离散的电压等级。比如,一个8位ADC会将输入电压分为256个等级(2⁸)。</p>
<h3>3. 编码</h3>
<p>将量化后的等级用二进制数字表示。例如,0V对应0x00,参考电压对应0xFF。</p>
<hr />
<p><img src="data/attachment/forum/202603/30/173631oret1loucwlttybr.png" alt="image.png" title="image.png" /></p>
<p>打卡第二十集-串口通信</p>
<p>通信指设备之间通过一定的协议进行的信息交换。</p>
<h3>串口通信核心概念</h3>
<ul>
<li><strong>串行 vs 并行</strong><br />
串行通信逐位传输,节省引脚、适合长距离;并行通信多位同时传输,速度快但线缆多、抗干扰差。</li>
<li><strong>异步 vs 同步</strong><br />
串口通信多为<strong>异步</strong>,无独立时钟线,依赖双方约定的波特率;同步通信需时钟线,如I²C、SPI。</li>
<li><strong>全双工/半双工/单工</strong>
<ul>
<li><strong>全双工</strong>:可同时收发(如RS-232)。</li>
<li><strong>半双工</strong>:可分时收发(如RS-485)。</li>
<li><strong>单工</strong>:单向传输。</li>
</ul>
</li>
</ul>
<hr />
<h3>二、物理层:电气标准与连接</h3>
<h4>1. 常见电气接口</h4>
<table>
<thead>
<tr>
<th>标准</th>
<th>逻辑电平</th>
<th>特点</th>
<th>典型应用</th>
</tr>
</thead>
<tbody>
<tr>
<td>TTL</td>
<td>0V=0,3.3V/5V=1</td>
<td>短距离,单片机直接使用</td>
<td>开发板、传感器</td>
</tr>
<tr>
<td>RS-232</td>
<td>-3~-15V=1,+3~+15V=0</td>
<td>点对点,15米内</td>
<td>老式PC、工业设备</td>
</tr>
<tr>
<td>RS-485</td>
<td>差分电压差(A-B)决定逻辑</td>
<td>多点通信,1200米以上,抗干扰</td>
<td>工业总线、智能仪表</td>
</tr>
</tbody>
</table>
<h4>2. 最小连接(三线制)</h4>
<ul>
<li><strong>TXD</strong> → 对方 <strong>RXD</strong></li>
<li><strong>RXD</strong> → 对方 <strong>TXD</strong></li>
<li><strong>GND</strong> → 对方 <strong>GND</strong></li>
</ul>
<p>*注:TTL与RS-232互连需电平转换芯片(如MAX3232)。*</p>
<hr />
<h3>三、协议层:通信参数</h3>
<p>通信双方必须完全一致以下参数:</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>说明</th>
<th>常用值</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>波特率</strong></td>
<td>每秒传输的符号数(bps),决定位宽时间。</td>
<td>9600, 115200, 460800</td>
</tr>
<tr>
<td><strong>数据位</strong></td>
<td>实际数据的位数。</td>
<td>8(常用),7,5</td>
</tr>
<tr>
<td><strong>停止位</strong></td>
<td>标志帧结束的位数。</td>
<td>1(常用),2</td>
</tr>
<tr>
<td><strong>校验位</strong></td>
<td>奇/偶/无校验,用于简单检错。</td>
<td>None, Even, Odd</td>
</tr>
</tbody>
</table>
<p><strong>数据帧结构</strong>:<br />
起始位(1位低电平) + 数据位(LSB在前) + 校验位(可选) + 停止位(高电平)</p>
<p><img src="data/attachment/forum/202603/30/183106svswr4ozouwvu4mw.png" alt="image.png" title="image.png" /></p>
<p><img src="data/attachment/forum/202603/30/183128rrqqwwccy9cqfcv4.png" alt="image.png" title="image.png" /></p>
<p>打卡第二十一集-串口应用</p>
<p><img src="data/attachment/forum/202603/30/210601dnxhiu74udxiywwh.png" alt="image.png" title="image.png" /></p>
<p>TXD几和RXD几表示这是第几组串口</p>
<p>TXD_几和RXD_几表示这是该组串口的第几个通道</p>
<p>芯片与芯片之间的通讯:如果通信协议和电平都一样,直接TX和RX链接就可以通信。</p>
<hr />
<p><strong>驱动(Device Driver)</strong> 是操作系统与硬件设备之间的“翻译官”和“管理者”。它是一个软件程序,让操作系统(如Windows、Linux)能够识别硬件并与之通信,而无需上层应用程序关心硬件的具体电路细节。</p>
<hr />
<h3>一、驱动的本质:软硬件的桥梁</h3>
<p>硬件设备(如USB转串口模块、显卡、网卡)由电子电路构成,只能识别高低电平、执行指令;而操作系统和应用程序运行在抽象的软件层面。驱动的作用就是<strong>将操作系统通用的“打开、读写、关闭”等抽象操作,转换为硬件能够理解的寄存器配置、时序信号和数据传输</strong>。</p>
<p><strong>没有驱动</strong>,操作系统只能识别设备的存在(如“未知设备”),但不知道如何控制它。</p>
<hr />
<h3>二、驱动的主要作用</h3>
<h4>1. 设备识别与初始化</h4>
<ul>
<li>向操作系统报告设备的基本信息(厂商ID、设备ID、功能)。</li>
<li>对硬件进行上电、复位、初始状态配置,使其进入可用状态。</li>
</ul>
<h4>2. 提供统一的访问接口</h4>
<ul>
<li>为上层应用提供标准化的API(如Windows下的 <code>CreateFile</code>、<code>ReadFile</code>、<code>WriteFile</code>;Linux下的文件操作 <code>open</code>、<code>read</code>、<code>write</code>)。</li>
<li>应用程序可以像操作普通文件一样操作硬件,无需了解底层时序。</li>
</ul>
<h4>3. 数据转换与传输</h4>
<ul>
<li>将系统内存中的数据转换为硬件能识别的格式(如串口驱动将字节转换为带有起始位、停止位的串行比特流)。</li>
<li>管理数据的缓冲、流控、超时重试等机制。</li>
</ul>
<h4>4. 资源管理与冲突解决</h4>
<ul>
<li>管理硬件占用的中断号(IRQ)、I/O地址、DMA通道等稀缺资源。</li>
<li>协调多个程序同时访问同一设备(如通过互斥锁、队列)。</li>
</ul>
<h4>5. 错误处理与电源管理</h4>
<ul>
<li>检测硬件异常(如断开连接、通信超时),向上层报告错误码。</li>
<li>在系统休眠/唤醒时对硬件进行相应的电源状态切换。</li>
</ul>
<hr />
<h3>三、串口通信中的驱动实例</h3>
<p>以你之前关注的<strong>USB转TTL串口模块</strong>(如CH340、CP2102)为例,驱动的作用非常典型:</p>
<ol>
<li><strong>无驱动时</strong>:插入模块,系统弹出“无法识别的USB设备”,设备管理器中出现黄色感叹号。操作系统只知道有一个USB设备插入了,但不知道它是串口,也不知道如何与之通信。</li>
<li><strong>安装驱动后</strong>:
<ul>
<li>驱动告知操作系统:这是一个“串行通信设备”。</li>
<li>操作系统为其创建一个虚拟COM口(如Windows下的COM3,Linux下的 <code>/dev/ttyUSB0</code>)。</li>
<li>驱动将上层应用程序对COM口的读写操作,转化为USB总线上的批量传输,进而由模块硬件转换为TTL串口电平。</li>
</ul>
</li>
</ol>
<p><strong>结果</strong>:你的串口调试助手可以通过 <code>COM3</code> 打开设备,像操作传统物理串口一样进行收发数据,而实际上底层是通过USB传输的——这一切转换都由驱动完成。</p>
<hr />
<h3>四、驱动的分类</h3>
<table>
<thead>
<tr>
<th>类型</th>
<th>特点</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>内核驱动</strong></td>
<td>运行在内核态,直接与硬件交互</td>
<td>显卡驱动、网卡驱动、USB主控驱动</td>
</tr>
<tr>
<td><strong>用户态驱动</strong></td>
<td>运行在用户态,通常通过内核驱动间接访问硬件</td>
<td>某些打印机驱动、虚拟串口驱动</td>
</tr>
<tr>
<td><strong>通用驱动</strong></td>
<td>操作系统自带,支持标准设备</td>
<td>CDC类驱动(用于简易USB串口)、HID驱动</td>
</tr>
<tr>
<td><strong>私有驱动</strong></td>
<td>硬件厂商提供,实现特定功能</td>
<td>CH340驱动、FTDI驱动、CP2102驱动</td>
</tr>
</tbody>
</table>
<hr />
<h3>五、常见场景中的驱动</h3>
<ul>
<li><strong>单片机开发</strong>:使用USB转TTL模块时,需要安装对应芯片(CH340/CP2102/FT232)的驱动,否则电脑无法识别出COM口。</li>
<li><strong>显卡</strong>:没有显卡驱动,屏幕只能显示基本画面(VGA兼容模式),无法实现高分辨率、3D加速、多显示器。</li>
<li><strong>打印机</strong>:安装驱动后,操作系统才能知道打印机的具体指令集(如PCL、PostScript),将文档转换为打印机识别的数据。</li>
<li><strong>网卡</strong>:驱动负责将网络协议栈的IP包转换为网卡能够发送的物理层信号。</li>
</ul>
<hr />
<h3>六、总结</h3>
<p><strong>驱动是硬件在操作系统中的“代言人”</strong>。它屏蔽了硬件的物理细节,让应用程序可以用统一的方式操作不同设备。对于串口通信而言,驱动就是让电脑能够通过USB口“模拟”出一个传统串口的软件层,从而让你能够用调试助手、Python脚本与单片机进行通信。</p>