我电容发光了 发表于 2025-11-14 15:59:14

学习STC8H8K64U

系统时钟
一、学习目标

[*]了解系统时钟概念
[*]了解时钟周期概念
[*]了解指令周期(机器周期)概念
二、学习内容
(一)时钟与周期

[*]系统时钟
系统时钟是计算机中用于控制各个设备协调工作的“节拍器”。
它是 CPU 的主频,是 CPU 和外设工作的基础,常用单位为 Hz(赫兹),如 1MHz、10MHz 等。
系统时钟的时钟信号通常由晶振提供。
STC8H 单片机支持 外部晶振 和 内部晶振 两种时钟源,可以通过相应配置来选择。

[*]时钟周期
时钟周期是系统时钟完成一个完整周期所需要的时间。
时钟周期的倒数就是时钟频率(每秒的时钟周期数)。
例如:
若 STC8H 时钟频率为 24MHz,则每个时钟周期时间为:
1 / 24MHz ≈ 41.67ns

[*]机器周期(指令周期)
机器周期也叫指令周期,是 CPU 执行一条指令所需的时间。
早期 STC 单片机的机器周期 = 12 个时钟周期。
现在的 STC8H 可以配置为两种模式:1T 模式 或 12T 模式。

[*]12T 模式:

[*]当系统时钟为 24MHz 时
[*]每个机器周期时间:12 × 41.67ns ≈ 500ns
[*]1T 模式:

[*]当系统时钟为 24MHz 时
[*]每个机器周期时间:1 × 41.67ns ≈ 41.67ns
(二)NOP 指令
NOP 指令是汇编中的一条指令,全称 “No Operation”,即“不做任何操作”。
特点:

[*]不改变寄存器内容;
[*]不修改存储器数据;
[*]常用于 延时 或 调整代码执行节奏。
在大多数处理器中,NOP 指令会被翻译为一条或多条机器指令,从而实现“什么都不干,但占用时间”的效果。
在 STC8H 单片机中:

[*]NOP 指令是一条 1 字节长的指令;
[*]不进行任何运算,只消耗一个指令周期。
用途包括:

[*]作为程序中的微小延时单元(可以理解为“睡一个 NOP 指令周期”);
[*]用来填充未使用空间,使程序达到某种对齐要求,提升执行效率。
(三)库函数中的系统时钟配置
在 config.h 中配置系统主时钟频率,例如:
//#define MAIN_Fosc                22118400L      // 定义主时钟
//#define MAIN_Fosc                12000000L      // 定义主时钟
//#define MAIN_Fosc                11059200L      // 定义主时钟
//#define MAIN_Fosc               5529600L      // 定义主时钟
#define MAIN_Fosc                24000000L      // 定义主时钟
根据实际使用的晶振或系统时钟设置 MAIN_Fosc。
注意:
系统时钟配置确定后,烧录软件中的时钟频率设置必须与这里保持一致,否则容易出现各种“诡异问题”(例如波特率不对、延时不准等)。
(四)测试不同时钟下的执行周期
思路:
让程序在一高一低两个电平之间快速翻转,并在中间插入一个指令周期(例如 NOP1),然后用示波器观察引脚电平的高、低电平宽度。切换不同主频,观察输出波形变化,从而直观体会主频不同带来的影响。
示例代码:
#include "config.h"
#include "GPIO.h"
#include "delay.h"

void GPIO_config(void) {
    GPIO_InitTypeDef GPIO_InitStructure;   // 结构定义
    GPIO_InitStructure.Pin= GPIO_Pin_3;    // 指定要初始化的 IO
    GPIO_InitStructure.Mode = GPIO_PullUp;   // GPIO_PullUp, GPIO_HighZ, GPIO_OUT_OD, GPIO_OUT_PP
    GPIO_Inilize(GPIO_P5, &GPIO_InitStructure); // 初始化
}

int main() {
    GPIO_config();
   
    while (1) {
      P53 = 1;
      NOP1();   // 睡一个指令周期
      P53 = 0;
      //NOP1();
    }
}
在不同主频下,用示波器观察 P5.3 引脚高电平和低电平的持续时间。
例如:

[*]24MHz 主频下的波形(此处可在富文本中插入示波器截图)
[*]12MHz 主频下的波形
[*]6MHz 主频下的波形
小结:

[*]主频越高,执行速度越快,同样一段代码用时越短;
[*]主频越高,电磁干扰也越强,设计和布线要求更高,更容易出现信号完整性问题。



神农鼎 发表于 2025-11-14 16:11:26

要 做到 USB不停电下载;
要 尝试 图形化配置外设;
推荐优先看的 printf_usb("Hello World !\r\n")及usb不停电下载, 演示视频链接
从 www.STCAI.com
下载 最新的 AiCube-ISP-V6.96F 或以上版本软件 !
下载 最新的 USB库函数,永远用最新的 USB库函数 !
下载 最新的 用户手册 !
下载 最新的 上机实践指导书 !


https://v.stcai.com/sv/e49742d-1978afcb431/e49742d-1978afcb431.mp4
上面是 小李 演示:STC8H8K64U, printf_usb("Hello World !\r\n")及usb不停电下载@AiCube之图形化程序自动生成

https://v.stcai.com/sv/61d1aa5-1978c2a6adb/61d1aa5-1978c2a6adb.mp4
上面是 小赵 演示:STC8H8K64U, printf_usb("Hello World !\r\n")及usb不停电下载@AiCube之图形化程序自动生成

芯Skye 发表于 2025-11-14 16:30:25

请您后续打卡在同一帖子下,不同楼层回复,谢谢配合

我电容发光了 发表于 2025-11-14 16:36:45

芯Skye 发表于 2025-11-14 16:30
请您后续打卡在同一帖子下,不同楼层回复,谢谢配合

好的谢谢

我电容发光了 发表于 2025-11-14 16:45:12

<p>GPIO 的理解</p>
<hr />
<p>一、学习目标</p>
<ol>
<li>了解 C51 中 GPIO 的工作模式。</li>
<li>熟悉芯片手册中 IO 口相关章节的阅读方法。</li>
<li>掌握如何把手册里的寄存器/模式说明,转换成真正可用的代码。</li>
</ol>
<hr />
<p>二、学习内容</p>
<p>(一)电灯案例代码解析</p>
<pre><code class="language-c">#include &quot;STC8H.H&quot;

