简介:STC8系列单片机实验例程合集,面向嵌入式学习与项目开发,从基础到进阶系统覆盖常见外设操作。基础部分包含初始化、LED闪烁、数码管显示和按键检测,适合快速熟悉Keil uVision环境与C语言编程;中级部分涉及UART串口、定时器、PWM调光和A/D、D/A转换,可用于电机控制与模拟信号处理;高级部分则深入到I2C/SPI总线、中断服务和RTOS应用,帮助读者构建复杂系统。压缩包共748个文件,约16.47MB,以C源码(121个)、头文件(63个)和Keil工程文件(76个uvproj/uvopt)为主,同时提供对应hex固件便于直接烧录验证,还包含SHT30/SHT20温湿度、红外遥控、射频收发、OV7670摄像头等模块实验,适合对照硬件进行驱动移植与功能调试。已有1279人学习。通过逐步运行和修改例程,可系统掌握STC8的寄存器配置与接口时序,为实际项目提供扎实的参考基础。
1. STC8实验例程:从89C52迁移到STC8需要重新认识的外设手册
STC8的各种实验例程,表面上是LED、按键、串口、ADC、PWM这些老生常谈的实验集合,实质是一套针对STC8系列新架构的寄存器级驱动代码库。STC8虽然还是51内核,但流水线、时钟树、复位逻辑和大部分外设寄存器都重新设计过,直接照搬STC89C52的代码,十有八九会翻车。这套例程的价值在于:它把官方数据手册里分散的寄存器说明,变成了一个个能编译、能烧录、能观察现象的工程模板。适合正要入门STC8的学生、想评估STC8能否替代现有方案的硬件工程师,以及被某个外设寄存器搞得头大的开发者。
2. 开发环境与工程骨架:STC8例程的编译烧录基础
2.1 芯片选型与工具链:为什么例程多数基于STC8H
STC8家族分几个子系列。STC8H系列外设最全,ADC、PWM、串口、定时器都有,适合做实验底板;STC8G系列更便宜,适合小体积产品;STC8A系列在低功耗方面有优势。实验例程大多以STC8H为基准,因为它的寄存器布局更接近“所有外设都给你留了一份”,学完再往其他系列迁移,只需要改头文件和少数寄存器的位定义。工具链方面,官方SDK不依赖特定IDE,用Keil C51就能编译,输出hex文件后用STC-ISP烧录。我建议把例程工程里的Target Options里Device选成你手里那颗芯片,否则编译出来的代码在跑长延时或大量用xdata时会出问题。
STC8的下载方式和老款51不太一样,这点在工程搭建时就要有心理准备。老款STC89C52用串口下载时,很多开发板做了自动断电复位电路,点一下下载按钮就能搞定;STC8的ISP下载通常要求冷启动,这在后面的烧录流程里会具体说明。如果你手里有多颗不同系列的STC8芯片,最好在工程里用宏区分型号,例如#define CHIP_STC8H,因为不同系列在ADC的位数、PWM的通道数、串口数量上存在差异,靠一个宏控制可以减少迁移时的改动量。
2.2 一个最小工程模板:从main.c到hex的完整路径
建立工程时,最小模板包含三类文件:寄存器头文件、时钟配置、外设驱动。寄存器头文件stc8.h里已经按系列把SFR地址定义好,不用自己改。时钟配置是STC8例程和旧51最大的区别:STC8上电默认使用内部高精度IRC时钟,不需要外部晶振也能跑出11.0592MHz,这是串口波特率精确的关键。工程模板里通常在main()开头先设置时钟频率,再初始化外设。
| 文件 | 作用 |
|---|---|
| stc8.h | 寄存器地址、位定义、中断向量宏 |
| main.c | 主循环、初始化顺序控制 |
| uart.c | 串口驱动、printf重定向 |
| delay.c | 软件延时、定时器封装 |
#include "stc8.h" void main(void) { // 把系统时钟设为内部IRC 11.0592MHz // STC8系列上电默认就在内部IRC运行,这里只是显式确认频率 WDT_CONTR &= ~0x10; // 关闭看门狗,防止启动阶段被复位 UART1_Init(); // 串口初始化,调试输出用 ADC_Init(); // ADC初始化,准备采集模拟量 while(1) { // 主循环逻辑 } }注释里的时钟设置寄存器写法,不同子系列略有差异,以头文件中的宏为准。烧录时STC-ISP也会根据你在软件里选择的IRC频率写入对应配置,保险做法是烧录时“输入用户程序运行时的IRC频率”填11.0592MHz,和代码里设置保持一致。看门狗这行很关键,实验板供电抖动时,看门狗复位比程序bug还难查,我见过有人调了两天程序,最后发现是ISP软件里勾选了“启动看门狗”选项。
2.3 烧录流程与冷启动:STC8第一个拦路虎
STC8和STC89C52的ISP下载方式不同。后者很多板子支持自动下载,前者多数型号需要冷启动:先让目标板处于断电状态,点软件里的“下载/编程”,再给目标板上电,芯片在复位后的短暂时间内才会进入ISP模式。我最初在STC8实验板上反复超时,就是没搞懂这一点。解决方法是把板子的电源开关当成下载按钮的一部分。STC-ISP里“每次下载前先发送复位命令”的选项慎用,它依赖USB转串口的DTR/RTS信号,有些USB转串口芯片的驱动不标准,反而会卡在“正在检测目标单片机”。
提示:如果冷启动也失败,把波特率降到2400试试。连线过长或USB转串口质量差时,高速同步会失败,降低速率往往能过。
烧录成功后先别急着拔线,用ISP软件自带的“串口助手”发一个字节,如果芯片能原样返回,说明串口例程的收发通路是通的。这个习惯能帮你把“烧录问题”和“代码问题”快速分开,省下很多排查时间。
3. GPIO与定时器实验例程:从LED闪烁到按键状态机
3.1 GPIO模式配置:准双向、推挽、开漏与高阻
STC8的每个IO端口都有PxM0和PxM1两个模式寄存器,组合出四种模式:准双向(默认)、推挽、高阻输入、开漏。LED实验最常见的坑是把推挽输出当成唯一输出模式,导致引脚驱动能力过强或反向时过流。实验例程里点灯通常用准双向模式,因为它兼容输入输出,驱动LED这种低功耗外设够用。
#include "stc8.h" // LED接在P1.0,低电平点亮 sbit LED = P1^0; void delay_ms(unsigned int ms) { unsigned int i, j; for(i = 0; i < ms; i++) for(j = 0; j < 1100; j++); } void main(void) { P1M0 = 0x00; // P1全部准双向 P1M1 = 0x00; while(1) { LED = 0; // 点亮 delay_ms(500); LED = 1; // 熄灭 delay_ms(500); } }delay_ms里的1100是经验值,和编译器优化等级强相关,换优化等级或换芯片型号就要重新标定。这就是为什么例程里定时器比软件延时更可靠。IO模式配置必须在操作引脚前完成,否则上电瞬间引脚可能是高阻或浮空,会误触发后续逻辑。准双向模式内部有弱上拉,直接驱动LED时电流通常只有几毫安,亮度够用但不会超过规格,这是例程选择它的底层原因。
如果你要把LED改成用推挽输出驱动,记得给LED串一个限流电阻,否则引脚高电平时电流可能超过LED最大额定值。开漏模式常用于I2C或需要电平转换的场景,实验例程里很少单独出现,但理解它和准双向的差别,对后面调总线类外设有帮助。
3.2 定时器例程:重装初值、1T/12T与中断服务程序
STC8定时器的模式0是16位自动重装,和传统51的13位模式0不同。定时器时钟可以选1T(不分频)或12T(12分频)。实验例程中我习惯用1T模式配11.0592MHz算出精确延时,因为1T模式下计算值正好落在16位范围内,能覆盖几毫秒到几十毫秒的常用延时。
#include "stc8.h" // 1ms定时,11.0592MHz,Timer0模式0(16位自动重装),1T void Timer0_Init(void) { AUXR |= 0x80; // Timer0设为1T模式 TMOD &= 0xF0; // 只修改Timer0相关位 TMOD |= 0x00; // 模式0,16位自动重装 TL0 = 0xCD; // 65536 - 11059 = 54477 = 0xD4CD TH0 = 0xD4; ET0 = 1; // 使能Timer0中断 EA = 1; // 打开总中断 TR0 = 1; // 启动Timer0 } void Timer0_ISR(void) interrupt 1 { static unsigned int cnt = 0; cnt++; if(cnt >= 500) { cnt = 0; LED = ~LED; // 500ms翻转一次 } }初值计算基于11.0592MHz的1T模式:计数频率11.0592MHz,1ms需要11059个脉冲,重装值65536 - 11059 = 54477。这里有一个很阴间的细节:STC8的模式0叫16位自动重装,但寄存器位TMOD的模式编码和传统51完全一样,代码里“TMOD |= 0x00”只是形式上保留,真正决定自动重装的是芯片型号自身行为,所以别指望通过改TMOD让模式回到13位。中断里不要做耗时操作,LED翻转这种几条指令的没问题,如果要在中断里处理串口收发,一定要把缓冲区做大并加临界区保护。
关于1T和12T的选择,还有一个容易被忽略的点:STC8H的定时器在1T模式下计数频率等于系统时钟,所以如果你的系统时钟不是11.0592MHz而是24MHz,算出来的初值要重算。很多例程注释里写着“@11.0592MHz”,换时钟后不改初值,就会得到几乎两倍快的定时,这是最常见的定时翻车现场。
3.3 按键实验:阻塞消抖与状态机写法的取舍
单按键实验的例程大多用延时消抖:检测到按键电平变化后,延时10ms再确认一次。这种方法简单,但在主循环里会阻塞其他任务。进阶一点的例程会引入状态机:把按键扫描当作定时器中断中的周期任务,用“未按下、消抖中、已按下、等待释放”几个状态迁移。
typedef enum {KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_WAIT_RELEASE} KeyState; KeyState key_state = KEY_IDLE; unsigned char key_scan_cnt = 0; void KeyTask(void) // 每1ms调用一次 { unsigned char key_level = KEY; // 读取P3.2电平 switch(key_state) { case KEY_IDLE: if(key_level == 0) { key_state = KEY_DEBOUNCE; key_scan_cnt = 0; } break; case KEY_DEBOUNCE: if(key_level == 0) { if(++key_scan_cnt >= 10) { key_state = KEY_PRESSED; key_event = 1; // 置位事件标志 } } else key_state = KEY_IDLE; break; case KEY_PRESSED: if(key_level == 1) key_state = KEY_WAIT_RELEASE; break; case KEY_WAIT_RELEASE: if(key_level == 1) key_state = KEY_IDLE; break; } }这个代码里我简化了状态变量,实际例程中会用一个全局事件标志,避免在中断上下文里直接处理业务逻辑。状态机的好处是主循环不会被消抖延时卡住,代价是代码量增加,且每个状态迁移都要考虑“如果按键在中途松开”的边界情况。我在给A同学的例程里看到过按键状态机漏了等待释放状态,导致一次按下触发多次,后来把状态图补全才解决。
如果你的例程只需要处理一个按键,阻塞消抖其实够用;但一旦要同时扫描多个按键,状态机几乎是唯一选择。还有一点,状态机的扫描周期要稳定,例程里通常放在定时器1ms中断中调用,而不是放在主循环里用软件延时,否则系统忙的时候按键响应会变慢。
4. 串口、中断与模拟量实验:打通数据上行链路
4.1 串口实验:波特率计算、printf重定向与接收缓存
STC8串口实验例程以UART1最常用,波特率常常选9600或115200。STC8的波特率可以使用独立的波特率发生器,也可以复用定时器1或定时器2。例程里我通常让UART1走定时器1,因为这样代码结构最接近传统51,方便对比。波特率计算依赖系统时钟、分频和SMOD位,这里最容易出错的是把STC8默认的1T时钟当成了传统51的12T。
void UART1_Init(void) { SCON = 0x50; // 8位可变波特率,使能接收 TMOD &= 0x0F; TMOD |= 0x20; // 定时器1,8位自动重装 AUXR &= 0xBF; // 定时器1 12T模式 TH1 = 0xFD; // 9600bps @11.0592MHz TL1 = 0xFD; TR1 = 1; ES = 1; EA = 1; } void UART1_ISR(void) interrupt 4 { if(RI) { RI = 0; rx_buf[rx_head] = SBUF; rx_head = (rx_head + 1) % RX_BUF_SIZE; } if(TI) TI = 0; }TH1=0xFD对应公式:波特率 = 11.0592MHz / 12 / 32 / (256 - 0xFD) = 9600。如果你的系统时钟不是11.0592MHz而是别的频率,需要重新计算。接收缓存用环形队列,这是STC8例程和纯教学例程的区别,纯教学代码通常在中断里直接处理一个字节,生产环境必须缓存。
printf重定向需要单独实现putchar:
char putchar(char c) { SBUF = c; while(!TI); TI = 0; return c; }需要在Keil工程选项里勾选“使用微库”,否则重定向不会生效。勾选后调用printf就会从串口输出,但要注意printf是阻塞式的,在低波特率下大量打印会拖慢主循环,我一般只在调试阶段打开,发布版本换成自己写的格式化输出或直接发二进制数据。
4.2 中断优先级与外部中断:掉电唤醒不是玄学
STC8外部中断的例程通常包括INT0下降沿触发、INT1上升沿触发、以及掉电唤醒实验。外部中断的配置比定时器简单,但有一个容易忽略的点:STC8的IO口在使能外部中断前必须已配置为输入模式,否则外部引脚悬空时会因为浮空输入产生随机跳变,导致中断“莫名触发”。这个现象很让人头疼,排查半天以为代码写错,其实是引脚复用没配好。
void INT0_Init(void) { P3M0 &= ~0x04; // P3.2设为准双向 P3M1 &= ~0x04; IT0 = 1; // 下降沿触发 EX0 = 1; EA = 1; } void INT0_ISR(void) interrupt 0 { // 标记事件,主循环处理 int0_flag = 1; }中断服务函数只有几字节,为什么还要“标记事件”?这是51系列中断的一个通用约束,Keil C51的中断函数编译后会做现场保护和恢复,如果中断里跑复杂逻辑,栈开销和嵌套风险都会上来。实验例程里这样写是在演示一个习惯:中断只置位,主循环查询。
掉电唤醒是STC8一个比较有吸引力的功能,例程里通常把它和外部中断绑定:进入掉电模式前先把IO配置好,外部信号来了能从掉电模式唤醒。这个功能我在一个低功耗采集项目中用过,实测待机电流能压到微安级,但坑也明显——唤醒后系统时钟需要重新稳定,如果唤醒后立刻读串口或ADC,大概率读到错误数据,需要加一段延时等待时钟稳定。
4.3 ADC采样:参考电压、通道切换与数据抖动
STC8的ADC是10位或12位精度,实验例程通常用12位模式,结果寄存器分高8位和低4位。采样前必须把对应引脚配成高阻输入,否则模拟信号会被IO内部下拉电阻分压。例程里我用ADC采集电位器或光敏电阻电压,再把结果通过串口打印。
void ADC_Init(void) { P1M0 = 0x00; P1M1 = 0x01; // P1.0高阻输入 ADCCFG = 0x2F; // 12位, ADC时钟 = SYS_CLK / 16 ADC_CONTR = 0x80; // 开启ADC电源 } unsigned int ADC_Read(unsigned char ch) { ADC_CONTR &= 0xF8; // 清通道选择 ADC_CONTR |= ch; // 选择通道 ADC_CONTR |= 0x40; // 启动转换 while(!(ADC_CONTR & 0x20)); // 等待ADC_FLAG置位 ADC_CONTR &= ~0x20; // 清标志 return (ADC_RES << 8) | ADC_RESL; }ADCCFG的bit5是结果位数选择,0x2F这个值是在“12位、右对齐”的前提下算出来的,如果你用10位模式,要把对应位改掉。ADC第一通道采样结果经常比后面的通道“毛”很多,因为刚上电时内部采样电容没有稳定,例程里我习惯在初始化后做一次弃样:连续采三次,丢掉第一次,取后两次平均。
ADC参考电压的选择直接影响测量精度。如果VREF引脚没有接外部基准,默认参考电压是VCC,而VCC可能因为负载变化波动几十毫伏,这对12位ADC来说是几十个LSB的误差。实验例程里倾向于直接用VCC做参考,因为方便;但如果你要测的是电池电压,VCC本身就在变化,测出来永远是满量程附近的值。这时应该换成内部参考电压或外部基准芯片。
5. 实验例程避坑指南:烧录、配置与逻辑的踩坑记录
5.1 烧录与供电:冷启动失败、芯片发热、反复复位
现象1:点下载按钮后,ISP软件一直提示“正在检测目标单片机”,目标板没有任何反应。
原因:STC8在复位后短暂时间窗口内才会检测ISP命令,如果目标板一直处于上电状态,芯片已经正常跑用户程序,自然不会进ISP。
解决:先点“下载/编程”,再给目标板上电。如果是电池供电,断开电源线再重新接上也算冷启动。
现象2:烧录成功后,芯片发热严重,某个引脚带负载时电压被拉低。
原因:IO口被配置成推挽输出后直接驱动了重负载,或者两个推挽输出引脚短接。
解决:把相关IO口改为准双向模式,先检查PxM0/PxM1寄存器,再查原理图确认没有引脚对地短路。
现象3:程序跑几秒后自动复位,重新上电又正常。
原因:看门狗没有关闭或喂狗周期太短。STC8例程中有些型号默认看门狗关闭,有些在ISP烧录时勾选了“使能看门狗”,导致程序启动后看门狗已经开始计时。
解决:在main()第一行关闭看门狗,或者在初始化时配置并定期喂狗。我在排查某个板子时花了一下午,最后发现是烧录软件里看门狗选项被误勾选。
5.2 外设配置:定时器快慢、乱码、采样跳变
现象4:用定时器写1ms延时,实测变成0.5ms或2ms。
原因:AUXR中T0x12位被意外改变,定时器工作在1T或12T模式与预期不一致;初值计算时没有考虑自动重装。
解决:在初始化函数里显式设置AUXR对应位,不要依靠默认值;用示波器或逻辑分析仪量一次实际周期,反推当前模式。
现象5:串口打印出乱码,波特率配成9600但PC端收到的是乱码。
原因:系统时钟不是11.0592MHz,或者USB转串口线质量差导致丢位;另一个常见原因是printf重定向时勾选了微库但putchar里没有清TI标志。
解决:先用STC-ISP确认烧录时IRC频率是多少,再核对代码里的计算值。把波特率降到4800测试,如果恢复正常就是信号完整性问题。
现象6:ADC值在某个通道跳动几十个LSB,接电位器拧到中间位置时抖动明显。
原因:引脚悬空、采样时间不足、参考电压VREF没有加去耦电容。
解决:给ADC引脚对地加0.1uF电容,ADC_Read里增加采样延时,VREF引脚并联10uF电解电容和0.1uF瓷片电容。还不行就改用内部参考电压,并做多次采样取平均。
5.3 代码逻辑:中断不响应、跑飞、EEPROM丢数据
现象7:外部中断函数写好了,触发条件也满足,但断点就是进不去。
原因:没有开EA总中断,或者某个外设的中断标志一直保持置位,导致同优先级其他中断被阻塞。
解决:检查EX0/EX1对应位、EA位,并在中断服务函数里主动清标志。STC8有些外设的中断标志需要软件清,不能用“自动清零”想当然。
现象8:程序里用了大数组后,运行一段时间变量被莫名改写。
原因:Keil C51默认把小变量放DATA区,大数组放XDATA需要显式声明,如果数组定义在函数内且超过data空间,编译器会报错或静默溢出。
解决:把大数组用xdata关键字声明,例如unsigned char xdata buf[1024];,同时检查链接映射文件,看data使用率是否超过80%。
现象9:往EEPROM写数据后,重新上电读出来是0xFF。
原因:EEPROM写前必须先擦除,或者写入过程中掉电导致扇区损坏。
解决:写EEPROM前先擦除扇区,写入后读回校验;重要数据双备份,两个扇区交替写。
6. 进阶验证技巧:用串口做一台“虚拟示波器”
没有示波器时,验证PWM或定时器例程最笨但最有效的办法是利用串口回传数据。以PWM占空比实验为例,PWM输出经过一级RC低通滤波后接到ADC引脚,理论电压 = 占空比 × VCC。我写一个简单的上位机脚本接收串口数据,把电压值换算成占空比实时画折线。这样即使边沿时间测不到,平均值也能确认占空比寄存器是否按预期变化。
对于定时器精度,更直接的办法是让定时器中断里翻转一个GPIO,然后用一块几十块钱的逻辑分析仪去看波形周期。逻辑分析仪不需要探头补偿,采样率2MHz就够看毫秒级信号。我在某个例程里曾经把定时器初值算错一位,延时变成刚好的两倍,用LED肉眼看几乎无感,接上逻辑分析仪量一下就立刻暴露。
除了验证PWM,这套“ADC回读 + 串口上报”的组合还能用来验证电源噪声、按键抖动、外部中断触发频率。自己写串口上位机时,不需要复杂的框架,Python的pyserial就能满足大部分调试需求。第一次调STC8的PWM时,我用逻辑分析仪看波形发现高电平时间反了,后来再调STC8就养成“输出前先算占空比寄存器值,然后在输出端加RC滤波用ADC验证”的双保险。这次调STC8例程最困难的部分不是寄存器,而是调试意识,先把验证手段准备好,代码写起来会踏实很多。希望帮到你。
本文还有配套的精品资源,点击获取