简介:这是一套面向嵌入式初学者与单片机开发者的STC单片机C语言范例程序合集,围绕ADC采样、串口通信、EEPROM读写、ISP在系统编程、时钟中断、端口测试及优化编程思想等核心功能展开,适合用于课程实验、自学练手与项目参考。资源包共612个文件,约14.88MB,以plg、uv2、hex、opt、lnp等工程与烧录文件为主,配合h头文件、c源码、asm汇编及timer-pwm、pca-pwm、ir-tx、lcd1602_driver、spi-pm25lv040等模块文件,另含pdf说明书、doc布局图与xls记录表,覆盖从代码到硬件的完整实验链路。目前已有5855人学习下载。读者可借助这些实例掌握外部TL431基准测压、PCA模拟全双工串口、内部RC时钟与ID读取、看门狗及低功耗唤醒等典型用法,并参考实验箱说明与更新记录快速搭建调试环境,提升STC单片机编程与排错能力。
1. 从一份 STC 单片机范例程序说起:为什么它值得你花时间拆一遍
如果你手头正躺着一块 STC 的单片机开发板,或者刚接手一个用 STC 芯片做的老项目,大概率会遇到同一个问题:官方手册翻了几十页,寄存器配置还是对不上,定时器算出来的初值烧进去就是不准。这份 STC 单片机范例程序,就是冲着这个场景来的。它不是一份只讲原理的教程,而是一堆可以直接编译、烧录、跑起来的工程文件,覆盖了 GPIO、定时器、串口、ADC、PWM、EEPROM 读写这些最常被用到的外设。适合谁?适合刚从 STM32 或者 Arduino 转过来、被 STC 的寄存器操作和 ISP 下载方式搞得有点懵的嵌入式新手,也适合需要快速验证某个外设配置的老手。我拿到这份资源的第一反应是:先别急着看代码,把工程结构摸清楚,比什么都重要。
2. 拆开范例包:工程结构、编译链路与芯片型号匹配
2.1 目录里到底放了什么
一份典型的 STC 范例程序包,解压之后通常能看到几个平行的文件夹,比如GPIO、Timer、UART、ADC、PWM、EEPROM,每个文件夹里是一个独立的 Keil 工程。每个工程下面一般有User、Driver、Output这几个子目录。User放main.c和中断服务函数,Driver放外设的初始化代码,Output是编译产物。有些包还会带一个Doc文件夹,里面塞着芯片手册和原理图。我一般会先打开main.c扫一眼,看看它调了哪些初始化函数,这样能快速判断这个例程覆盖了哪些外设。
这里有个容易翻车的地方:不同系列的 STC 芯片,寄存器名字和地址可能完全不一样。比如 STC89 系列和 STC8 系列,虽然都是 8051 内核,但外设寄存器的定义差得很远。如果你拿 STC8 的例程直接烧到 STC89 的板子上,编译可能过,但跑起来大概率是玄学现象——灯不亮、串口没输出、定时器不中断。所以第一步永远是确认芯片型号,再去找对应的例程文件夹。
2.2 编译前的三个关键配置
在 Keil 里打开工程之后,别急着点 Build。先检查三个地方:
第一,Device 选对没有。在Project -> Options for Target -> Device里,要选到和你手上芯片对应的型号。STC 的芯片在 Keil 官方器件库里不一定都有,很多时候需要手动安装 STC 提供的器件支持包,或者直接选一个兼容的 8051 型号,再手动改头文件。
第二,时钟频率设对没有。在Target标签页里有一个Xtal频率,这个值必须和你实际用的晶振或者内部时钟一致。STC 很多型号支持内部时钟,出厂默认可能是 11.0592MHz 或者 24MHz,如果你这里填错,定时器算出来的初值全是错的。
第三,输出 HEX 文件勾上没有。在Output标签页里,Create HEX File必须勾上,否则 STC-ISP 下载工具找不到可烧录的文件。
// 以 STC8 系列为例,系统时钟初始化常见写法 #include "stc8.h" void SystemInit(void) { // 选择内部高精度时钟,具体频率根据型号手册调整 IRC24MCR = 0x80; // 使能内部 24MHz 时钟 while (!(IRC24MCR & 0x01)); // 等待时钟稳定 CLKDIV = 0x00; // 系统时钟不分频 }这段代码的逻辑很直接:先使能内部时钟源,等它稳定,再设置分频系数。参数IRC24MCR和CLKDIV是 STC8 系列特有的寄存器,不同型号名字可能不同,一定要对着手册改。如果你用的是外部晶振,这段就不需要,直接在 Keil 里设好频率就行。
2.3 从编译到烧录的完整链路
编译通过之后,打开 STC-ISP 下载工具。步骤是:选芯片型号、选串口号、打开 HEX 文件、点下载、然后给单片机上电。注意,STC 的下载需要冷启动——也就是先点下载,再断电重新上电,它才能进入 ISP 模式。很多人第一次用的时候,点了下载没反应,就是因为板子一直在通电状态。
提示:如果串口一直识别不到,先检查 USB 转串口驱动装了没有,再检查 TX/RX 有没有接反。STC 的下载电路通常只需要 TX、RX、GND 三根线。
3. 外设例程怎么读:GPIO、定时器与串口的参数化拆解
3.1 GPIO 例程:从点灯到按键消抖
GPIO 例程通常是第一个要跑的。STC 的 GPIO 配置比 STM32 简单,但模式设置容易搞混。以 STC8 为例,每个端口有PxM0和PxM1两个寄存器,组合起来决定引脚是准双向、推挽、高阻还是开漏。准双向口是 8051 的传统模式,灌电流能力强,但拉电流弱;推挽模式驱动能力强,适合直接驱动 LED。
// GPIO 初始化:P2.0 推挽输出,P2.1 准双向输入 void GPIO_Init(void) { P2M0 = 0x01; // P2.0 设为推挽输出 P2M1 = 0x00; P2M0 &= ~0x02; // P2.1 设为准双向输入 P2M1 &= ~0x02; }参数说明:P2M0和P2M1是按位控制的,每一位对应一个引脚。0x01表示只把 P2.0 对应的位置 1,其他位保持 0。如果你要同时配置多个引脚,直接按位或就行。按键消抖部分,例程里一般用延时函数做软件消抖,但实际项目里我更推荐用定时器做状态机消抖,稳定性高很多。
3.2 定时器例程:初值计算与中断频率
定时器是 STC 单片机里最容易出问题的地方。8051 的定时器是向上计数的,溢出后产生中断。初值计算公式是:初值 = 65536 - (系统时钟 / 12 / 中断频率)。注意,传统 8051 是 12 分频,但 STC 很多新型号支持 1T 模式,也就是不分频,这时候公式里的 12 就要去掉。
// 定时器0初始化,1ms中断一次,假设系统时钟 24MHz,1T 模式 void Timer0_Init(void) { TMOD &= 0xF0; // 清除定时器0模式位 TMOD |= 0x00; // 定时器0设为模式0(16位自动重载) TL0 = (65536 - 24000) & 0xFF; // 低8位 TH0 = (65536 - 24000) >> 8; // 高8位 ET0 = 1; // 使能定时器0中断 EA = 1; // 开总中断 TR0 = 1; // 启动定时器0 }这段代码里,24000就是 24MHz 除以 1000 得到的计数值。如果你用的是 12T 模式,这个值要除以 12。中断服务函数里记得手动重装初值,除非你用的是自动重载模式。我见过太多人忘了重装,结果中断只进一次就没了。
3.3 串口例程:波特率配置与收发缓冲
串口例程的核心是波特率计算。STC 的串口有两种模式:模式 1 是 8 位 UART,波特率可变;模式 0 是同步移位寄存器,一般不用。波特率公式是:波特率 = 系统时钟 / (32 * (65536 - 初值)),如果是 1T 模式,还要再乘一个系数。实际配置的时候,直接用 STC-ISP 工具里的波特率计算器最省事,它会把初值算好给你。
// 串口初始化,9600 波特率,系统时钟 24MHz,1T 模式 void UART_Init(void) { SCON = 0x50; // 模式1,8位UART,允许接收 T2L = 0xE0; // 波特率初值低8位,具体值用ISP工具算 T2H = 0xFF; // 波特率初值高8位 AUXR |= 0x01; // 定时器2作为波特率发生器 AUXR |= 0x04; // 定时器2 1T 模式 ES = 1; // 使能串口中断 EA = 1; }收发缓冲这块,例程里一般只给一个字节的收发,实际项目里建议加一个环形缓冲区,不然高速收发的时候很容易丢数据。我一般会定义一个 64 字节的接收缓冲数组,在中断里往里面塞,主循环里慢慢处理。
4. 避坑与排查:STC 例程跑不起来时先查这五件事
4.1 现象:编译通过,烧录也成功,但灯就是不亮
原因:最常见的是 GPIO 模式设错了。比如你以为是推挽输出,实际寄存器配成了高阻输入,引脚根本没驱动能力。还有一种可能是开发板上的 LED 是共阳极接法,你输出低电平才亮,但例程里写的是输出高电平。
解决:先拿万用表量一下引脚电压,确认有没有变化。如果电压不变,回去查PxM0和PxM1的配置。如果电压变了但灯不亮,检查 LED 的极性,把输出电平反过来试一下。
4.2 现象:串口能发不能收,或者收到的全是乱码
原因:波特率不对。要么是系统时钟频率和 Keil 里设的不一致,要么是波特率初值算错了。还有一种情况是串口线太长或者没有共地,导致信号畸变。
解决:先用 STC-ISP 工具里的波特率计算器重新算一遍初值,确认系统时钟频率和实际一致。然后检查 TX/RX 有没有接反,GND 有没有共地。如果还是乱码,把波特率降到 4800 试试,能通说明是信号完整性问题。
4.3 现象:定时器中断进不去,或者进了只执行一次
原因:中断服务函数里忘了重装初值,或者中断优先级配置有问题。还有一种可能是EA总中断没开,或者ET0没使能。
解决:检查中断服务函数里有没有TL0 = ...; TH0 = ...;这两行。如果是自动重载模式,检查TMOD里的模式位设对没有。再确认EA = 1和ET0 = 1都写了。
4.4 现象:EEPROM 读写数据对不上
原因:STC 的 EEPROM 操作需要先擦除再写入,而且擦除和写入都需要时间,如果连续操作没有等待,数据就会错。还有一种可能是地址算错了,STC 的 EEPROM 地址和程序空间是分开的,不能直接按程序地址访问。
解决:每次写之前先擦除对应扇区,擦除和写入之后加一个延时或者查询忙标志。地址计算参考手册里的 EEPROM 章节,不同型号的扇区大小不一样。
4.5 现象:ADC 采样值跳动很大
原因:参考电压不稳,或者采样通道配置错了。STC 的 ADC 一般用 VCC 做参考,如果 VCC 本身有纹波,采样值就会跳。还有一种可能是采样时间设得太短,内部采样电容还没充到位。
解决:在 ADC 输入引脚旁边加一个 0.1uF 的滤波电容。采样时间设长一点,比如把ADCCFG里的采样周期位调大。如果对精度要求高,建议用外部基准电压芯片。
5. 进阶用法:把例程改造成自己的项目模板
5.1 建立自己的驱动层
例程里的代码通常是面向单个外设的,直接拿来做项目会显得散。我一般会做一层简单的封装:把 GPIO、定时器、串口、ADC 的初始化函数统一放到bsp.c里,对外只暴露BSP_Init()一个接口。这样主函数里只需要调一次初始化,后面改硬件配置也只改bsp.c一个文件。
// bsp.h #ifndef __BSP_H #define __BSP_H void BSP_Init(void); void LED_On(void); void LED_Off(void); void UART_SendByte(unsigned char dat); #endif // bsp.c #include "bsp.h" #include "stc8.h" void BSP_Init(void) { GPIO_Init(); Timer0_Init(); UART_Init(); ADC_Init(); } void LED_On(void) { P20 = 0; // 假设 LED 接在 P2.0,低电平点亮 } void LED_Off(void) { P20 = 1; }这样封装之后,主函数里就干净很多。换芯片的时候,只需要改bsp.c里的寄存器操作,上层逻辑不用动。
5.2 用条件编译适配不同型号
STC 的型号太多,如果每个型号都维护一份代码,维护成本很高。我一般用条件编译来处理差异:
#if defined(STC8A) #define SYS_CLK 24000000UL #define UART_T2L 0xE0 #elif defined(STC15) #define SYS_CLK 22118400UL #define UART_T2L 0xD0 #else #error "请先定义芯片型号" #endif在 Keil 的Options for Target -> C51里,Define框里填上型号宏,比如STC8A,编译的时候就会自动选对应的参数。这样一份代码可以覆盖多个型号,省得来回改。
5.3 验证方法:用示波器和串口打印双重确认
改完代码之后,怎么确认它真的跑对了?我的习惯是:先用示波器量引脚波形,确认频率和占空比符合预期;再通过串口打印关键变量的值,确认逻辑没问题。比如定时器中断,我会在中断里翻转一个引脚,用示波器量翻转频率,同时串口打印一个计数变量,两个数据对得上,才说明定时器配置没问题。
注意:串口打印本身会占用时间,如果中断频率很高,打印可能会影响实时性。这时候可以只翻转引脚,用示波器看波形。
5.4 一个具体技巧:把 EEPROM 当配置存储器用
很多 STC 项目需要保存一些掉电不丢的参数,比如阈值、校准系数。我一般会在 EEPROM 里划出两个扇区,一个存参数,一个存备份。写入的时候先擦备份扇区,把新参数写进去,再擦主扇区,把备份复制回来。这样即使写入过程中断电,至少还有一个扇区的数据是完整的。
// 双扇区备份写入示例 void EEPROM_SaveConfig(unsigned char *buf, unsigned int len) { unsigned int addr_backup = 0x0200; // 备份扇区地址 unsigned int addr_main = 0x0000; // 主扇区地址 EEPROM_Erase(addr_backup); EEPROM_Write(addr_backup, buf, len); EEPROM_Erase(addr_main); EEPROM_Write(addr_main, buf, len); }这个思路不复杂,但能有效防止单扇区写入时掉电导致的数据丢失。从那以后我每次做带参数存储的项目,都强制走一遍双扇区备份,哪怕参数只有几个字节。希望帮到你。
本文还有配套的精品资源,点击获取