int main() {
    P5M0 = 0x00;
    P5M1 = 0x00;

    P53 = 1;
    while(1) {}
}
</code></pre>
<p>逐行说明:</p>
<ul>
<li><code>#include &quot;STC8H.H&quot;</code><br />
引入 STC8H 系列专用的头文件,里面定义了各个特殊功能寄存器、位定义等。</li>
<li><code>P5M0 = 0x00;</code></li>
<li><code>P5M1 = 0x00;</code><br />
配置 P5 端口所有引脚为某种工作模式(这里是“准双向口”模式,后面会详细解释)。</li>
<li><code>P53 = 1;</code><br />
设置 P5 端口的 3 号引脚(P5.3)为高电平,用来点亮 LED 或控制外接电路。</li>
</ul>
<hr />
<p>(二)头文件 STC8H.H</p>
<ul>
<li>针对 STC8H 系列的寄存器、位地址、特殊功能都在 <code>STC8H.H</code> 中定义。</li>
<li>如果 Keil 没有配置 STC8 环境,工程里是无法直接 <code>#include &quot;STC8H.H&quot;</code> 的。</li>
<li>一般情况下,此文件在 Keil 安装目录下:<br />
<code>C51\INC\STC</code><br />
这个目录下还有其它 STC 系列的头文件,如果你使用其它 STC 芯片,就需要 <code>include</code> 对应型号的头文件。</li>
</ul>
<hr />
<p>(三)引脚工作模式</p>
<ol>
<li>从手册中获取的信息</li>
</ol>
<p>通过 STC-ISP 下载 STC8H 用户手册,打开 IO 口章节,可以看到:</p>
<ul>
<li>一个端口对应 8 个引脚(Pn.0 ~ Pn.7)。</li>
<li>每个端口都有自己的配置寄存器。</li>
<li>不同系列芯片,端口数量不完全相同。</li>
<li>每个引脚可以配置为 4 种不同工作模式。</li>
</ul>
<ol start="2">
<li>四种工作模式(用 PnM1 / PnM0 组合控制)</li>
</ol>
<p>每个 IO 引脚有两个配置位:<code>PnM1</code> 和 <code>PnM0</code>。两位不同组合,对应不同工作模式:</p>
<ul>
<li><strong>准双向口</strong>(PnM1 = 0,PnM0 = 0)
<ul>
<li>内部有弱上拉电阻;</li>
<li>既能输入也能输出;</li>
<li>灌电流可达约 20 mA,上拉电流约 270~150 uA。</li>
<li>适合做普通 IO,简单点灯、按键等。</li>
</ul>
</li>
<li><strong>推挽输出</strong>(PnM1 = 0,PnM0 = 1)
<ul>
<li>内部为强上拉+强下拉结构;</li>
<li>可输出较大电流(约 20 mA),但外部一定要有合适的限流电阻;</li>
<li>适合需要较强驱动能力的场景(如直接驱动 LED、小继电器驱动前级等)。</li>
</ul>
</li>
<li><strong>高阻输入</strong>(PnM1 = 1,PnM0 = 0)
<ul>
<li>引脚呈高阻状态;</li>
<li>电流基本不流入、不流出,只用于检测外部电平;</li>
<li>适合作为纯输入端口使用。</li>
</ul>
</li>
<li><strong>开漏输出</strong>(PnM1 = 1,PnM0 = 1)
<ul>
<li>内部上拉电阻断开,仅提供“拉低”能力;</li>
<li>要正确读外部状态或输出高电平,通常需要<strong>外接上拉电阻</strong>;</li>
<li>适合多机总线(如 I²C)、线与逻辑等。</li>
</ul>
</li>
</ul>
<ol start="3">
<li>P5.3 不同模式的配置示例</li>
</ol>
<pre><code class="language-c">P5M1 &amp;= ~0x08;P5M0 &amp;= ~0x08; // 准双向口
P5M1 &amp;= ~0x08;P5M0 |=0x08; // 推挽输出
P5M1 |=0x08;P5M0 &amp;= ~0x08; // 高阻输入
P5M1 |=0x08;P5M0 |=0x08; // 开漏输出
</code></pre>
<p>说明:</p>
<ul>
<li><code>P5</code> 表示端口 5。</li>
<li><code>0x08</code> 对应二进制 <code>0000 1000</code>,表示第 3 位,即 P5.3。</li>
<li><code>&amp;= ~0x08</code>:把这一位清零。</li>
<li><code>|=0x08</code>:把这一位置 1。</li>
</ul>
<ol start="4">
<li>引脚编号与掩码值对应关系</li>
</ol>
<ul>
<li>0 号引脚 → <code>0x01</code></li>
<li>1 号引脚 → <code>0x02</code></li>
<li>2 号引脚 → <code>0x04</code></li>
<li>3 号引脚 → <code>0x08</code></li>
<li>4 号引脚 → <code>0x10</code></li>
<li>5 号引脚 → <code>0x20</code></li>
<li>6 号引脚 → <code>0x40</code></li>
<li>7 号引脚 → <code>0x80</code></li>
</ul>
<p>只要记住这一列映射,就可以按同样方式配置任意端口、任意引脚的工作模式。</p>
<hr />
<p>(四)软延时(软件延时)</p>
<ol>
<li>概念</li>
</ol>
<p>软延时:通过执行一段“什么实事都不干、只消耗时间”的代码来实现延时。<br />
常用于:</p>
<ul>
<li>简单点灯/闪烁;</li>
<li>上电初始化等待;</li>
<li>按键消抖等。</li>
</ul>
<ol start="2">
<li>借助 STC-ISP 工具生成延时代码</li>
</ol>
<p>在 STC-ISP 中,可以根据:</p>
<ul>
<li>系统频率(主频);</li>
<li>期望的延时时长;</li>
<li>芯片型号(指令集);</li>
</ul>
<p>自动生成相应的 C 语言延时函数。</p>
<p>要点:</p>
<ul>
<li><strong>系统频率</strong>:必须和你烧录软件里的设置一致,否则延时时间会不准确。</li>
<li><strong>指令集/芯片型号</strong>:要选择包含你实际使用的芯片系列,这里用 STC-Y6(兼容 STC8H)。</li>
<li><strong>延时时长</strong>:根据实际需要选择,比如 1 秒(1000 ms)。</li>
</ul>
<ol start="3">
<li>让 LED 每隔 1 秒闪烁一次的完整示例</li>
</ol>
<pre><code class="language-c">#include &quot;STC8H.H&quot;

void Delay1000ms()      //@11.0592MHz
{
    unsigned char i, j, k;

    i = 57;
    j = 27;
    k = 112;
    do
    {
      do
      {
            while (--k);
      } while (--j);
    } while (--i);
}

int main() {
    P5M1 &amp;= ~0x08;P5M0 &amp;= ~0x08; // 准双向口
    //P5M1 &amp;= ~0x08;P5M0 |=0x08; // 推挽输出
    //P5M1 |=0x08;P5M0 &amp;= ~0x08; // 高阻输入
    //P5M1 |=0x08;P5M0 |=0x08; // 开漏输出

    while (1) {
      P53 = 1;      // 开灯
      Delay1000ms();// 延时 1 秒
      P53 = 0;      // 关灯
      Delay1000ms();// 再延时 1 秒
    }
}
</code></pre>
<p>你可以把被注释掉的几行模式配置依次打开测试,观察在不同模式下点灯有无差别、是否稳定,顺便加深对各种模式的理解。</p>

我电容发光了 发表于 2025-11-14 16:52:15

