简介:这是一份面向嵌入式初学者与单片机开发者的STC单片机C语言范例程序合集,围绕ADC采样、串口通信、EEPROM读写、ISP在系统编程、时钟中断、端口测试及优化编程思想等核心功能展开,适合用于课程实验、自学练手与项目参考。资源包共612个文件,约14.88MB,以plg、uv2、hex、opt、lnp等工程与烧录文件为主,配合h头文件、c源码、asm汇编及pdf说明文档,覆盖ADC、PCA模拟全双工串口、红外收发、PWM、LCD驱动、按键扫描、看门狗等典型模块,并附实验箱布局图与使用说明。目前已有5855人学习下载,读者可从中获得可直接编译运行的完整工程、寄存器配置思路与硬件调试参考,便于快速理解STC单片机各外设的编程方法,提升实际开发效率。
1. 从一块“点不亮”的板子说起:STC 单片机范例程序到底能帮你省下多少试错时间
很多人第一次接触 STC 单片机,卡住的地方往往不是写不出代码,而是代码写完了烧不进去、灯不亮、串口没反应。你手里可能有一块最小系统板、一根 USB 转串口线,还有一份不知道从哪下载的例程,打开一看全是寄存器操作,注释寥寥。STC 单片机范例程序这份资源,解决的就是这个最实际的问题:它把 GPIO 点灯、定时器、串口收发、ADC 采样、PWM 输出、EEPROM 读写这些常用外设的完整工程打包在一起,每个工程都是可以直接编译、直接烧录的独立项目。适合谁?适合刚上手 STC 系列、不想从零搭工程模板的嵌入式新手,也适合需要快速验证某个外设功能的熟手。你不需要先去啃几百页的数据手册,先把例程跑通,再回头对照寄存器,理解会快得多。
2. 工程结构拆解:拿到压缩包后先看哪几个文件
2.1 目录组织与工程模板的复用逻辑
一份典型的 STC 单片机范例程序包,解压后通常不是一个大杂烩,而是按外设功能分目录。常见做法是每个功能一个独立文件夹,比如01_GPIO_LED、02_Timer0、03_UART1、04_ADC、05_PWM、06_EEPROM,每个文件夹里放一个完整的 Keil 工程。这样设计的好处是,你不需要在同一个工程里靠宏定义切换功能,直接打开对应文件夹就能编译烧录,互不干扰。
我一般拿到这种包,第一件事不是急着打开.uvproj,而是先看根目录有没有Common或Library文件夹。如果有,说明作者把延时函数、串口打印、类型定义这些公共代码抽出来了,各个例程通过相对路径引用。这种结构在二次开发时最省事,你改一处公共代码,所有例程都能受益。如果没有公共文件夹,每个例程都是自包含的,那就意味着代码有重复,改的时候要逐个同步。
# 典型的目录结构示意 STC_Examples/ ├── 01_GPIO_LED/ │ ├── main.c │ ├── led.c │ ├── led.h │ └── project.uvproj ├── 02_Timer0/ │ ├── main.c │ ├── timer.c │ ├── timer.h │ └── project.uvproj ├── 03_UART1/ │ ├── main.c │ ├── uart.c │ ├── uart.h │ └── project.uvproj ├── Common/ │ ├── delay.c │ ├── delay.h │ ├── type.h │ └── uart_printf.c └── README.md上面这个结构里,Common文件夹是关键。delay.c提供毫秒和微秒级延时,type.h做类型重定义,uart_printf.c把printf重定向到串口。这些在每个例程里都会被引用。你打开01_GPIO_LED的工程,在 Keil 的工程树里应该能看到Common组,里面包含这些文件。如果看不到,说明路径没配好,需要手动添加。
2.2 Keil 工程配置里最容易忽略的三个选项
打开.uvproj之后,先别急着点编译。STC 单片机的工程配置有几个地方和 STM32 不一样,配错了就是“编译通过但跑不起来”。第一个是芯片型号选择。STC 的 Keil 支持包安装后,在 Device 列表里能搜到具体型号,比如 STC89C52RC、STC12C5A60S2、STC15W4K56S4 等。选错型号最直接的后果是寄存器地址不对,程序跑飞。
第二个是时钟频率设置。在Target选项卡的Xtal里填的数值,会影响 Keil 仿真时的时序计算,但真正决定串口波特率的是你代码里写的定时器初值。很多人在这里填 11.0592MHz,代码里却按 12MHz 算,结果串口出来全是乱码。常见做法是:晶振用 11.0592MHz,串口波特率才准;如果板子上焊的是 12MHz,那串口波特率就得用定时器 2 或者手动算误差。
第三个是输出 HEX 文件的勾选。在Output选项卡里,Create HEX File必须勾上,否则编译完没有可烧录文件。STC-ISP 下载软件只认.hex或.bin,不认.axf。
// Common/delay.h 里常见的宏定义 #ifndef __DELAY_H #define __DELAY_H #include "type.h" // 假设系统时钟为 11.0592MHz // 这两个宏在 STC 例程里出现频率极高 #define MAIN_Fosc 11059200L // 主频,单位 Hz #define DELAY_MS(ms) delay_ms(ms) #define DELAY_US(us) delay_us(us) void delay_ms(u16 ms); void delay_us(u16 us); #endif这段代码里,MAIN_Fosc这个宏很关键。很多例程的延时函数、串口波特率计算、定时器初值都依赖它。如果你板子上的晶振不是 11.0592MHz,第一件事就是改这个宏,然后重新编译。改完还不算完,串口波特率的定时器初值也要跟着重算,否则通信还是有问题。
2.3 从点灯例程看 STC 的 GPIO 模式配置
STC 单片机和传统 8051 最大的区别之一,就是 GPIO 多了模式寄存器。以 STC15 系列为例,每个端口都有PxM0和PxM1两个寄存器,组合起来决定引脚是准双向、推挽、高阻还是开漏。很多新手从 STM32 转过来,习惯性地直接给端口赋值,结果 LED 亮度不对或者根本驱动不了。
// 01_GPIO_LED/led.c 中的初始化片段 #include "led.h" void LED_Init(void) { // 配置 P1.0 为推挽输出,驱动 LED P1M1 &= ~0x01; // P1.0 模式位 M1 清零 P1M0 |= 0x01; // P1.0 模式位 M0 置一 // 组合结果:M1=0, M0=1 -> 推挽输出 P1_0 = 1; // 初始熄灭,取决于硬件接法 } void LED_Toggle(void) { P1_0 = !P1_0; // 翻转电平 }这段代码里,P1M1和P1M0的位操作是 STC 特有的。推挽输出模式下,引脚能输出较大电流,直接驱动 LED 没问题。如果是准双向模式,输出高电平时驱动能力很弱,LED 可能微亮或者不亮。这就是为什么有些人说“代码明明一样,我的灯就是不亮”——大概率是 GPIO 模式没配对。
3. 编译与烧录:从 Keil 到 STC-ISP 的完整链路
3.1 编译前必须确认的芯片型号与头文件
在 Keil 里打开工程后,左侧Project窗口里会列出所有源文件。先看Target名称,通常作者会写成芯片型号。右键Options for Target,在Device标签页确认选中的芯片和实际板子一致。如果列表里找不到你的型号,说明 STC 的 Keil 支持包没装,需要去 STC 官网下载安装。
头文件方面,STC 例程通常包含stc15.h、stc8.h或reg51.h。reg51.h是标准 8051 头文件,只包含基本寄存器,没有 STC 扩展的PxM0、PxM1、AUXR等。如果你在代码里用了这些扩展寄存器,但头文件是reg51.h,编译会报“未定义标识符”。常见做法是:STC12 系列用stc12.h,STC15 系列用stc15.h,STC8 系列用stc8.h。这些头文件在 STC-ISP 软件里自带,也可以从官网下载。
// main.c 中常见的头文件包含顺序 #include "stc15.h" // 芯片寄存器定义 #include "type.h" // 类型重定义 #include "delay.h" // 延时函数 #include "uart.h" // 串口驱动 #include "led.h" // LED 驱动 void main(void) { UART_Init(9600); // 串口初始化,波特率 9600 LED_Init(); // LED 初始化 while (1) { LED_Toggle(); delay_ms(500); // 延时 500ms printf("LED toggled\r\n"); // 串口输出调试信息 } }这段main函数的结构很典型:先初始化外设,然后进死循环。UART_Init(9600)里的参数是波特率,改这个值的时候要同步改定时器初值。printf能用的前提是uart_printf.c里重定向了putchar或者用了TI中断发送。如果串口助手收不到数据,先检查波特率、数据位、停止位是否和代码一致,再检查硬件接线:STC 的TXD接 USB 转串口的RXD,RXD接TXD,共地。
3.2 STC-ISP 下载软件的参数设置与冷启动
编译出.hex文件后,打开 STC-ISP 软件。第一步选芯片型号,必须和 Keil 里选的一致。第二步选串口号,插上 USB 转串口线后,软件会自动识别,如果没识别到,检查驱动是否安装。第三步打开.hex文件。第四步设置硬件选项,比如是否使用内部晶振、复位脚是否用作 IO。
点击“下载/编程”按钮后,软件会提示“正在检测目标单片机”。这时候需要给单片机上电冷启动——也就是先断电,点下载,再上电。很多新手卡在这里,因为板子一直通着电,软件检测不到。冷启动是 STC 单片机 ISP 下载的固定流程,因为芯片上电时会先运行 Bootloader,检测串口是否有下载命令。
提示:如果一直检测不到,先检查串口线是否插紧,再检查板子上的晶振是否起振。有些最小系统板需要手动按复位键,有些则完全断电再上电。
下载成功后,软件会显示“操作成功”。这时候板子上的 LED 应该开始闪烁,串口助手也能看到打印信息。如果 LED 不闪,先量一下 LED 引脚电压是否在翻转,再检查 LED 极性是否接反。
3.3 串口调试:用 printf 定位问题的最快方式
在嵌入式开发里,串口打印是最朴素的调试手段,但也是最有效的。STC 例程里通常会把printf重定向到串口,这样你就能用printf("变量值:%d\r\n", val)这种方式输出中间变量。重定向的代码一般长这样:
// Common/uart_printf.c 中的重定向实现 #include "stc15.h" #include "uart.h" // 重定向 putchar,使 printf 输出到串口 char putchar(char c) { while (!TI); // 等待上一个字节发送完成 TI = 0; // 清除发送完成标志 SBUF = c; // 写入发送缓冲区 return c; }这段代码里,TI是串口发送完成标志位,SBUF是发送/接收缓冲区。while(!TI)会阻塞等待,直到上一个字节发完。这种写法简单,但如果在中断里调用printf,可能会死锁。常见做法是:主循环里用printf没问题,中断里尽量只置标志位,不要直接打印。
串口助手的设置要和代码一致:波特率 9600、数据位 8、停止位 1、无校验。如果收到乱码,先检查晶振频率和波特率是否匹配。11.0592MHz 晶振配 9600 波特率误差为 0,12MHz 晶振配 9600 误差约 0.16%,短距离通信没问题,长距离可能丢包。
4. 核心外设例程精讲:定时器、ADC 与 EEPROM
4.1 定时器中断:从寄存器配置到中断服务函数
定时器是单片机最常用的外设之一,STC 的定时器和标准 8051 兼容,但多了AUXR寄存器来控制分频。以定时器 0 为例,工作模式 1 是 16 位定时器,不自动重装;模式 2 是 8 位自动重装。做精确延时或者波特率发生器,通常用模式 2。
// 02_Timer0/timer.c 中的初始化代码 #include "stc15.h" #include "timer.h" void Timer0_Init(void) { AUXR &= ~0x80; // 定时器 0 时钟为 12T 模式,即 12 分频 TMOD &= ~0x0F; // 清除定时器 0 模式位 TMOD |= 0x01; // 定时器 0 工作模式 1,16 位定时器 // 假设晶振 11.0592MHz,定时 50ms // 12T 模式下,定时器计数频率 = 11.0592MHz / 12 = 921600Hz // 50ms 需要计数 921600 * 0.05 = 46080 次 // 初值 = 65536 - 46080 = 19456 = 0x4C00 TH0 = 0x4C; TL0 = 0x00; ET0 = 1; // 使能定时器 0 中断 EA = 1; // 使能总中断 TR0 = 1; // 启动定时器 0 } // 中断服务函数,中断号 1 对应定时器 0 void Timer0_ISR(void) interrupt 1 { TH0 = 0x4C; // 重新加载初值 TL0 = 0x00; // 用户代码,比如翻转 LED P1_0 = !P1_0; }这段代码里,AUXR寄存器的T0x12位决定定时器时钟是 12T 还是 1T。12T 模式下,定时器计数频率是晶振频率的 1/12,和传统 8051 一致。1T 模式下,计数频率等于晶振频率,定时更精确,但初值计算方式不同。TMOD的低 4 位控制定时器 0,0x01表示模式 1。中断服务函数后面的interrupt 1是 Keil C51 的语法,数字 1 对应定时器 0 中断向量。
初值计算是新手最容易出错的地方。TH0和TL0组成 16 位计数器,从初值开始往上数,数到 65536 溢出,触发中断。所以初值 = 65536 - 需要计数的次数。需要计数的次数 = 定时时间 × 计数频率。把这三个量搞清楚,定时器就不会配错。
4.2 ADC 采样:通道选择与参考电压
STC 的 ADC 模块在 STC12、STC15、STC8 系列上都有,通常是 10 位精度,8 通道。用 ADC 的时候,除了配置寄存器,还要注意参考电压。有些型号的参考电压是内部固定的,有些可以外接。如果测出来的值一直不准,先检查参考电压是否稳定。
// 04_ADC/adc.c 中的采样函数 #include "stc15.h" #include "adc.h" u16 ADC_Read(u8 channel) { // 选择通道,P1ASF 寄存器对应 P1 口 P1ASF = 1 << channel; ADC_CONTR = 0x80; // 使能 ADC 模块 ADC_CONTR |= channel; // 选择通道 ADC_CONTR |= 0x08; // 启动转换 while (!(ADC_CONTR & 0x10)); // 等待转换完成 ADC_CONTR &= ~0x10; // 清除完成标志 // 返回 10 位结果,高 8 位在 ADC_RES,低 2 位在 ADC_RESL return ((u16)ADC_RES << 2) | (ADC_RESL & 0x03); }这段代码里,P1ASF寄存器把 P1 口的某个引脚设为模拟输入。ADC_CONTR的0x80位使能 ADC,0x08位启动转换,0x10位是转换完成标志。结果拼接的时候,ADC_RES是高 8 位,ADC_RESL的低 2 位是低 2 位,拼起来就是 10 位结果。如果只需要 8 位精度,直接读ADC_RES就行。
实际使用中,ADC 的输入电压范围是 0 到 VCC。如果 VCC 是 5V,10 位 ADC 的分辨率就是 5V / 1024 ≈ 4.88mV。测出来的值乘以 4.88mV 就是实际电压。如果 VCC 不稳定,测出来的值会跳。常见做法是:在 VCC 和 GND 之间并一个 0.1uF 电容,滤掉高频噪声。
4.3 EEPROM 读写:掉电保存参数的实现
STC 的 EEPROM 其实是 Data Flash,用来保存掉电后需要保留的参数,比如校准值、用户设置。操作 EEPROM 需要先擦除扇区,再写入。擦除和写入都需要时间,期间 CPU 会暂停。
// 06_EEPROM/eeprom.c 中的读写函数 #include "stc15.h" #include "eeprom.h" void EEPROM_Erase(u16 addr) { IAP_CONTR = 0x80; // 使能 IAP IAP_CMD = 0x03; // 扇区擦除命令 IAP_ADDRH = addr >> 8; IAP_ADDRL = addr & 0xFF; IAP_TRIG = 0x5A; // 触发命令 IAP_TRIG = 0xA5; IAP_CONTR = 0x00; // 关闭 IAP } void EEPROM_Write(u16 addr, u8 dat) { IAP_CONTR = 0x80; IAP_CMD = 0x02; // 字节写入命令 IAP_ADDRH = addr >> 8; IAP_ADDRL = addr & 0xFF; IAP_DATA = dat; IAP_TRIG = 0x5A; IAP_TRIG = 0xA5; IAP_CONTR = 0x00; } u8 EEPROM_Read(u16 addr) { u8 dat; IAP_CONTR = 0x80; IAP_CMD = 0x01; // 字节读取命令 IAP_ADDRH = addr >> 8; IAP_ADDRL = addr & 0xFF; IAP_TRIG = 0x5A; IAP_TRIG = 0xA5; dat = IAP_DATA; IAP_CONTR = 0x00; return dat; }这段代码里,IAP_CONTR的0x80位使能 IAP 操作,IAP_CMD选择命令:0x01读、0x02写、0x03擦除。IAP_TRIG需要依次写入0x5A和0xA5才能触发操作,这是 STC 的固定流程,顺序不能反。擦除是按扇区进行的,一个扇区通常 512 字节,擦除后整个扇区变成0xFF。写入只能把1变成0,所以写之前必须先擦除。
注意:EEPROM 的擦写次数有限,通常 10 万次左右。不要在主循环里频繁写 EEPROM,否则寿命很快耗尽。常见做法是:只在参数变化时写,或者加一个延时,几秒内不重复写。
5. 避坑与排查:STC 例程跑不起来时先查这五条
5.1 编译通过但下载失败
现象:Keil 编译无报错,生成.hex文件,但 STC-ISP 一直提示“正在检测目标单片机”。原因通常是冷启动流程不对,或者串口被占用。解决:先关闭其他占用串口的软件,比如串口助手;然后给单片机断电,点击下载按钮,再上电。如果还是不行,换一根 USB 转串口线,有些线质量差,下载不稳定。
5.2 LED 不亮或亮度异常
现象:程序下载成功,但 LED 不亮,或者微亮。原因大概率是 GPIO 模式没配对。STC 的准双向模式输出高电平驱动能力弱,推挽模式才能正常驱动 LED。解决:检查PxM0和PxM1寄存器配置,确保 LED 引脚设为推挽输出。另外检查 LED 极性,有些板子是低电平点亮,代码里初始电平要对应。
5.3 串口乱码或收不到数据
现象:串口助手收到乱码,或者完全没数据。原因通常是波特率不匹配、晶振频率和代码不一致、接线错误。解决:先确认代码里的MAIN_Fosc和实际晶振一致;再确认串口助手波特率和代码一致;最后检查TXD和RXD是否交叉连接,共地是否接好。如果用的是内部晶振,波特率误差可能较大,建议用外部晶振。
5.4 定时器中断不触发
现象:定时器初始化了,但中断服务函数不执行。原因可能是总中断没开、定时器没启动、中断号写错。解决:检查EA = 1和ET0 = 1是否都写了;检查TR0 = 1是否启动;检查中断服务函数的interrupt编号是否和定时器对应。定时器 0 是 1,定时器 1 是 3,定时器 2 是 5。
5.5 ADC 采样值跳动大
现象:ADC 读出来的值不稳定,跳动几十个数值。原因可能是参考电压不稳、模拟输入引脚悬空、电源噪声大。解决:在 VCC 和 GND 之间加滤波电容;模拟输入引脚接一个下拉电阻,避免悬空;采样时多读几次取平均值。如果对精度要求高,外接一个稳定的参考电压源。
6. 二次开发改造:把例程变成自己的项目模板
6.1 从单个例程到多外设协同的工程重组
例程跑通之后,下一步就是做自己的项目。你不可能把 LED 例程、串口例程、定时器例程各烧一遍,而是要把它们合并到一个工程里。常见做法是:新建一个工程文件夹,把Common目录复制过来,再把需要的外设驱动文件按功能分组添加。比如drivers/led.c、drivers/uart.c、drivers/timer.c,每个驱动配一个头文件,main.c只负责初始化和主循环逻辑。
// 重组后的 main.c 结构 #include "stc15.h" #include "type.h" #include "delay.h" #include "led.h" #include "uart.h" #include "timer.h" #include "adc.h" void System_Init(void) { UART_Init(9600); // 串口初始化 LED_Init(); // LED 初始化 Timer0_Init(); // 定时器初始化 ADC_Init(); // ADC 初始化 } void main(void) { System_Init(); while (1) { u16 adc_val = ADC_Read(0); // 读通道 0 printf("ADC: %d\r\n", adc_val); delay_ms(1000); } }这种结构的好处是,每个驱动模块独立,改一个不影响其他。System_Init集中初始化,主循环只处理业务逻辑。如果你要做低功耗,可以在主循环里加PCON |= 0x01进空闲模式,定时器中断唤醒。
6.2 用宏定义做功能裁剪与参数配置
例程里的参数,比如波特率、定时时间、ADC 通道,最好用宏定义抽出来,放在一个config.h里。这样换板子或者改需求的时候,只改一个文件,不用满工程搜数字。
// config.h 中的可配置参数 #ifndef __CONFIG_H #define __CONFIG_H #define MAIN_Fosc 11059200L // 主频 #define UART_BAUD 9600 // 串口波特率 #define TIMER0_MS 50 // 定时器 0 周期,单位 ms #define ADC_CHANNEL 0 // ADC 通道 #define LED_PIN P1_0 // LED 引脚 #endif改MAIN_Fosc的时候,延时函数和串口波特率都会跟着变,前提是驱动代码里用的是这个宏,而不是硬编码的数字。我见过很多例程,延时函数里直接写for(i=0;i<1000;i++),这种延时不准,换晶振就废了。合格的做法是用定时器做延时,或者用_nop_()配合循环,但循环次数要根据主频算。
6.3 验证改造是否成功的三个检查点
改造完工程,烧录之前先做三个检查。第一,编译无警告。Keil 的警告往往藏着问题,比如未使用变量、类型不匹配,别忽略。第二,串口打印正常。在main开头打印一行版本信息,确认程序跑起来了。第三,外设逐个验证。先点灯,再串口,再定时器,最后 ADC。每加一个外设,烧一次,确认没问题再加下一个。这样出问题的时候,范围小,好定位。
从那以后我每次拿到新的例程包,都强制走一遍“先点灯、再串口、后中断”的流程,确认基础链路通了,再往上堆功能。希望帮到你。
本文还有配套的精品资源,点击获取