
简介这是一套面向嵌入式初学者与单片机开发者的STC单片机C语言范例程序合集围绕ADC采样、串口通信、EEPROM读写、ISP在系统编程、时钟中断、端口测试及优化编程思想等核心功能展开适合用于课程实验、自学练手与项目参考。压缩包共612个文件约14.88MB以plg、uv2、hex、opt、lnp等工程与烧录文件为主配合h头文件、c源码、asm汇编及pdf说明文档覆盖ADC、PCA模拟全双工串口、红外收发、PWM、LCD驱动、按键扫描、看门狗等典型实验模块并附实验箱布局图与使用说明书。目前已有5855人学习下载读者可借助完整工程与源码快速理解STC单片机各外设的配置流程对照C与汇编实现掌握底层寄存器操作积累可复用的驱动代码与排错思路提升实际开发效率。1. 从一份“祖传”工程包说起STC单片机范例程序到底能帮你省下多少事如果你手头正躺着一块 STC 单片机或者刚接手一个用 STC 芯片做的老项目大概率会遇到一个很现实的问题官方例程散落在各种压缩包里有的用 Keil C51有的用 SDCC还有的干脆是汇编想找一个能直接编译、直接烧录、直接改的完整工程比翻自己三年前的代码还费劲。这份 STC 单片机范例程序资源包解决的就是这个事。它把 STC 常用外设的驱动代码按模块整理成独立工程从 GPIO 点灯、定时器、串口通信到 ADC 采样、PWM 输出、EEPROM 读写每个例程都是一个可以独立打开、编译、下载的完整项目。适合谁刚接触 STC 的嵌入式新手可以拿它当“抄作业”的模板做过 STM32 想回头搞 51 的熟手可以快速对齐 STC 的寄存器操作习惯需要维护老设备的工程师也能从里面找到串口、定时器这类基础模块的可靠实现。一句话它不是教程是一堆能直接跑的工程骨架。2. 工程结构拆解拿到压缩包先看哪几个目录2.1 顶层目录的划分逻辑一份整理得比较规范的 STC 范例包顶层通常不会把所有 .c 文件堆在一起而是按“外设类型 工程模板”来分。常见做法是根目录下先分01_GPIO、02_Timer、03_UART、04_ADC、05_PWM、06_EEPROM这样的编号目录每个目录里再放一个完整的 Keil 工程文件夹。编号不是为了好看是为了让你按顺序从简单到复杂过一遍GPIO 跑通了再去看定时器串口调通了再去碰 ADC这个顺序对新手特别友好。每个外设目录内部结构基本一致01_GPIO/ ├── main.c // 主程序包含初始化调用和主循环 ├── gpio.c // GPIO 驱动实现 ├── gpio.h // 驱动头文件对外暴露接口 ├── config.h // 芯片型号、时钟频率等全局配置 ├── Project.uvproj // Keil C51 工程文件 └── Project.uvopt // Keil 工程选项文件这种“一个外设一个工程”的做法好处是隔离性强。你调串口的时候不会被 ADC 的中断代码干扰想移植到自己的项目里直接把gpio.c和gpio.h拷过去改一下config.h里的引脚定义就行。我一般拿到这种包第一件事是打开config.h确认芯片型号和晶振频率跟手头板子一致不一致就先改这里不然后面所有延时、波特率全是错的。2.2 config.h 里的关键参数config.h是整个工程的“总开关”通常包含这几类定义// config.h - 全局配置头文件 #ifndef __CONFIG_H #define __CONFIG_H // 芯片型号选择不同型号寄存器地址可能不同 #define STC8A8K64S4A12 1 // 系统时钟频率单位 Hz影响延时和串口波特率 #define MAIN_Fosc 24000000L // 串口波特率配置 #define UART1_BaudRate 115200L // 是否使能看门狗 #define ENABLE_WDT 0 #endif这里MAIN_Fosc是最容易翻车的地方。STC 不同系列的芯片出厂默认时钟可能不一样有的用内部 IRC有的用外部晶振。如果你板子上是 11.0592MHz 的晶振而config.h里写的是 24MHz串口打出来就是乱码定时器也不准。所以每次新建工程先确认这个值再去看延时函数和串口初始化这是血泪经验。2.3 驱动层与应用层的分离好的范例程序不会把寄存器操作直接写在main.c里。以 GPIO 为例gpio.c里通常长这样// gpio.c - GPIO 驱动实现 #include gpio.h // 初始化 LED 引脚为推挽输出 void GPIO_LedInit(void) { P2M1 ~0x01; // 设置 P2.0 为推挽输出 P2M0 | 0x01; P2_0 1; // 默认输出高电平LED 熄灭 } // 翻转 LED 状态 void GPIO_LedToggle(void) { P2_0 !P2_0; }main.c里只调用GPIO_LedInit()和GPIO_LedToggle()不直接碰P2M1、P2M0这些寄存器。这样做的好处是哪天你把 LED 从 P2.0 挪到 P1.5只需要改gpio.c里的引脚定义main.c一行都不用动。移植到别的项目时整个gpio.c和gpio.h直接拷走改一下引脚宏定义就能用。这种分层思路在 51 这种资源紧张的芯片上看起来有点“重”但实际维护起来省心得多。3. 跑通第一个例程从 Keil 编译到 STC-ISP 烧录3.1 编译环境的准备STC 单片机主流开发环境是 Keil C51也有用 SDCC 的但范例包一般以 Keil 工程为主。如果你还没装 Keil去官网下 C51 版本安装时注意两点一是安装路径不要带中文和空格二是装完后需要把 STC 的器件库导进去否则新建工程时找不到 STC8A8K64S4A12 这类型号。STC-ISP 下载软件里自带“Keil 仿真设置”功能点一下就能把器件库和仿真驱动装好比手动复制文件靠谱。装好之后打开范例包里的Project.uvproj先别急着编译看一眼 Options for Target 里的设置Target 标签页确认晶振频率和config.h里一致Output 标签页勾选“Create HEX File”不然编译完没有 .hex 文件可烧C51 标签页确认 Include Paths 里包含了当前目录否则找不到gpio.h这三处检查完按 F7 编译正常情况应该零错误零警告。如果报“无法打开 source file”多半是工程路径变了在 Project 窗口里把缺失的文件重新添加一下就行。3.2 烧录工具与接线STC 芯片烧录用的是串口不需要专门的下载器一根 USB 转 TTL 线就能搞定。接线就三根TXD 接芯片的 RXDRXD 接芯片的 TXDGND 对接。注意 STC 的串口是交叉接法别接成 TXD 对 TXD那样永远下载不了。打开 STC-ISP 软件步骤是选择芯片型号比如 STC8A8K64S4A12选择串口号就是 USB 转 TTL 线对应的 COM 口打开 .hex 文件就是 Keil 编译出来的那个点“下载/编程”按钮给单片机上电冷启动STC 的下载协议要求冷启动也就是点完下载按钮之后再给芯片上电它才会进入 bootloader。如果你先上电再点下载大概率提示“正在检测目标单片机...”然后一直卡住。这个坑几乎每个新手都会踩一次记住顺序先点下载再上电。3.3 串口打印验证跑通 GPIO 点灯之后建议马上试一下串口例程。串口是后面调试所有外设的基础串口通了你才能打印 ADC 采样值、看定时器计数、输出调试信息。串口初始化的核心代码通常是这样// uart.c - 串口初始化以 STC8 系列为例 void UART1_Init(void) { SCON 0x50; // 8 位数据可变波特率 T2L (65536 - (MAIN_Fosc / 4 / UART1_BaudRate)); T2H (65536 - (MAIN_Fosc / 4 / UART1_BaudRate)) 8; AUXR | 0x15; // 定时器2做波特率发生器1T 模式 AUXR | 0x01; // 串口1 用定时器2 做波特率 ES 1; // 使能串口中断 EA 1; // 开总中断 } // 串口发送一个字节 void UART1_SendByte(uint8_t dat) { SBUF dat; while (!TI); // 等待发送完成 TI 0; // 清除发送标志 }这里MAIN_Fosc和UART1_BaudRate都来自config.h波特率计算公式跟芯片系列有关STC8 用定时器 2 做波特率发生器时是MAIN_Fosc / 4 / BaudRate而老一点的 STC89 系列用定时器 1 时是MAIN_Fosc / 12 / 32 / BaudRate。如果你从 STC89 的例程直接搬到 STC8 上波特率公式不改串口出来的就是乱码。这个细节在范例包的注释里一般会写但很多人不看注释直接抄代码然后就卡在乱码上。4. 核心外设驱动拆解定时器、ADC 与 EEPROM 的典型写法4.1 定时器中断从寄存器配置到回调定时器是 51 单片机里最常用的外设做任务调度、按键消抖、PWM 生成都离不开它。STC8 系列的定时器配置比传统 51 灵活很多支持 1T 模式也就是一个时钟周期计一次数不分频。一个典型的定时器初始化// timer.c - 定时器0 初始化1ms 中断一次 void Timer0_Init(void) { TMOD 0xF0; // 清除定时器0 模式位 TMOD | 0x00; // 定时器0 工作在模式016 位自动重载 TL0 (65536 - MAIN_Fosc / 1000); // 1ms 计数值低字节 TH0 (65536 - MAIN_Fosc / 1000) 8; // 高字节 TR0 1; // 启动定时器0 ET0 1; // 使能定时器0 中断 EA 1; // 开总中断 } // 定时器0 中断服务函数 void Timer0_ISR(void) interrupt 1 { static uint16_t cnt 0; cnt; if (cnt 1000) { // 累计 1000 次即 1 秒 cnt 0; P2_0 !P2_0; // 每秒翻转一次 LED } }这里MAIN_Fosc / 1000算的是 1ms 对应的计数值。如果MAIN_Fosc是 24MHz1T 模式下 1ms 就是 24000 个计数65536 - 24000 41536写入 TL0 和 TH0。注意 STC8 的定时器模式 0 是 16 位自动重载跟传统 51 的模式 116 位不重载不一样传统 51 在中断里需要手动重装初值STC8 不用硬件自动重载。这个差异导致很多从 STC89 转过来的代码在 STC8 上跑起来定时不准因为中断里多了一次手动重装反而把自动重载的值覆盖了。4.2 ADC 采样通道选择与滤波STC8A8K64S4A12 内部集成了 12 位 ADC最多 8 个通道。ADC 例程的核心是三步选通道、启动转换、读结果。常见写法// adc.c - ADC 初始化与采样 void ADC_Init(void) { P1M1 ~0x01; // P1.0 设为高阻输入做 ADC 输入 P1M0 ~0x01; ADCCFG 0x0F; // 结果右对齐时钟分频 ADC_CONTR 0x80; // 使能 ADC 模块 } // 读取指定通道的 ADC 值带 8 次平均滤波 uint16_t ADC_Read(uint8_t ch) { uint32_t sum 0; uint8_t i; ADC_CONTR 0x80 | ch; // 选择通道 for (i 0; i 8; i) { ADC_CONTR | 0x40; // 启动转换 while (!(ADC_CONTR 0x20)); // 等待转换完成 ADC_CONTR ~0x20; // 清除完成标志 sum (ADC_RES 8) | ADC_RESL; } return (uint16_t)(sum / 8); // 取平均降低噪声 }ADC_CONTR的 bit6 是启动位bit5 是完成标志读完之后要手动清零。ADC_RES和ADC_RESL拼起来是 12 位结果右对齐时高 8 位在ADC_RES低 4 位在ADC_RESL的低 4 位。这里做了 8 次平均是因为 STC 内部 ADC 在没有外部基准的情况下单次采样跳动比较大取平均是最简单的滤波方式。如果你对精度要求更高可以换成中值滤波或者一阶滞后滤波但 8 次平均在大多数场景下够用了。4.3 EEPROM 读写掉电保存的可靠做法STC 系列内部有 Data Flash可以当 EEPROM 用存一些掉电不能丢的参数比如校准值、设备地址、运行时间累计。操作上跟外部 I2C EEPROM 不一样STC 的 IAP 寄存器直接操作不需要走 I2C 时序。典型流程// eeprom.c - 内部 EEPROM 读写 void EEPROM_Write(uint16_t addr, uint8_t dat) { IAP_CONTR 0x80; // 使能 IAP设置等待时间 IAP_CMD 0x02; // 写命令 IAP_ADDRH addr 8; IAP_ADDRL addr 0xFF; IAP_DATA dat; IAP_TRIG 0x5A; // 触发序列 IAP_TRIG 0xA5; IAP_CMD 0; // 清除命令 IAP_CONTR 0; // 关闭 IAP } uint8_t EEPROM_Read(uint16_t addr) { IAP_CONTR 0x80; IAP_CMD 0x01; // 读命令 IAP_ADDRH addr 8; IAP_ADDRL addr 0xFF; IAP_TRIG 0x5A; IAP_TRIG 0xA5; IAP_CMD 0; IAP_CONTR 0; return IAP_DATA; }触发序列0x5A和0xA5必须连续写入中间不能被打断所以写 EEPROM 的时候最好关中断。另外 STC 的 EEPROM 擦写次数有限虽然标称 10 万次但如果你在 main 循环里频繁写几天就能写坏一个扇区。常见做法是加一个“变化才写”的判断或者用两个扇区交替写做磨损均衡。范例包里一般只给基础读写磨损均衡需要自己加这是二次开发时值得花时间的地方。5. 避坑与排查STC 开发中最容易翻车的五个点5.1 串口乱码先查时钟频率再查波特率公式现象串口助手收到一堆乱码或者偶尔对几个字符。原因通常有两个一是config.h里的MAIN_Fosc跟实际芯片时钟不一致比如板子上是 11.0592MHz 晶振配置里写 24MHz二是波特率计算公式用错了系列STC89 和 STC8 的公式不一样。解决先用示波器或者频率计测一下芯片实际输出时钟或者写个最简单的 GPIO 翻转程序用示波器看翻转周期反推时钟。确认时钟后对照芯片手册里的波特率公式重新算一遍别直接抄别的系列的代码。5.2 定时器不准模式 0 和模式 1 的区别现象定时器中断频率跟预期差很多比如想要 1ms 实际是 1.2ms。原因STC8 的定时器模式 0 是 16 位自动重载模式 1 才是传统的不重载。很多从 STC89 转过来的代码在中断里手动重装初值但 STC8 模式 0 已经自动重载了手动重装反而把自动重载的值覆盖掉导致周期变长。解决确认TMOD里的模式位STC8 用模式 0 时中断里不要重装初值用模式 1 时才需要。5.3 下载失败冷启动顺序和串口占用现象STC-ISP 一直显示“正在检测目标单片机”进度条不动。原因一是没有冷启动先上电再点下载二是串口被其他软件占用比如串口助手还开着同一个 COM 口三是 USB 转 TTL 线的 TXD/RXD 接反了。解决先关掉所有串口助手确认接线是交叉的然后点下载按钮之后再给单片机上电。如果还是不行换一根 USB 转 TTL 线试试有些便宜线在 STC 的下载协议下兼容性不好。5.4 ADC 跳动大参考电压和滤波现象ADC 采样值跳动十几甚至几十个 LSB读出来的数据没法用。原因STC 内部 ADC 的参考电压默认是 VCC如果 VCC 本身不稳采样值就会跟着跳另外没有加滤波单次采样噪声大。解决硬件上在 VCC 和 GND 之间加去耦电容软件上做多次平均或者中值滤波。如果精度要求高可以外接基准电压芯片但大多数场景下 8 次平均加去耦电容就够了。5.5 EEPROM 写丢数据触发序列被打断现象EEPROM 写入后读出来还是旧值或者偶尔写成功偶尔失败。原因IAP_TRIG的0x5A和0xA5触发序列被中断打断导致写操作没有真正触发。解决在写 EEPROM 的代码前后关中断和开中断确保触发序列连续执行。另外注意写之前要先擦除对应扇区STC 的 EEPROM 是按扇区擦除的不擦除直接写数据写不进去。6. 二次开发怎么改从范例到产品的三个改造思路6.1 把外设驱动抽成独立模块范例包里的驱动是按外设分的但每个工程都是独立的。如果你要做产品第一步是把gpio.c、uart.c、timer.c、adc.c、eeprom.c这些驱动文件抽到一个公共目录建一个统一的bsp文件夹然后每个驱动配一个.h对外只暴露初始化函数和读写接口。这样你的产品工程里main.c只负责业务逻辑硬件相关的全在bsp里。移植到不同板子时只改bsp里的引脚定义和config.h业务代码不动。6.2 用状态机替代延时函数范例程序为了简单经常用delay_ms()做延时比如按键消抖、LED 闪烁。但在实际产品里delay_ms()会阻塞 CPU导致串口收不到数据、按键响应迟钝。改造思路是把这些延时逻辑改成状态机用定时器中断驱动。比如按键消抖可以每 10ms 在定时器中断里采样一次按键状态连续 3 次相同就确认这样主循环里不用任何delayCPU 可以腾出来处理串口和 ADC。// key.c - 状态机按键扫描10ms 调用一次 typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED } KeyState; static KeyState keyState KEY_IDLE; static uint8_t keyCnt 0; void Key_Scan(void) // 在 10ms 定时器中断里调用 { switch (keyState) { case KEY_IDLE: if (KEY_PIN 0) { keyState KEY_DEBOUNCE; keyCnt 0; } break; case KEY_DEBOUNCE: if (keyCnt 3) { // 连续 3 次即 30ms if (KEY_PIN 0) { keyState KEY_PRESSED; Key_OnPress(); } else keyState KEY_IDLE; } break; case KEY_PRESSED: if (KEY_PIN 1) keyState KEY_IDLE; break; } }这个状态机不占用主循环时间按键响应也稳定。我一般会把所有需要周期执行的任务都挂到定时器中断里主循环只做低优先级的后台处理这样整个系统的实时性会好很多。6.3 加一层参数管理方便量产校准产品量产时经常需要写序列号、校准值、设备地址如果每次都改代码重新编译效率太低。改造思路是在 EEPROM 里划一块参数区定义一个结构体上电时读出来通过串口命令修改。比如// param.c - 参数管理 typedef struct { uint16_t magic; // 标志字判断参数区是否已初始化 uint16_t deviceAddr; // 设备地址 int16_t adcOffset; // ADC 校准偏移 uint32_t runTime; // 累计运行时间单位秒 } DeviceParam; static DeviceParam param; void Param_Load(void) { uint8_t *p (uint8_t *)param; uint16_t i; for (i 0; i sizeof(param); i) p[i] EEPROM_Read(PARAM_BASE i); if (param.magic ! 0x55AA) { // 首次上电写默认值 param.magic 0x55AA; param.deviceAddr 0x0001; param.adcOffset 0; param.runTime 0; Param_Save(); } } void Param_Save(void) { uint8_t *p (uint8_t *)param; uint16_t i; for (i 0; i sizeof(param); i) EEPROM_Write(PARAM_BASE i, p[i]); }这样产线工人只需要通过串口发一条命令就能改设备地址不用重新烧录固件。magic字用来判断参数区是不是第一次使用如果是空白 Flash读出来全是 0xFFmagic不等于0x55AA就写入默认值。这个套路在 STC 项目里很实用尤其是需要小批量生产的时候。从那以后我每次拿到新的 STC 范例包都强制自己先跑一遍 GPIO 和串口确认时钟和波特率没问题再去看其他外设。因为这两个是最基础的它们通了后面的 ADC、PWM、EEPROM 才有调试的通道。希望帮到你。本文还有配套的精品资源点击获取