<p>库函数</p>
<hr />
<p>一、学习目标</p>
<ol>
<li>理解为什么需要学习库函数。</li>
<li>掌握基于库函数进行开发的大致流程。</li>
<li>会用常用库函数做 IO 操作(例如点灯、延时)。</li>
</ol>
<hr />
<p>二、学习内容</p>
<p>(一)开发过程回顾:以“点灯”为例</p>
<p>典型步骤:</p>
<ol>
<li>查看原理图,找到控制 LED 的引脚。</li>
<li>查芯片手册:
<ul>
<li>配置该引脚工作模式;</li>
<li>控制该引脚输出高/低电平。</li>
</ul>
</li>
</ol>
<p>其中:</p>
<ul>
<li>第 1 步属于 <strong>硬件/开发板设计</strong> 范畴;</li>
<li>第 2 步是 <strong>软件代码编写</strong> 范畴。</li>
</ul>
<p>本质上,我们现在的做法是:</p>
<ul>
<li><strong>面向芯片手册开发</strong>(也叫 <strong>面向寄存器开发</strong>)。</li>
<li>换芯片就得换:
<ul>
<li><code>#include &lt;STC8H.H&gt;</code> 这一类头文件;</li>
<li>查新的数据手册,使用新寄存器名、位定义、配置方式。</li>
</ul>
</li>
</ul>
<p>如果连 <code>STC8H.H</code> 这种厂商提供的头文件都没有:</p>
<ul>
<li>你就得完全根据数据手册,自己用 <code>sfr</code> / <code>sbit</code> 把寄存器地址、位挨个定义出来,然后再写代码。</li>
</ul>
<p>下面是一个 <strong>不依赖头文件、完全用寄存器定义</strong> 的点灯示例:</p>
<pre><code class="language-c">sfr   P5M1   = 0xC9;
sfr   P5M0   = 0xCA;
sfr   P5   = 0xC8;
sbit    P53    = P5^3;

void Delay1000ms()      //@11.0592MHz
{
    unsigned char i, j, k;

    i = 57;
    j = 27;
    k = 112;
    do
    {
      do
      {
            while (--k);
      } while (--j);
    } while (--i);
}

int main() {
    //P5M1 &amp;= ~0x08;P5M0 &amp;= ~0x08; // 准双向口
    P5M1 &amp;= ~0x08;P5M0 |=0x08; // 推挽输出
    //P5M1 |=0x08;P5M0 &amp;= ~0x08; // 高阻输入
    //P5M1 |=0x08;P5M0 |=0x08; // 开漏输出

    while (1) {
      P53 = 1;   // 开灯
      Delay1000ms();
      P53 = 0;   // 关灯
      Delay1000ms();
    }
}
</code></pre>
<p>说明:</p>
<ul>
<li><code>sfr</code> 关键字:定义一个 <strong>特殊功能寄存器</strong>(Special Function Register),如端口寄存器、定时器寄存器等。</li>
<li><code>sbit</code> 关键字:定义寄存器中的某一位,方便按位读写。</li>
</ul>
<p>通过 <code>sfr</code> / <code>sbit</code>,我们可以:</p>
<ul>
<li>直接访问和控制单片机内部的各种功能模块;</li>
<li>但完全依赖手册、地址、位定义,<strong>工作量大且易出错</strong>。</li>
</ul>
<hr />
<p>(二)使用库函数点灯</p>
<ol>
<li>导入库函数文件</li>
</ol>
<p>从 STC 官方库函数压缩包中,拷贝以下文件到你的工程目录(或某个公共目录):</p>
<ul>
<li><code>Config.h</code></li>
<li><code>Type_def.h</code></li>
<li><code>GPIO.h</code></li>
<li><code>GPIO.c</code></li>
</ul>
<ol start="2">
<li>新建工程 &amp; 添加文件</li>
</ol>
<ul>
<li>新建 <code>main.c</code>;</li>
<li>在工程中把 <code>Config.h / Type_def.h / GPIO.h / GPIO.c</code> 全部加进来;</li>
<li>编译后能看到 <code>GPIO.c</code> 已经在工程文件树中。</li>
</ul>
<ol start="3">
<li>在 <code>main.c</code> 中用库函数控制 LED</li>
</ol>
<pre><code class="language-c">#include &quot;Config.h&quot;
#include &quot;GPIO.h&quot;

void Delay500ms()       //@11.0592MHz
{
    unsigned char data i, j, k;

    i = 29;
    j = 14;
    k = 54;
    do
    {
      do
      {
            while (--k);
      } while (--j);
    } while (--i);
}

void GPIO_config() {
    GPIO_InitTypeDef gpioInit;

    gpioInit.Mode = GPIO_OUT_PP;   // 推挽输出
    gpioInit.Pin= GPIO_Pin_3;    // P5.3
    GPIO_Inilize(GPIO_P5, &amp;gpioInit);
}

void main() {
    //方式一:宏配置准双向
    //P5_MODE_IO_PU(GPIO_Pin_3);

    //方式二:用初始化函数配置推挽输出
    GPIO_config();

    while (1) {
      P53 = 1;
      Delay500ms();
      
      P53 = 0;
      Delay500ms();
    }
}
</code></pre>
<p>对比“纯寄存器写法”:</p>
<ul>
<li>现在只需要关心:
<ul>
<li>我想把哪个 IO 设成什么模式?</li>
<li>调哪个初始化函数?</li>
</ul>
</li>
<li>底层 P5M0 / P5M1 这些寄存器由库函数来配置。</li>
</ul>
<hr />
<p>(三)什么是“库函数”?</p>
<p>简单讲:</p>
<blockquote>
<p>库函数就是别人已经写好的一组通用功能代码,你只需要 <strong>调用</strong>,不用自己从零写。</p>
</blockquote>
<p>特点:</p>
<ol>
<li><strong>简化编程难度</strong>
<ul>
<li>不必记一堆寄存器名、位含义;</li>
<li>避免很多手误、低级错误。</li>
</ul>
</li>
<li><strong>提高可读性</strong>
<ul>
<li><code>GPIO_Inilize(GPIO_P5, &amp;gpioInit);</code> 比 <code>P5M1 / P5M0</code> 直观多了。</li>
</ul>
</li>
<li><strong>节省时间</strong>
<ul>
<li>常用功能已经调试好,可直接拿来用。</li>
</ul>
</li>
<li><strong>更易移植</strong>
<ul>
<li>换同系列/同厂芯片时,库函数接口可以基本不变;</li>
<li>只需替换底层库实现或配置。</li>
</ul>
</li>
<li><strong>更安全</strong>
<ul>
<li>避免自己写寄存器操作时产生的溢出、死循环等隐蔽 bug。</li>
</ul>
</li>
</ol>
<p>但也要注意:</p>
<ul>
<li>对时间极端敏感的场景(比如极限优化中断响应),<strong>直接寄存器操作有时更高效</strong>;</li>
<li>实战里经常是:
<ul>
<li>大部分用库函数保证效率和可靠性;</li>
<li>少量关键路径用寄存器精调。</li>
</ul>
</li>
</ul>
<hr />
<p>(四)使用 Delay 模块做延时</p>
<p>在库函数里,延时也通常封装成一个独立模块,方便复用。</p>
<ol>
<li>拷贝延时模块文件</li>
</ol>
<p>把库里这两个文件拷贝到工程:</p>
<ul>
<li><code>Delay.c</code></li>
<li><code>Delay.h</code></li>
</ul>
<ol start="2">
<li>在需要延时的 C 文件中包含头文件</li>
</ol>
<pre><code class="language-c">#include &quot;Delay.h&quot;
</code></pre>
<ol start="3">
<li>使用库函数进行延时</li>
</ol>
<pre><code class="language-c">delay_ms(250);   // 延时 250 ms(这里支持 1~255 ms)
</code></pre>
<p>注意:</p>
<ul>
<li>延时函数内部是按你工程里配置的系统时钟来算时间的;</li>
<li><strong>主频配置要和下载器/ISP 中的主频设置一致</strong>,否则延时会不准。</li>
</ul>
<hr />
<p>三、面向库函数 vs 面向寄存器</p>
<p>可以简单理解:</p>
<ul>
<li><strong>面向寄存器开发</strong>:
<ul>
<li>查手册 → 记寄存器名和位 → 自己写 <code>sfr</code> / <code>sbit</code> / 位操作。</li>
<li>优点:灵活、精细、极限优化时更适合。</li>
<li>缺点:难度大、易出错、可读性差、移植麻烦。</li>
</ul>
</li>
<li><strong>面向库函数开发</strong>:
<ul>
<li>调库里的初始化函数、设置结构体参数;</li>
<li>大部分时候不直接碰寄存器。</li>
<li>优点:简单、易读、可移植性好、开发快。</li>
<li>缺点:极端场景下可能不如手写寄存器“极限压榨”性能。</li>
</ul>
</li>
</ul>
<p>实际项目里通常是 <strong>两者结合</strong>:</p>
<ul>
<li>先用库函数快速把功能跑起来;</li>
<li>确认瓶颈后,再在个别关键模块用寄存器级优化。</li>
</ul>

