
简介这份资源是面向嵌入式开发者与单片机初学者的STC8系列实验例程合集覆盖从基本I/O操作到复杂总线通信的完整学习路径。包内共748个文件以C源代码、Keil工程文件、HEX烧录文件及调试辅助文件为主整体体积16.47MB目录按基础、中级、高级与传感器例程分类组织便于按需查阅。基础部分包含LED闪烁、数码管显示与按键输入帮助快速上手编程环境中级例程涉及UART串口、定时器、PWM及AD/DA转换适合驱动电机和模拟信号处理高级部分则深入I2C、SPI总线与中断设计并补充了DHT11、红外遥控、加速度计等主流传感器接入代码配套OV7670摄像头实验可用于图像采集类项目。全部例程基于Keil uVision等常见工具链编写直接打开工程即可编译运行。已有1279人学习下载适合希望系统掌握STC8单片机特性、提升嵌入式实战能力的读者作为参考与练习素材。1. STC8 实验例程拿到手不是看是先能跑起来这套 STC8 系列实验例程价值不在于代码写得多花哨而在于它把数据手册里分散的寄存器配置翻译成了能直接编译、下载、观察现象的工程。我拆过不少这种例程包最常见的情况是新手下了压缩包打开 Keil 发现工程路径全是绝对路径编译一遍十几个报错直接劝退。熟练工反而用得顺手因为外设初始化代码都是验证过的拿过来改参数就能用。这份资源适合正在学 STC8、手头有开发板、打算用它做产品原型的人——它解决的是“寄存器手册看懂了但写不出初始化代码”和“写出了代码但波形不对”这两类问题。2. 先搞懂 STC8 到底是什么选型、编译器和下载链路2.1 选型先看 FLASH 和封装例程对应的是哪颗芯片STC8 是一个系列不是一颗芯片从 STC8A 到 STC8H不同后缀对应不同的外设资源和引脚数。打开例程包第一件事不是看代码而是看工程配置里选的是哪颗芯片。常见的是 STC8H8K64U 这类大容量型号64KB FLASH、8KB SRAM带硬件 USB 和 ADC。如果你的板子是 STC8G 系列2KB SRAM直接跑大容量的例程就会出现数组越界但编译可能不报错运行起来就是各种莫名其妙的现象。例程包里的 User 目录下基本都有config.h或者target.h里面定义了芯片型号和系统时钟。我一般会先搜#include STC8H.h或者类似头文件确认资源包内置的头文件和你手头芯片是否匹配。STC8 系列寄存器地址不统一用错头文件编译不会报错因为宏定义能对上但实际写到的寄存器地址和你想的不是一回事。提示先确认例程工程里的 Device 型号再看代码。型号选错是最隐蔽的错误报错往往出现在你完全想不到的地方。2.2 Keil C51 还是 SDCC例程工程直接打开可能翻车STC8 的例程绝大多数是为 Keil C51 准备的工程文件后缀是 .uvproj 或 .uvprojx。如果你的电脑只装了 Keil MDKARM 版打开这种工程会直接提示不支持。这不是例程的问题是工具链不对。Keil C51 和 Keil MDK 是两个独立的 IDE安装路径可以共存但工程格式不通用。SDCC 的情况更特殊。有部分开源爱好者在 SDCC 环境下移植过 STC8 的例程但大多数官方或第三方例程默认 Keil C51。如果你用 SDCC需要自己处理中断向量、寄存器定义工作量不小。我的建议是复现例程阶段用 Keil C51等你把外设行为摸清了再考虑迁移 SDCC。安装 Keil C51 之后用 Keil 打开例程工程第一次编译通常会有两种情况一是编译器路径不对因为原工程是在别人机器上创建的相对路径失效二是头文件路径缺失。解决方法是在 Options for Target 的 C51 选项卡里把 Include Paths 指向例程包的 Source 目录确保STC8H.h这类头文件能被找到。2.3 下载电路为什么“下载不进去”是最常见的第一个坑STC8 大部分型号支持串口下载通过 UART 的 TXD/RXD 引脚配合冷启动实现。所谓冷启动就是先让单片机断电点下载按钮后再上电芯片上电后先运行 ISP 引导区检测到合法的下载命令就擦除并写入 FLASH。例程包里的下载说明通常写着“用 USB 转 TTL 模块TXD 接 RXD、RXD 接 TXD”这句话很多人交过学费。问题往往出在三点第一地线没共地。USB 转 TTL 模块的地必须和开发板的地连在一起否则电平参考不一致数据怎么发都收不到。第二上电时序不对。STC 的 ISP 下载需要先点下载再给目标板上电。如果你用的是带电源开关的开发板先开电再点下载大概率失败。有些开发板是自动上电的接入 USB 就上电这种板子需要按一下复位键让芯片重新进入 ISP。第三波特率设置过高。例程里默认的下载波特率如果是 115200劣质 USB 转 TTL 线在这种速率下容易被干扰打断。可以尝试降到 9600速度慢但稳定得多。// 例程包中常见的串口下载检查代码用于确认通信是否建立 // 该代码不参与业务逻辑仅用于 ISP 下载模式验证 void SystemInit(void) { // 设置 IRC 频率为 24MHz // STC8 系列上电默认使用内部高精度 IRC不需要外部晶振也能跑 // 这里关闭看门狗防止 ISP 下载过程中被看门狗复位打断 WDT_CONTR 0x00; // WDT_CONTR 的第 7 位为看门狗使能位 // 置 0 表示关闭看门狗下载过程中芯片不会意外复位 // 后续波特率、定时器初始化要等主时钟确定后再配置 }这段代码逻辑上很简单但要注意 WDT_CONTR 这个寄存器在 STC8 的某些型号上地址不同。例程包既然是针对特定型号写的里面的寄存器地址不会错但如果你换了系列就一定要对照数据手册重新确认。看门狗不关下载过程中芯片持续复位ISP 通信根本建立不起来表现出来的现象就是“一直连接不上”。我把这归为 STC8 的第一个玄学坑因为出错信息不会告诉你是因为看门狗只会显示“握手失败”。3. 核心外设例程的正确打开方式GPIO、时钟和串口3.1 GPIO 的四种模式准双向口才是默认别以为是推挽STC8 的 GPIO 和传统 51 不一样每个 IO 口都可以独立配置成四种模式准双向口、推挽输出、高阻输入、开漏输出。例程包的gpio.c里一般有一套配置函数核心是操作PxM0和PxM1两个寄存器每根引脚用两位来控制。// 初始化 P1 口P1.0 推挽输出P1.1 高阻输入 // PxM1 和 PxM0 的组合 // 00 准双向口01 推挽输出10 高阻输入11 开漏输出 void GPIO_Init(void) { P1M0 0x01; // P1.0 置 1表示 P1.0 为推挽模式 P1M1 0x02; // P1.1 置 1表示 P1.1 为高阻输入 P1 0xFF; // 输出数据寄存器先置高避免上电瞬间引脚误动作 }这里有个容易理解错的地方51 单片机默认 IO 是准双向口特点是“读引脚前要先写 1”。但 STC8 的准双向口内部带弱上拉直接读也行只不过驱动能力弱。例程里如果要把引脚当输入用正确做法是配置成高阻输入而不是准双向口。准双向口模式下读到的电平可能被内部上拉影响外接下拉电阻时尤其明显。参数说明PxM0和PxM1的每一位对应一个引脚比如 P1M1 的第 3 位对应 P1.3。配置时不要手写二进制数然后靠注释猜建议列出完整的对应表格确认每一位落在哪个引脚上。我拆例程时经常发现有人把P1M0 0x01理解为“P1.0 是推挽”这没错但把P1M1 0x02也归到 P1.0 就错了——M1 的 0x02 对应的是 P1.1。两个寄存器要合在一起看。3.2 时钟树与波特率先定时钟再算波特率顺序错了全乱STC8 例程里最容易被忽略的是时钟配置。芯片默认用内部高精度 IRC频率出厂设置可能是 24MHz 或者 11.0592MHz不同批次可能不一样。例程里通常会在SystemInit()里显式设置 IRC 频率而不是依赖默认值。// 将内部 IRC 频率设置为 24MHz // STC8 的 IRC 频率调整寄存器是 IRC_CR 相关的组合不同型号名称不同 void CLK_Init(void) { // 本例程包对应芯片的 IRC 设置寄存器 // 0x80 表示启动 IRC频率选择通过后续两个寄存器配合完成 IRCCR 0x80; // 等待 IRC 稳定手册要求这几个微秒实际例程里一般读标志位或延时 // 这里的延时不能太短否则 IRC 没起振就往下跑串口波特率就全错了 Delay_Ms(10); }波特率计算是另一个重灾区。STC8 的串口波特率由定时器产生常用的是定时器 1 作为波特率发生器。例程里的计算公式一般是波特率 时钟频率 / (4 * (65536 - 重装值))注意是 4 分频和传统 51 的 12 分频不一样。用 24MHz 时钟算 9600 波特率重装值是 65536 - 24000000 / 4 / 9600 65536 - 625 64911也就是 0xFD8F。如果用错了分频系数算出错误的重装值串口输出的全是乱码而且乱码规律是规律性的容易被误判为硬件问题。提示遇到串口乱码先别怀疑线没接好用示波器或逻辑分析仪抓一下 TXD 引脚的波形数一下每位的时间宽度。如果位宽不对问题一定在波特率重装值不在接线。3.3 串口例程的工程化写法查询方式和中断方式的差异例程包里串口部分通常有两种写法一种是轮询发送、轮询接收简单但 CPU 全被占用另一种是中断接收、查询发送这是我在实际项目里更推荐的形式因为接收不丢数据发送端也不会因为缓冲区阻塞主流程。// 串口 1 初始化9600bps8 位数据1 位停止位 // 使用定时器 1 作为波特率发生器16 位自动重装模式 void UART1_Init(void) { // 设置定时器 1 为 16 位自动重装模式 TMOD 0x0F; // 高四位清零不影响定时器 0 TMOD | 0x20; // 高四位设为 0010定时器 1 工作在模式 28 位自动重装 // 注意STC8 的定时器模式 2 是 16 位重装与传统 51 不同 AUXR | 0x40; // T1x12 1定时器 1 使用 1T 模式不分频 // 设置重装值低字节和高字节 TH1 0xFD; // 高字节 0xFD TL1 0x8F; // 低字节 0x8F合成 0xFD8F 64911 // 验证24000000 / 4 / 9600 62565536 - 625 64911 TR1 1; // 启动定时器 1 // 串口模式设置为 8 位 UART波特率可变 SCON 0x50; // SM00, SM11模式 1REN1允许接收 ES 1; // 打开串口 1 中断 EA 1; // 打开总中断 }这段代码里AUXR | 0x40是最关键的一步。很多人在传统 51 上写串口初始化时没有这个寄存器迁移到 STC8 后照搬旧代码结果波特率变成原来的 12 倍或 1/12。AUXR里的 T1x12 位决定了定时器 1 是 1T 还是 12T 模式。STC8 默认可能是 12T如果不显式设置为 1T重装值算得再精确也没用。中断接收的服务函数写法也有讲究例程里常见的是直接在函数体内处理收到的字节但工程上我更建议用一个环形缓冲区接住数据在主循环里再解析。原因很简单串口是异步的中断什么时候进来你控制不了如果中断处理函数里做太多事比如调用延时、处理字符串就会拖长中断响应时间下一个字节如果到了就丢数据了。4. 定时器、中断与 PWM例程里最容易被误用的三处代码4.1 定时器初值计算16 位自动重装的坑STC8 的定时器有三种工作模式例程里最常见的是 16 位自动重装模式。这个模式和传统 51 的有区别不需要在中断里重新赋初值硬件自动重装省掉了两句赋值也减少了重装误差。// 定时器 0 配置为 16 位自动重装模式定时 1ms // 系统时钟 24MHz1T 模式 void Timer0_Init(void) { AUXR | 0x80; // T0x12 1定时器 0 使用 1T 模式 TMOD 0xF0; // 低四位清零不影响定时器 1 TMOD | 0x00; // 模式 016 位自动重装STC8 特有 // 定时时间 重装值 / 时钟频率 // 1ms x / 24000000x 24000 // 初始计数 65536 - 24000 41536 0xA240 TL0 0x40; // 低字节 0x40 TH0 0xA2; // 高字节 0xA2 TR0 1; // 启动定时器 0 ET0 1; // 打开定时器 0 中断 EA 1; // 打开总中断 }注意一个细节TMOD | 0x00这行看起来是多余的但写上有助于别人理解这是 16 位自动重装模式。实际例程里可能直接用TMOD 0x00前提是不影响定时器 1。如果定时器 1 已经被配置成波特率发生器TMOD 0x00会把定时器 1 的模式也改了串口立刻乱码。这就是为什么我建议用先清零再或运算的方式操作 TMOD不要整体赋值。定时器初值计算还有个容易犯的错忽略了中断响应时间。定时器从溢出到进入中断服务函数之间有中断响应延迟通常是几个时钟周期。如果做高精度计时这几十个纳秒的误差要考虑进去但例程里的 LED 闪烁、按键扫描这种场景完全不用管。只有在做红外解码或者频率测量时才需要精确校正。4.2 中断服务函数的书写规范为什么中断里不能放延时STC8 例程的中断服务函数有固定的写法interrupt关键字后面的数字是中断号这个数字要查手册不能猜。定时器 0 是 1定时器 1 是 3外部中断 0 是 0串口 1 是 4ADC 是 12 之类。写错了编译也能通过但运行时会跳错向量程序直接飞掉。// 定时器 0 中断服务函数中断号 1 // 这里只做一件事将标志位置 1主循环检测到标志位后再处理业务 void Timer0_ISR(void) interrupt 1 { // 标志位需要在头文件中声明为 volatile // 否则编译器可能优化掉循环中的检测导致主循环永远看不到标志翻转 flag_1ms 1; // 不要在中断里调用 printf 或长时间延时 // 中断服务函数只做最短的事其他全部扔给主循环 }关于volatile这是我拆例程时经常提醒别人的一个点。中断服务函数里修改的全局变量主循环检测正常情况下没问题。但开了优化后编译器可能认为主循环里这个变量没有变化做个缓存然后你就看到“中断明明触发了但主循环就是不动”的问题。加个volatile就能解决。例程包里的代码可能没注意这个细节因为默认优化等级是 0不开优化就永远不暴露。中断里放延时是一个高频翻车点。假设你在中断里调用一个Delay_Ms(1)这个延时函数本身依赖定时器或空循环。如果是空循环倒也还好只是中断时间变长如果延时函数依赖定时器中断那就死锁了——中断里等另一个中断。程序表现为“卡死”仿真时看不出问题只有跑硬件才能发现。例程包里不会有这种低级错误但你自己改成可能引入。4.3 PWM 与 PCA呼吸灯和舵机控制的参数调整STC8 的 PWM 模块比传统 51 高级得多可以输出多路独立 PWM也可以配置成互补输出带死区做电机驱动很合适。例程里做呼吸灯是最典型的入门实验核心是改变占空比并让占空比按一定规律变化。// PWM 输出初始化P2.0 引脚映射到 PWM1 通道 // 频率 PWM时钟 / PWM周期计数 void PWM_Init(void) { // 这里以 STC8H 系列的 PWM 模块为例 // ENC 使能 PWM 时钟PWM 时钟源选择内部时钟 PWM_CFG 0x80; // 设置周期21MHz PWM 时钟周期计数 21000输出频率 1kHz PWM_CNTH 0x52; // 21000 8 0x52 PWM_CNTL 0x08; // 21000 0xFF 0x08 // 设置占空比初始 50% PWM_DTY1H 0x29; // 10500 8 0x29 PWM_DTY1L 0x04; // 10500 0xFF 0x04 // 使能 PWM1 输出对应引脚 P2.0 PWM_CR 0x01; }呼吸灯的占空比变化曲线例程里通常用sin函数或者直接线性加减小延时。实际效果上线性渐变看起来不舒服人眼对亮度的感知是对数的同样的占空比增量低亮度区域变化很明显高亮度区域几乎看不出变化。例程如果用的是线性渐变你在低亮度段会觉得跳变严重。改成指数曲线或者正弦查表会好得多。舵机控制是 PWM 的另一类典型应用。舵机需要的频率一般是 50Hz对应周期 20ms脉宽 0.5ms 到 2.5ms 对应 0 到 180 度。这个频率用 STC8 的 PWM 模块很容易实现重点是把 PWM 时钟和周期计数配合好。例程里如果默认频率是 1kHz直接接舵机是不转的因为舵机要求 50Hz。修改方法很多可以直接改 PWM 时钟源或者周期计数值。注意舵机信号的失败模式是“不转”和“抖动”。抖动通常是因为占空比分辨率不够周期计数器太大每一步变化的增量大于舵机分辨率。调低 PWM 时钟让周期计数少一些分辨率反而更合适。5. 避坑STC8 例程移植到自研板上的五个常见坑5.1 现象工程编译报错错误信息指向“无法打开头文件”原因例程工程是在原开发者的绝对路径下创建的包含的#include STC8H.h在你的电脑上找不到对应目录。解决在 Keil C51 的 Options for Target 里找到 C51 选项卡把 Include Paths 手动添加为例程包的 Source 目录。或者更省事的方法直接把 STC8H.h 从例程目录复制到工程根目录因为 Keil 会优先搜索工程文件所在目录。我一般选择后者因为这样压缩包换到任何电脑上都不会出问题。5.2 现象串口输出乱码但不是完全不可读而是首字母正确、后面全错原因这是波特率误差累积的结果。首次传输的字符可能碰巧对了后续字符因为位周期误差逐渐累积错位。典型场景是用了 24MHz 的 IRC但波特率计算时套用了 12T 模式公式实际芯片跑在 1T 模式。解决确认AUXR里 T1x12 的设置再重新计算波特率重装值。建议先用逻辑分析仪测量一个字符的完整波形位时间确认起始位宽度是否等于1 / 波特率。如果差了一倍以上优先检查分频位。5.3 现象ADC 采集值跳动剧烈尤其是采集电源电压时原因STC8 的 ADC 参考电压和电源直接相关采集电源电压时如果开关电源纹波大参考源也跟着波动采集值自然不稳定。另外 ADC 采样的保持电容需要时间充电例程里如果采样通道切换后立即读取值不准确。解决通道切换后加一个短的建立时间通常几十微秒软件上连续采样多次取平均。如果单片机连接了电机或继电器等感性负载检查 ADC 引脚附近是否串接了 RC 滤波。例程里不会考虑你的 PCB 布局这部分要自己加。5.4 现象进入掉电模式后唤不醒外部中断触发但没反应原因STC8 的掉电模式分为普通掉电和深度掉电唤醒源不同。例程里进入掉电前没有正确配置唤醒引脚。另外有些引脚在掉电模式下内部上拉被关闭外部按键接的是“按键按下接地”的电路上拉被关闭后引脚悬空电平不稳定唤醒条件永远无法满足。解决查看数据手册确认当前型号支持哪些唤醒源外接引脚配置为带上拉的高阻输入或准双向口确保待机时引脚电平确定。按键唤醒通常需要外部上拉电阻而不是依赖内部上拉。例程里的代码在开发板上能用因为开发板自带外部上拉电阻自研板就容易翻车。5.5 现象程序跑起来后按键反应迟钝或者按键一次触发两次原因按键消抖处理不当。例程里如果是简单延时消抖在快速点按和长按时会出现误判。另外按键读取时如果配置成准双向口“读前写 1”的操作在按键抖动期间容易出问题。解决用两次读取加确认的方式比如第一次读到低电平后延时 10ms 再读一次如果还是低电平才确认按下。STC8 的 GPIO 配置成高阻输入加外部下拉或上拉比准双向口更适合直接做按键输入因为准双向口的内部弱上拉会和外部电阻形成分压导致电平无法完全拉低或拉高。6. 改造例程成自己的工程骨架从这里开始按你的方式组织代码6.1 驱动层和业务层分开例程不是终点是起点例程包里的文件组织通常是平铺的main.c 里塞满了外设初始化、状态判断和业务逻辑。我用过的工程里超过上千行后的可维护性很差改一个引脚定义要找半天。我的习惯是把驱动层单独抽出来gpio.c、uart.c、timer.c每个文件只做一件事函数接口对外暴露不直接操作寄存器。比如gpio.c提供一个GPIO_SetMode(Port, Pin, Mode)的函数内部通过查表找到对应的 PxM0 和 PxM1 地址而不是在 main.c 里直接P1M0 0x01。这样当板子改版、引脚功能调整时只改一个文件业务逻辑完全不用动。例程包已经帮你验证了底层寄存器配置的正确性你只需要封装接口不用重头验证寄存器值。6.2 一个配置文件统一管引脚映射和时钟频率另一点值得做的是把系统参数集中到一个config.h文件里。包括系统时钟频率、串口波特率、定时器定时间隔、引脚复用选项全部用#define定义。这样以后换芯片或者换板子只改这个文件整个工程的初始化代码都能自动适配。// config.h全工程唯一需要根据硬件修改的文件 #define SYS_CLOCK_HZ 24000000UL // 系统时钟 24MHz #define UART1_BAUD_RATE 9600 // 串口波特率 #define TIMER0_INT_MS 1 // 定时器 0 中断周期单位 ms #define PWM1_FREQUENCY_HZ 1000 // PWM1 输出频率 // 引脚映射统一在这里定义业务代码不要直接出现 P1、P2 等引脚号 #define LED_PIN_PORT 1 #define LED_PIN_BIT 0 #define KEY_PIN_PORT 1 #define KEY_PIN_BIT 1有了这个配置头文件移植到另一颗芯片时如果寄存器地址不同你还需要改驱动层但如果只是换频率、换波特率改 config.h 就够了。我从这套 STC8 例程里学到的最有价值的东西就是“先看配置再改代码”而那之后我每次拿到新例程包都强制自己走一遍“确认芯片型号 → 确认时钟频率 → 确认外设映射 → 编译下载”。这个习惯帮我避开了很多不必要的排查时间也希望帮到你。本文还有配套的精品资源点击获取