我电容发光了 发表于 2025-11-14 16:54:31

<p>中断系统 INT</p>
<p>一、学习目标</p>
<ol>
<li>理解中断的概念,掌握中断的分类和优先级。</li>
<li>理解中断的响应机制和处理方法。</li>
</ol>
<hr />
<p>二、学习内容</p>
<p>(一)中断的概念</p>
<p>中断系统是为使 CPU 具有对外界紧急事件的实时处理能力而设置的。</p>
<p>当中央处理机 CPU 正在处理某件事情的时候,外界发生了紧急事件请求,要求 CPU 暂停当前的工作,转而去处理这个紧急事件。处理完以后,再回到原来被中断的地方,继续原来的工作,这样的过程称为“中断”。</p>
<p>实现这种功能的部件称为“中断系统”,向 CPU 发出中断请求的请求源称为“中断源”。</p>
<p>微型机的中断系统一般允许多个中断源。当几个中断源同时向 CPU 请求中断、要求为它服务时,就存在一个问题:CPU 优先响应哪一个中断源?</p>
<p>通常根据中断源的重要程度和紧急程度排队,优先处理最紧急事件的中断请求源,即规定每一个中断源有一个优先级别。CPU 总是先响应优先级别最高的中断请求。</p>
<p>当 CPU 正在处理一个中断源请求(执行相应的中断服务程序)的时候,如果又发生了另外一个优先级比它还高的中断源请求,且 CPU 能够:</p>
<ol>
<li>暂停当前正在执行的中断服务程序,</li>
<li>转去处理优先级更高的中断请求,</li>
<li>处理完以后再回到原来较低优先级的中断服务程序继续执行,</li>
</ol>
<p>这样的过程称为“中断嵌套”。具有中断嵌套功能的中断系统称为多级中断系统,没有中断嵌套功能的称为单级中断系统。</p>
<p>用户可以通过总中断允许位 EA(IE.7)或相应中断的允许位来屏蔽相应的中断请求,也可以通过打开相应的中断允许位,使 CPU 响应对应的中断请求。</p>
<p>每一个中断源都可以用软件独立控制为“开中断”或“关中断”状态。部分中断的优先级别也可以用软件设置。</p>
<p>规则总结:</p>
<ol>
<li>高优先级的中断请求可以打断低优先级的中断。</li>
<li>低优先级的中断请求不能打断高优先级的中断。</li>
<li>当两个相同优先级的中断同时产生时,由查询次序决定系统先响应哪个中断。</li>
</ol>
<p>(此处原文包含中断系统结构相关示意图,可在富文本中插入示意图。)</p>
<hr />
<p>(二)中断源</p>
<p>能向 CPU 提出中断请求的信号源称为“中断源”。</p>
<p>STC8H 中的中断源种类较多(如外部中断、定时器中断、串口中断等),具体可以参考芯片手册中的中断源表格和功能说明,在富文本中可插入该表格或图片作为辅助理解。</p>
<hr />
<p>(三)中断寄存器</p>
<p>通过 STC8H 的用户手册可以查询所有与中断相关的寄存器,以及对应的中断请求位信息。</p>
<p>例如:中断允许寄存器 IE、中断优先级寄存器 IP 以及扩展中断控制寄存器等。</p>
<p>相关资料可参考:<br />
STC8H 数据手册(PDF):<br />
http://www.stcmcudata.com/STC8F-DATASHEET/STC8H.pdf</p>
<hr />
<p>(四)中断函数</p>
<p>在 C 语言中,可以通过关键字 interrupt 定义中断函数。</p>
<p>示例:</p>
<pre><code class="language-c">void UART1_int (void) interrupt 0
{
}
</code></pre>
<p>说明:</p>
<ol>
<li><code>UART1_int</code> 是中断函数的名称,可以根据需要自行命名。</li>
<li><code>interrupt</code> 关键字用于声明当前函数为中断函数。</li>
<li><code>0</code> 是中断号(中断入口向量编号),需要根据实际业务、查阅用户手册后确定。</li>
</ol>
<p>中断函数可以理解为“回调函数”:</p>
<ul>
<li>函数定义好之后,何时被调用不是由我们在主程序中直接调用决定的,而是由硬件事件触发、由系统自动调用。</li>
<li>我们只需要关心“什么事件会触发这个中断函数”,并在中断函数中编写需要执行的业务逻辑。</li>
</ul>
<hr />
<p>(五)验证 UART 串口的中断函数</p>
<p>示例需求:<br />
接收数据时点亮 LED,发送数据时熄灭 LED。</p>
<p>相关寄存器与位定义:</p>
<pre><code class="language-c">sfr   P5M1   = 0xC9;
sfr   P5M0   = 0xCA;
sfr   P5   = 0xC8;
sbit    P53    = P5^3;

sfr   T2L    = 0xD7;
sfr   T2H    = 0xD6;
sfr   AUXR   = 0x8E;

sfr   IE   = 0xA8;
sbit    EA   = IE^7;
sbit    ES   = IE^4;

sfr   SCON   = 0x98;
sfr   SBUF   = 0x99;
sbit    RI   = SCON^0;
sbit    TI   = SCON^1;
</code></pre>
<p>UART 中断服务函数:</p>
<pre><code class="language-c">void uart_hello(void) interrupt 4 {
    if (RI) {
      // 如果接收标志位 RI 触发了中断,打开灯
      RI = 0;
      P53 = 1;   // 开
    }

    if (TI) {
      // 如果发送标志位 TI 触发了中断,关掉灯
      TI = 0;
      P53 = 0;   // 关
    }
}
</code></pre>
<p>简单延时函数(约 1000 ms,@11.0592MHz):</p>
<pre><code class="language-c">void Delay1000ms()      //@11.0592MHz
{
    unsigned char i, j, k;

    i = 57;
    j = 27;
    k = 112;
    do
    {
      do
      {
            while (--k);
      } while (--j);
    } while (--i);
}
</code></pre>
<p>主函数示例:</p>
<pre><code class="language-c">int main() {
    // P5.3 推挽输出,用作 LED 指示灯
    P5M1 &amp;= ~0x08;
    P5M0 |=0x08;

    // 串口 1 配置
    SCON = 0x50;

    // 波特率发生器配置:使用定时器 2
    // 65536 - 11059200 / 115200 / 4 = 0xFFE8
    T2L= 0xE8;
    T2H= 0xFF;
    AUXR = 0x15;   // 启动定时器 2,选择波特率时钟等

    // 开总中断与串口中断
    EA = 1;
    ES = 1;

    P53 = 0;       // 初始灯灭

    while (1) {
      // 休眠 1000 ms
      Delay1000ms();

      // 发送一个数据 0x11
      SBUF = 0x11;

      // 将 TI 位置 1(实际上只要给 SBUF 赋值,硬件会自动置位 TI)
      TI = 1;
    }
}
</code></pre>
<p>本例中完成的内容包括:</p>
<ol>
<li>配置 UART 初始化(包括波特率定时发生器)。</li>
<li>查询并使用几个关键寄存器地址:如 SBUF、IE 等。</li>
<li>在中断服务函数中根据接收/发送标志位控制 LED 的亮灭,用于直观验证中断触发情况。</li>
</ol>

我电容发光了 发表于 2025-11-15 16:25:27

串口通信 UART
一、学习目标

[*]了解串口通信的基本概念。
[*]掌握 STC8H 的串口通信原理。
[*]掌握 STC8H 的串口通信编程套路。
[*]学会使用逻辑分析仪调试串口。
二、学习内容
(一)什么是串口(UART)串口是一种非常常见的通信接口,全名 UART:Universal Asynchronous Receiver/Transmitter(通用异步收发传输器)。
特点:

[*]异步:没有单独的时钟线,用约定好的波特率“约定俗成”。
[*]串行:一位一位按顺序发送。
[*]简单可靠:硬件资源少、调试方便,嵌入式里到处都是 UART。
常见用途:

[*]与 传感器、LCD 模块、WiFi 模块、蓝牙模块、GPS 模块 等通信;
[*]与 PC 进行调试(日志输出、命令交互)。
关键引脚:

[*]TXD(Transmit Data):发送数据的引脚;
[*]RXD(Receive Data):接收数据的引脚。
工作过程(概念版):

[*]发送端:把要发的数据写入串口发送缓冲区 → 串口硬件按位通过 TXD 发出去;
[*]接收端:通过 RXD 接收到的比特流 → 串口硬件组装成字节 → 放到接收缓冲区 → 软件再去读取。
需要注意:

[*]电平标准要统一:

[*]常见有 TTL 电平(0~3.3V / 5V) 和 RS232 电平(±12V 左右);
[*]TTL 对接 RS232 必须加电平转换芯片(如 MAX232)。
[*]通信参数必须一致:

[*]波特率(Baud Rate)
[*]数据位(一般 8 位)
[*]校验位(无 / 奇 / 偶)
[*]停止位(一般 1 位)
STC8H 串口引脚分布STC8H 提供 4 组 UART,每组有多组可复用引脚:

串口RXD 备选引脚TXD 备选引脚
UART1P3.0 / P3.6 / P1.6 / P4.3P3.1 / P3.7 / P1.7 / P4.4
UART2P1.0 / P4.6P1.1 / P4.7
UART3P0.0 / P5.0P0.1 / P5.1
UART4P0.2 / P5.2P0.3 / P5.3
在富文本里可以插入芯片引脚图,用高亮标出各 UART 的 TXD/RXD 引脚位置。
(二)串口 TTL 通讯协议TTL 串口:指使用 TTL 电平的 UART 通信,电压范围一般为 0~5V 或 0~3.3V。
信号线:

[*]TX(输出):本设备发出去的数据;
[*]RX(输入):本设备接收进来的数据。
数据传输格式:

[*]采用 异步串行:数据被分成一个个“帧”:
一个典型的数据帧结构:
空闲线(高电平) → 起始位(1 位低电平) → 数据位(5~8 位,一般 8 位) → 校验位(可选) → 停止位(1 或 2 位高电平)
波特率(Baud Rate):

[*]表示每秒传输的比特数,单位 bps;
[*]常见:9600、19200、57600、115200 等;
[*]发送端和接收端必须 波特率一致,否则会出现乱码。
注意事项:

[*]不同设备的 TTL 电平可能是 3.3V 或 5V,混接时要看是否兼容;
[*]波特率越高,传输越快,但线太长 / 干扰太多 / 配置不合理时更容易丢帧。
在富文本中可以插入一张“UART 波形图”,标明起始位、数据位、停止位。
(三)串口转 USB串口转 USB 模块:把 UART(TXD/RXD)转换为 USB,让没有 USB 功能的单片机也能方便连到 PC。
内部结构:

[*]常见芯片:CH340、FT232/FTDI 等;
[*]一侧接 MCU 的 TX/RX,引脚;
[*]另一侧是 USB 插头接 PC;
[*]PC 侧通过驱动程序,把它识别成一个“虚拟串口(COMx)”。
使用步骤:

[*]把 USB 头插到 PC;
[*]安装对应驱动(尤其是 CH340);
[*]串口工具中选择对应 COM 口;
[*]设置波特率等参数,与 MCU 侧一致;
[*]就可以像传统串口一样收发数据。
在富文本中可插入一张“USB 转串口模块照片” + 原理框图,更直观。
(四) STC8H 核心板上的串口调试
[*]原理图要点:

[*]USB 口(D+ / D-)→ CH340 → MCU 的 P3.0(RXD)/ P3.1(TXD);
[*]PC 通过 USB → CH340 → UART1(P3.0/P3.1);
[*]所以 PC 串口工具看到的就是 MCU 的 UART1。
[*]为什么要在 PC 上安装串口驱动?

[*]驱动是给 CH340/FTDI 这类芯片用的;
[*]驱动把它映射成一个标准串口(COMx),串口工具才能识别。
(五)使用库函数写 UART 通信(回显程序)需求:通过 PC 串口助手发送数据到开发板,开发板收到后 原样返回。
1. 工程准备
[*]新建工程,创建 main.c。
[*]拷贝并添加下面这些库函数文件到工程:

[*]Config.h、Type_def.h
[*]GPIO.h、GPIO.c
[*]Delay.h、Delay.c
[*]UART.h、UART.c、UART_Isr.c
[*]NVIC.h、NVIC.c
[*]Switch.h
2. 串口发送示例(只发不收)#include "Config.h"
#include "GPIO.h"
#include "UART.h"
#include "Delay.h"
#include "NVIC.h"
#include "Switch.h"

/************* 功能说明 **************
双串口全双工中断方式收发通讯程序。

通过 PC 向 MCU 发送数据, MCU 收到后通过串口把数据原样返回。
默认参数:115200, N, 8, 1。

通过在 UART.h 中开启 UART1~UART4 的宏定义,
可以启用不同通道的串口通信。
******************************************/

/************* IO 配置 *****************/
void GPIO_config(void) {
    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.Pin= GPIO_Pin_0 | GPIO_Pin_1; // P3.0, P3.1
    GPIO_InitStructure.Mode = GPIO_PullUp;
    GPIO_Inilize(GPIO_P3, &GPIO_InitStructure);
}

/************* 串口初始化 *************/
void UART_config(void) {
    COMx_InitDefine COMx_InitStructure;

    COMx_InitStructure.UART_Mode      = UART_8bit_BRTx; // 8位,可变波特率
    COMx_InitStructure.UART_BRT_Use   = BRT_Timer1;   // 波特率发生器
    COMx_InitStructure.UART_BaudRate= 115200ul;       // 波特率
    COMx_InitStructure.UART_RxEnable= ENABLE;         // 允许接收
    COMx_InitStructure.BaudRateDouble = DISABLE;      // 不加倍

    UART_Configuration(UART1, &COMx_InitStructure);   // 配置 UART1
    NVIC_UART1_Init(ENABLE, Priority_1);                // 开 UART1 中断,优先级 1

    UART1_SW(UART1_SW_P30_P31); // UART1 使用 P3.0 / P3.1
}

void main(void) {
    GPIO_config();
    UART_config();
    EA = 1; // 打开全局中断

    TX1_write2buff(0x23); // 发送一个字符 '#'
    printf("STC8H8K64U UART1 Test Programme!\r\n");
    PrintString1("STC8H8K64U UART1 Test Programme!\r\n");

    while (1) {
      TX1_write2buff(0x2F); // 发送 '/'
      delay_ms(250);
      delay_ms(250);
      delay_ms(250);
      delay_ms(250);
    }
}3. 串口接收 + 原样回写(回显)#include "Config.h"
#include "GPIO.h"
#include "UART.h"
#include "Delay.h"
#include "NVIC.h"
#include "Switch.h"

void GPIO_config(void) {
    GPIO_InitTypeDef GPIO_InitStructure;

    GPIO_InitStructure.Pin= GPIO_Pin_0 | GPIO_Pin_1; // P3.0, P3.1
    GPIO_InitStructure.Mode = GPIO_PullUp;
    GPIO_Inilize(GPIO_P3, &GPIO_InitStructure);
}

void UART_config(void) {
    COMx_InitDefine COMx_InitStructure;

    COMx_InitStructure.UART_Mode      = UART_8bit_BRTx;
    COMx_InitStructure.UART_BRT_Use   = BRT_Timer1;
    COMx_InitStructure.UART_BaudRate= 115200ul;
    COMx_InitStructure.UART_RxEnable= ENABLE;
    COMx_InitStructure.BaudRateDouble = DISABLE;
    UART_Configuration(UART1, &COMx_InitStructure);

    NVIC_UART1_Init(ENABLE, Priority_1);
    UART1_SW(UART1_SW_P30_P31);
}

// 收到一批数据后的处理函数:原样回传
void on_uart1_recv(void) {
    u8 i;
    for (i = 0; i < COM1.RX_Cnt; i++) {
      u8 dat = RX1_Buffer;
      TX1_write2buff(dat); // 原样发回
    }
}

/**
* 功能:开启串口调试,接收数据并原样返回
*/
void main(void) {
    GPIO_config();
    UART_config();
    EA = 1; // 全局中断开关,必须要写!!!

    TX1_write2buff(0x23);
    PrintString1("STC8H8K64U UART1 Test Programme!\r\n");
    printf("STC8H8K64U UART1 Test Programme!\r\n");

    while (1) {
      // 超时计数机制:
      // 一旦收到了一个字节,COM1.RX_TimeOut 会被初始化为一个较小的值(如 5)
      if ((COM1.RX_TimeOut > 0) && (--COM1.RX_TimeOut == 0)) {
            if (COM1.RX_Cnt > 0) {
                // 一帧数据接收完成,处理之
                on_uart1_recv();
            }
            // 处理完清零计数
            COM1.RX_Cnt = 0;
      }

      delay_ms(10);
    }
}(六)串口调试的重难点汇总
[*]IO 模式配置

[*]对应的 TXD/RXD 引脚必须配置为正确的 GPIO 模式(通常为上拉输入/准双向等);
[*]UART1 默认引脚一般是准双向口,可能“勉强能用”,但其他 UART 通道一定要记得配置,不要养成侥幸习惯。
[*]UART Mode(UART_Mode)

[*]UART_ShiftRight:同步移位模式,按位移,极少用。
[*]UART_8bit_BRTx:最常用,8 位数据,可变波特率。
[*]UART_9bit:9 位数据,固定波特率。
[*]UART_9bit_BRTx:9 位数据 + 可变波特率,常用于带奇偶校验的通信。
[*]波特率(BaudRate)

[*]根据需求选,如 115200 基本够用;
[*]过高会增加丢帧风险,尤其是线长/干扰较大时。
[*]波特率发生器(BRT_Use)

[*]可以选择 Timer1 ~ Timer4 做波特率发生器:

[*]BRT_Timer1 / BRT_Timer2 / BRT_Timer3 / BRT_Timer4
[*]注意:UART2 固定使用 Timer2 作为波特率发生器。
[*]接收使能(RxEnable)

[*]必须置为 ENABLE,否则只发不收。
[*]波特率加倍(BaudRateDouble)

[*]DISABLE:正常波特率;
[*]ENABLE:波特率翻倍,容易过高导致丢帧,慎用。
[*]中断配置(Interrupt & Priority)

[*]串口的收发一般伴随 中断:

[*]要开启 UART 对应的中断;
[*]还要配置中断优先级(Priority_0 ~ Priority_3)。
[*]端口切换(UARTx_SW / P_SW)

[*]同一个 UART 可以映射到不同引脚上,必须指定使用哪一组:

[*]如:UART1_SW_P30_P31 / UART1_SW_P36_P37 等。
[*]总中断开关 EA

[*]即使单个 UART 中断打开,如果 EA = 0,所有中断仍然失效。
[*]所以:

[*]EA = 1; 必须写,重要的事情说三遍:

[*]使用 UART 一定要记得打开中断总开关!!!
[*]使用 UART 一定要记得打开中断总开关!!!
[*]使用 UART 一定要记得打开中断总开关!!!
[*]printf 输出串口选择

[*]在 UART.h 中:
#define UART1 1//#define UART2 2//#define UART3 3//#define UART4 4#define PRINTF_SELECTUART1
[*]确保 UART_Isr.c 已经添加进工程,否则 printf 也可能没输出。
(七)逻辑分析仪调试 UART通过逻辑分析仪同时观察 RXD/TXD 波形,可以非常直观地确认:

[*]数据有没有发出去?
[*]数据格式、波特率是否正确?

[*]原理图分析

[*]找到 UART1 的 TXD / RXD:

[*]P3.0:RXD
[*]P3.1:TXD
[*]接线方式

[*]逻辑分析仪 CH1 → 开发板 P3.0(RXD)
[*]逻辑分析仪 CH2 → 开发板 P3.1(TXD)
[*]逻辑分析仪 GND → 开发板 GND
[*]准备工作

[*]烧录好“接收后原样返回”的固件;
[*]打开 STC-ISP 串口调试助手:

[*]选对串口号;
[*]设置正确波特率;
[*]输入要发送的内容(例如 hello),可以设置自动周期性发送。
[*]分析步骤

[*]逻辑分析仪软件中选择协议解析:Async Serial;
[*]设置与代码一致的波特率;
[*]采集后:

[*]CH1 上可看到 PC → MCU 的 RXD 数据帧;
[*]CH2 上可看到 MCU → PC 的 TXD 回发数据帧;
[*]打开解析后的数据视图,可以直接看到 ASCII 内容,比如 hello。
(八)串口通信测试(两块板子互通)测试场景:

[*]PC 通过各自的 UART1 和两个开发板通信;
[*]两个开发板之间通过 UART4 互联;
[*]实现:在 PC 的两个串口工具窗口里对着两块板子聊天。
连接方式示意:

[*]PC 串口工具 A ↔ 开发板 A 的 UART1(P3.0 / P3.1)。
[*]PC 串口工具 B ↔ 开发板 B 的 UART1。
[*]两块板子之间用 UART4 相连:

[*]开发板 A 的 UART4 TXD → 开发板 B 的 UART4 RXD;
[*]开发板 A 的 UART4 RXD ← 开发板 B 的 UART4 TXD;
[*]两块板子 GND 必须相连(共地)。
引脚选择(示例):

[*]UART1:RXD = P3.0,TXD = P3.1
[*]UART4:RXD = P5.2,TXD = P5.3
代码思路:

[*]板子 A:

[*]从 UART1(PC A)接收 → 转发到 UART4(发给板子 B);
[*]从 UART4 接收 → 转发到 UART1(返回给 PC A)。
[*]板子 B:

[*]逻辑同理,只是对接的是 PC B。
这样就可以在 PC 的两个串口工具窗口中互发消息,验证两块板子的串口链路是否正常。
(九)常见问题与排查
[*]串口窗口出现乱码 可能原因:

[*]烧录时选择的 IRC 频率不是 24.000 MHz,而代码里按照 24MHz 来算波特率;
[*]串口工具中选择的波特率和代码中的不一致(例如代码 115200,工具选了 9600)。
[*]完全没数据 / 发送失败:

[*]没有开启对应 UART 中断;
[*]忘了 EA = 1;;
[*]IO 没配置成正确模式;
[*]端口复用(UARTx_SW)配置错了引脚;
[*]GND 没共地(两个板子或者板子与逻辑分析仪之间)。
[*]两个板子之间通信异常、死循环收发:

[*]TXD/RXD 接反或接错 UART 通道;
[*]没共地;
[*]两边都在“无脑回显”,容易形成“回声放大”,需要设计好协议或判定条件。

我电容发光了 发表于 2025-11-15 16:27:49

AStyle 代码格式化工具
一、AStyle 是什么?
AStyle(Artistic Style) 是一个开源的自动代码格式化工具,支持多种语言:

[*]C / C++ / C# / Objective-C / Objective-C++

[*]Java / Pike / 汇编 等

作用:

[*]自动整理缩进、括号位置、空格风格等

[*]统一整个工程的代码风格

[*]提高代码的可读性和可维护性

AStyle 可以:

[*]直接在 命令行 使用

[*]也可以集成到 IDE / 编辑器(比如 Keil) 里,一键格式化当前文件或整个工程

二、在 Keil 里集成 AStyle
下面是把 AStyle 挂到 Keil 的 Tools 菜单里的典型做法,你可以在富文本中给每一步配上截图说明:
1. 解压 AStyle
[*]把 AStyle 压缩包解压到某个固定目录,例如:


[*]D:\softwares\AStyle\

[*]确认里面能找到可执行文件:


[*]bin\AStyle.exe 或类似路径

建议放在一个不会乱动的目录,后面 Keil 的配置需要填绝对路径。
2. 打开 Keil 的 Tools 自定义菜单在 Keil 中:

[*]菜单栏选择:Tools → Customize Tools Menu...

[*]弹出的窗口里可以为每一行定义一个外部工具命令(名称、路径、参数等)。

你可以添加两条命令:

[*]一条:格式化当前文件

[*]一条:格式化当前目录下所有 .c / .h 文件

3. 添加“格式化当前文件”命令在 Customize Tools Menu 窗口中选择一个空行,填入:

[*]Menu Command(菜单名):


[*]比如:格式化当前文件

[*]Command(程序路径):


[*]D:\softwares\AStyle\bin\AStyle.exe

[*]路径按你自己的解压位置填写

[*]Arguments(参数):
常规写法:
!E推荐写法(避免生成 .orig 备份文件,并使用 4 空格缩进):
!E -n -s4含义:


[*]!E:Keil 的占位符,表示“当前编辑的文件名”;

[*]-n:不生成 .orig 备份文件;

[*]-s4:缩进使用 4 个空格。

设置完成后,点击 OK 保存。
4. 添加“格式化所有 .c / .h 文件”命令同样在 Customize Tools Menu 里再添加一条:

[*]Menu Command:


[*]比如:格式化当前目录所有 C 文件

[*]Command:


[*]指向 AStyle 主程序,例如:
D:\softwares\AStyle\bin\AStyle.exe或解压目录中的 astyle.exe:
xxxx\astyle-3.4-x64\astyle.exe其中 xxxx 是你的安装路径。

[*]Arguments:
"$E*.c" "$E*.h" -n含义说明:


[*]"$E*.c":当前工程目录下所有 .c 文件;

[*]"$E*.h":当前工程目录下所有 .h 文件;

[*]-n:不生成 .orig 备份文件。

5. 关于 .orig 备份文件AStyle 默认会在格式化前,给原文件做一个备份:

[*]源文件:xxx.c

[*]备份文件:xxx.c.orig

如果你不想看到这些 .orig 文件:

[*]在 Arguments 里追加 -n 参数即可关闭备份功能,例如:

!E -n -s4"$E*.c" "$E*.h" -n建议刚开始尝试时 不要加 -n,先观察一下 AStyle 的格式化风格,确认没问题再关闭备份。
三、配置快捷键(Keil 内)
为了用得顺手,可以给“格式化当前文件”设置快捷键,比如 Alt + Shift + F:
操作步骤:

[*]菜单:Edit → Configuration...

[*]切换到 Shortcut Keys 选项卡。

[*]找到:


[*]Tools: 格式化当前文件(刚才自定义的菜单命令)

[*]给它设置一个快捷键,比如:Alt + Shift + F

顺带可以把常用的注释快捷键也配上:

[*]Edit: Advanced: Comment Selection → Ctrl + /

[*]Edit: Advanced: Uncomment Selection → Ctrl + Shift + /

设置完成后,点击 OK。
这样:

[*]在 Keil 中编辑代码时,按 Alt + Shift + F 就能直接调用 AStyle 格式化当前文件;

[*]选中一段代码再用 Ctrl + / / Ctrl + Shift + / 可以快速注释/取消注释。

四、使用小提示

[*]先备份项目


[*]第一次大规模格式化整个工程前,建议先打一个备份或提交一次 Git。

[*]先少量文件试用


[*]可以先只格式化 1~2 个 .c 文件,看看风格是否符合你的习惯。

[*]统一团队格式


[*]如果你以后和别人协作开发,可以把 AStyle 的参数(如缩进、括号风格)统一约定好,大家用同一套命令行参数。


我电容发光了 发表于 2025-11-15 16:49:47

定时器 Timer(库函数版理解)
一、学习目标

[*]理解 定时器的基本概念。

[*]掌握 定时器的常见配置项 和 编程操作套路。

二、学习内容
(一)定时器是什么在单片机里,定时器本质是:计数器 + 时钟源。

[*]时钟源:来自晶振(系统时钟)或外部脉冲。

[*]计数器:按一定频率“数数”,数到一定值就溢出。

[*]溢出时:


[*]可以产生一次 中断;

[*]或者在某个引脚上输出脉冲。

常见用途:

[*]按固定时间间隔跑中断(做“系统心跳”)。

[*]测量输入信号周期 / 占空比。

[*]产生固定频率方波 / PWM。

在 STC8H 中,内置 5 个 16 位定时器:T0、T1、T2、T3、T4。
(二)Timer 实战案例:用定时器控制板载 LED需求:

[*]使用 定时器 0 周期性中断,在中断里翻转 P5.3,实现 LED 闪烁。

1. 完整示例代码#include "Config.h"
#include "Timer.h"
#include "GPIO.h"
#include "NVIC.h"

void GPIO_config(void) {
    GPIO_InitTypeDef GPIO_InitStructure;   // 结构定义
    GPIO_InitStructure.Pin= GPIO_Pin_3;    // 指定要初始化的 IO
    GPIO_InitStructure.Mode = GPIO_PullUp;   // GPIO_PullUp, GPIO_HighZ, GPIO_OUT_OD, GPIO_OUT_PP
    GPIO_Inilize(GPIO_P5, &GPIO_InitStructure); // 初始化
}

//int arr[];
//int counter = 3;

void TIMER_config(void) {
    TIM_InitTypeDef TIM_InitStructure;       // 结构定义

    // 定时器 0:16 位自动重装,产生 1000Hz 中断,每 1ms 进一次中断
    TIM_InitStructure.TIM_Mode      = TIM_16BitAutoReload;   // 工作模式: 16 位自动重装
    TIM_InitStructure.TIM_ClkSource = TIM_CLOCK_1T;          // 时钟源: 1T(跟随主频)
    TIM_InitStructure.TIM_ClkOut    = DISABLE;               // 不从 P3.5 输出高速脉冲
    TIM_InitStructure.TIM_Value   = 65536UL - (MAIN_Fosc / 1000UL);
    // 初值计算:希望中断频率为 1000Hz(每秒 1000 次,每次 1ms)
    // 注意:不要小于约 367Hz(2.7ms 周期),也不要大于 1 000 000Hz(1us 周期)

    TIM_InitStructure.TIM_Run       = ENABLE;                // 初始化后立即启动定时器
    Timer_Inilize(Timer0, &TIM_InitStructure);               // 初始化 Timer0(Timer0~Timer4)

    // 打开 Timer0 中断,优先级 Priority_0(最低)
    NVIC_Timer0_Init(ENABLE, Priority_0);
}

void main() {
    GPIO_config();
    TIMER_config();

    EA = 1;          // 开启全局中断

    P53 = 0;         // 初始灭灯

    while (1);
}定时器 0 中断服务函数:
//========================================================================
// 函数: Timer0_ISR_Handler
// 描述: Timer0 中断函数(进中断时硬件已清除标志位)
//========================================================================
void Timer0_ISR_Handler (void) interrupt TMR0_VECTOR {
    // 在此处添加用户代码
    P53 = ~P53;      // 翻转 P5.3,引脚电平取反 → LED 闪烁
}(三)定时器配置项逐条理解1. 工作模式(TIM_Mode)Timer 内部是 16 位计数器(0~65535),不同模式决定计数、重装载方式:

[*]16 位自动重装载模式(最常用)


[*]溢出后,计数器自动装载预设值继续计数;

[*]每次溢出都能产生中断 / 输出信号;

[*]非常适合周期性定时。

[*]16 位不可重装载模式


[*]计数到设定值后停止;

[*]需要重新初始化才能再跑。

[*]8 位自动重装载模式


[*]只用 8 位计数器,周期更短、分辨率更高。

[*]不可屏蔽中断的 16 位自动重装载模式


[*]本质仍是 16 位自动重装载,只是对应的中断更“强”,不容易被屏蔽。

建议:日常绝大部分场景直接用 TIM_16BitAutoReload。
2. 中断配置定时器不光要“会数数”,还要“会叫你”。

[*]NVIC_Timer0_Init(ENABLE, Priority_0);:打开 Timer0 中断 & 设定优先级。

[*]EA = 1;:打开 总中断开关。

缺一不可:

[*]忘记配 NVIC:定时器内部溢出,但不会进中断函数。

[*]忘记 EA = 1:所有外设中断都被关掉。

3. 时钟源(TIM_ClkSource)常用两种:

[*]TIM_CLOCK_1T:


[*]计数频率 = 主频(如 24MHz)。

[*]TIM_CLOCK_12T:


[*]计数频率 = 主频 / 12;

[*]相当于在硬件上再做一次 12 分频。

也可以选用外部时钟 TIM_CLOCK_Ext,用于“计数外部脉冲”场景。
4. 是否输出高速脉冲(TIM_ClkOut)
[*]DISABLE:只内部计时。

[*]ENABLE:在 P3.5 引脚 输出定时器时钟脉冲。

通常调试高频、测真实频率时会打开 ENABLE,然后用示波器量 P3.5。
5. 时钟周期 / 中断频率设置定时器溢出周期由 计数频率 和 装载值共同决定:
计数步数 = 65536 − TIM_Value
溢出周期 T = 计数步数 / 计数频率
例如代码中:
TIM_InitStructure.TIM_Value = 65536UL - (MAIN_Fosc / 10000UL);
[*]MAIN_Fosc / 10000UL 表示:


[*]希望定时器 每秒中断 10000 次;

[*]于是预装载值 = 65536 −(每次中断要计的步数)。

注意约束:

[*](MAIN_Fosc / Timer频率) 不能大于 65536UL,否则算出的初值为负数(超出 16 位范围)。

[*]理论上可以把 Timer 频率开得很高,但:


[*]24MHz 主频下,1 个时钟周期 ≈ 41.67ns;

[*]一条指令需要多个时钟周期,一段代码需要多条指令;

[*]如果你把 Timer 设成 500000UL(2µs 一次中断) 这种级别,中断函数里几乎什么都干不了。

**经验法则:**中断频率不要开到“极限”,要预留足够执行时间。
6. 启动配置(TIM_Run)TIM_InitStructure.TIM_Run = ENABLE;
[*]ENABLE:初始化完立即启动计数。

[*]DISABLE:先只配置不启动,需要时再手动开。


页: [1] 2
查看完整版本: 学习STC8H8K64U