简介这是一套针对IAP15W4K61S4单片机的传感器与外围模块实验例程集合内含15个相互独立的KEIL工程覆盖声控、红外、温度、步进电机、光敏、触摸、按键与数码管等常见应用既能作为51单片机进阶学习的配套资料也适合课程设计或竞赛前快速验证传感器驱动代码。压缩包共243个文件总量仅671KB主要包含C源文件、头文件、KEIL工程配置文件uvproj以及编译生成的hex固件同时附带lst、m51等调试过程文件可帮助读者在无开发板时也能对照工程理解编译链接流程。每例均提供可编译源码与烧录文件目录按实验模块命名清晰归类从定时器/延时两种步进电机驱动到DS18B20、DHT11传感器串口显示都给出了完整的初始化与数据处理逻辑便于直接移植或在此基础上扩展功能。目前已有1182人学习下载在同类单片机源码分享中具有较高的参考价值适合正在学习IAP15W4K61S4或STC增强型51系列的用户收藏使用。1. 这套 IAP15W4K61S4 实验例程真正值得读的是配置而不是源码IAP15W4K61S4 不是普通 51它是 1T 增强型 805161KB Flash、4KB SRAM还有内部高精度 RC 时钟和 10 位 ADC跑声控、红外、温度、步进电机、光敏这类传感器实验完全够用。你把这个压缩包解压后看到的 15 个例程源码绝大多数代码逻辑并不复杂真正决定能不能跑起来的是三件事主频配了多少、定时器工作在 1T 还是 12T、烧录时有没有选对型号。这套例程的服务对象很明确做课设、备赛蓝桥杯、或者想快速验证传感器模块好坏的人。新手可以照例程抄抄完能点亮老手则应该直接看它的寄存器配置反推这颗芯片的时钟树和外设边界。本文按工程结构 → 单传感器驱动 → 步进电机 → 多例程合并 → 验证这条线往下走。2. 例程工程怎么组织、Keil 怎么选型、STC 怎么烧录动手改任何一例之前先把整套工程的套路摸清楚。这类 15 例的传感器例程包最常见的组织方式是每个传感器一个独立工程文件夹里面放一个 main.c 加一个该传感器的驱动文件如 ds18b20.c、adc.c公共的延时、串口模块单独放。这样做的好处是你改某一例不会影响其他例程坏处是公共代码被复制了 15 份你改延时函数得改 15 遍。我一般拿到手第一件事不是看 main.c而是把公共文件找出来确认延时函数和串口函数用的是同一个版本的实现。2.1 15 个例程的常见目录约定每个例程独立公共文件单独放典型的目录结构长这样虽然不是每个包都完全一致但大同小异/01_LED_流水灯 main.c, delay.c /02_声控 main.c, sound.c, delay.c /03_红外 main.c, pir.c, delay.c /04_DS18B20温度 main.c, ds18b20.c, delay.c /05_步进电机 main.c, motor.c, delay.c /06_光敏 main.c, adc.c, delay.c /com delay.c, uart.c, stc15w4k.h这里有一个关键点IAP15W4K61S4 的例程几乎都依赖 STC 官方头文件STC15W4K.H。这个头文件里定义了 P0M0、P1ASF、ADC_CONTR、AUXR 这些扩展 SFR传统reg51.h里没有它们。如果你的工程编译报未定义的标识符九成是头文件没包含对或者 Keil 里没把 STC 的头文件路径加进去。2.2 Keil 里选型、STC-ISP 里定主频顺序不能反Keil C51 默认的设备数据库里没有 IAP15W4K61S4。最常见做法是用 STC-ISP 工具里的添加型号到 Keil功能把 STC 设备库装进 Keil就能直接搜到 IAP15W4K61S4。如果你不想装退而求其次选 Intel 8052 也能编译通过原因是这颗芯片的 8051 内核指令集完全兼容扩展 SFR 靠头文件补齐就行。选完型号之后还有一个非常容易被忽略的点IAP15W4K61S4 依赖内部 RC 振荡器工作主频不是由代码决定的而是在 STC-ISP 烧录界面里选的。如果例程里默认烧录时选了 11.0592MHz而你实际烧录时选了 24MHz串口波特率、DS18B20 时序、电机速度全部会偏但程序照常跑。先确认主频再编译代码这个顺序别反了。下面是一个典型的 main.c 骨架里面几个常量是后面所有例程共用的基准#include STC15W4K.H #define FOSC 11059200UL // 必须与 STC-ISP 里选择的主频一致 #define BAUD 9600UL void main(void) { P0M0 0x00; P0M1 0x00; // P0 口设为准双向 P1M0 0x00; P1M1 0x00; // P1 口默认准双向ADC 通道除外 P2M0 0x00; P2M1 0x00; P3M0 0x00; P3M1 0x00; uart_init(); EA 1; // 开总中断 while (1) { // 外设轮询逻辑 } }P0M0/P0M1这组寄存器是 STC15 系列特有的端口模式控制00 表示准双向口10 表示推挽输出01 表示高阻输入11 表示开漏。例程里如果不初始化端口模式默认也是准双向但显式写出来能避免某些引脚上电瞬间电平不确定的问题。FOSC这个宏后面所有延时、波特率、定时器初值都依赖它所以务必和烧录时选的 IRC 频率一致。2.3 烧录先点下载再上电这是 STC 系和 J-Link 最大的区别STC 单片机没有 SWD 接口它靠串口的 ISP 引导程序下载。IAP15W4K61S4 的IAP后缀说明它支持在系统编程下载流程比传统 STC89 还方便一点但核心操作是一样的。STC-ISP 的烧录步骤如下选择型号IAP15W4K61S4注意别选成 STC15F2K60S2SFR 基本一样但 Flash 和 EEPROM 不同。选择串口号USB 转串口设备在设备管理器里看到的 COM 号。设置 IRC 频率11.0592MHz 或例程注释里写的值。打开编译生成的 .hex 文件。点击下载/编程按钮。给目标板重新上电或者按板子上的断电按键再上电。第 6 步是关键。STC 的下载引导程序只在上电复位后的极短时间内检测下载指令所以要先让 STC-ISP 处于等待状态再给单片机冷启动。如果你点了下载但板子一直没反应先检查 USB 转串口的 TTL 电平是否正确IAP15W4K61S4 是 5V 器件3.3V 的 USB 转串口模块虽然能通但稳定性差一些。提示烧录完成后拔掉串口线再跑例程。串口模块的 TX 如果一直挂在 RXD 上可能干扰某些对时序敏感的外设比如 DS18B20。3. DS18B20、红外、光敏、声控四类传感器的驱动写法与参数坑这 15 个例程里传感器部分占了大多数。四类最常见的传感器分别是温度DS18B20、人体红外HC-SR501、光敏LM393 模块、声控驻极体话筒 LM393。它们的驱动难度差别很大DS18B20 要严格按时序读光敏和声控本质上就是读一个电平或 ADC 值。难点不在代码在于你不知道模块的电路细节。3.1 DS18B20 温度单总线时序的三个关键时隙主频一变全废DS18B20 是单总线器件一根 IO 线既要供电又要传数据所以它不区分时钟线通信靠主机精确控制高低电平的持续时间。IAP15W4K61S4 的例程里这部分代码看起来很长其实就三个动作复位、写一个字节、读一个字节。下面是最常见的实现sbit DQ P2^0; bit ds18b20_reset(void) { bit presence; DQ 0; delay_us(500); // 拉低 480-960us 产生复位脉冲 DQ 1; delay_us(60); // 释放总线等待从机应答 presence DQ; // 读到 0 说明存在 delay_us(420); return presence; // 返回 0 表示设备正常 } void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DQ 0; DQ dat 0x01; // 写 0 和写 1 的区别低电平持续时长 delay_us(60); DQ 1; // 释放总线 dat 1; } } unsigned char ds18b20_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { DQ 0; _nop_(); // 主机拉低至少 1us DQ 1; // 释放让从机驱动总线 dat 1; if (DQ) dat | 0x80; delay_us(60); // 读时隙总长约 60-120us } return dat; }这段代码的逻辑说明写字节时每写一位前先把总线拉低然后根据数据位决定继续拉低还是立即释放。读字节时主机只需要把总线拉低一小段再释放从机自己决定把总线拉高还是拉低。_nop_()在 11.0592MHz 下大约是一个机器周期1T 模式下约 0.09us刚好满足读时隙的最小拉低时间要求。最大的坑就是delay_us()。如果例程是按 11.0592MHz 写的你烧录时选了 24MHz或者反过来DS18B20 最常见的故障是读回 0xFF 或者 0x85上电复位值。判断方法很简单读到的温度一直是 85 度先查延时而不是查接线。DS18B20 的正常转换流程是复位 → 发 0xCC跳过 ROM→ 发 0x44启动转换→ 等 750ms12 位分辨率→ 复位 → 发 0xCC → 发 0xBE读暂存器→ 读两个字节。750ms 这个等待在很多例程里用的是delay_ms(800)如果你后面要合并多个传感器这里必须改成非阻塞第 5 章会展开。注意如果读回的温度是 0 或 85先量 DQ 引脚上有没有 4.7k 上拉电阻。DS18B20 工作在寄生供电模式时没有上拉电阻会读回 0。3.2 人体红外 HC-SR501 与红外对管数字输入第一件事是消除抖动红外这个标题词对应的可能是两类模块热释电人体红外 HC-SR501或者红外对管避障模块。HC-SR501 输出的是 TTL 电平没有人任何协议单片机这边就是一个普通 IO 读。但它有个特性容易被当 bug上电后模块内部自检输出会拉高大约 3 秒。你刚烧录完程序看 LED 一直亮以为误触发其实那是自检。例程里如果要写防误判一般是加一段启动延时等模块稳定后再进入主循环。HC-SR501 上有两个电位器一个调灵敏度检测距离一个调输出保持时间最短约 3 秒最长可到几分钟。很多例程的一直触发问题不是代码写错是延时电位器被拧到了最大。代码层面能做的只有消抖下面是上升沿触发的写法#define PIR P3_2 bit pir_triggered(void) { static bit last_level 0; if (PIR !last_level) // 检测上升沿 { delay_ms(10); // 简单消抖 if (PIR) { last_level 1; return 1; // 返回一次触发事件 } } if (!PIR) last_level 0; return 0; }这里的逻辑说明只在上跳沿返回一次触发主循环里必须把这个函数放在高频轮询位置不能隔着delay_ms(200)去查否则电平变化会被漏掉。如果你的例程包里红外指的是红外测温模块 MLX90614那就是另一套东西了它走 I2C 接口SCL/SDA 两根线例程代码里一定会有 I2C 起始、停止、读写字节的函数别和数字输出的 HC-SR501 混为一谈。3.3 光敏传感器AO 走 ADCDO 走 IO先搞清楚输出极性光敏模块最常见的电路是光敏电阻分压后接 LM393 比较器模块同时引出 DO数字输出和 AO模拟输出。DO 接单片机普通 IO拧电位器设阈值AO 接 ADC 引脚可以读光照的连续变化。IAP15W4K61S4 内置 10 位 ADC通道在 P1 口上这是这颗芯片比传统 51 强的地方。ADC 初始化代码在例程里通常是这样的void adc_init(unsigned char ch) { P1ASF 1 ch; // 把对应 P1.x 设为模拟输入 ADC_CONTR 0x80 | (ch 0x07); // 打开 ADC 电源选择通道 delay_ms(2); // 等待 ADC 电源稳定 } unsigned int adc_read(unsigned char ch) { unsigned int val; ADC_CONTR | 0x08; // 置 ADC_START 启动转换 while (!(ADC_CONTR 0x10)); // 等待 ADC_FLAG 置位 ADC_CONTR ~0x10; // 软件清标志 val (ADC_RES 2) | (ADC_RESL 0x03); // 高 8 位 低 2 位 return val; // 返回 10 位结果 }参数说明P1ASF 1 ch将 P1 口的对应位设为模拟输入这一位一旦设成 1该引脚就不能再当普通 IO 用。ADC_CONTR的 bit7 是电源控制位bit3 是启动位bit4 是转换完成标志转换完成后必须软件清零。ADC_RES存高 8 位ADC_RESL的低 2 位是 10 位结果的低 2 位拼接顺序别搞反。光敏模块的一个典型误读很多人默认光照越强 ADC 值越大实际上取决于模块的分压电阻接在光敏电阻上面还是下面。光照强时光敏电阻阻值变小如果模块是上拉接法AO 电压升高如果是下拉接法AO 电压降低。例程写的是拿 ADC 值做阈值判断你直接烧进去可能发现白天黑夜反了。处理办法不是改公式而是先串口打印一组白天和夜晚的 ADC 值再定阈值正向反向自己心里有数。下表汇总这四类传感器的接口方式传感器模块输出信号单片机接口最容易踩的坑DS18B20 温度单总线数字任意 IO 4.7k 上拉主频和延时不匹配读回 85 或 0xFFHC-SR501 红外TTL 数字任意 IO上电 3 秒自检高电平电位器误调光敏模块DO / AOIO 或 ADC (P1.x)AO 极性看分压接法阈值反向声控模块TTL 数字任意 IO阈值边界抖动LED 乱闪3.4 声控模块比较器输出防误触发的两个办法声控模块和光敏模块的电路结构几乎一样都是驻极体话筒信号经过运放放大后进 LM393 比较器。区别在于声控信号的幅值变化很快声音一停比较器输出马上回到原状态。这导致一个现象主循环轮询的时候刚好读到高电平就触发读的时候声音刚好过阈值就漏掉。例程里普遍的做法是检测到高电平就翻转 LED听起来对但实际声音在阈值附近抖动时LED 会不规则闪烁。两个处理办法任选其一。第一个是加延时锁定检测到触发后200ms 内不再响应第二次if (sound_trigger() (sys_tick - sound_lock_time 200)) { sound_lock_time sys_tick; // 执行声控动作 }第二个是软件迟滞触发阈值设为 60% 电平释放阈值设为 40%中间留一段死区。但对只有一路 DO 的模块来说迟滞做不了只能靠锁定时间。另外注意声控模块的灵敏度电位器别拧到底环境底噪会让比较器持续翻转表现是程序卡死或者 LED 常亮。例程如果出现这种问题先螺丝刀拧电位器再考虑改代码。4. 步进电机例程八拍时序、定时器调速和上下电抖动步进电机在这套例程里通常用的是 28BYJ-48 ULN2003 驱动板四相五线5V 供电。它便宜、皮实、课设够用。驱动代码核心就三块相序表、换相逻辑、调速方式。很多例程把这三块混在一个delay循环里单跑没问题但和别的传感器一合并就丢步原因在定时器那节说。4.1 ULN2003 驱动输入高电平是导通还是关断先纠正一个最容易误解的点。ULN2003 是达林顿管阵列内部结构是输入高电平→输出导通到地。也就是说单片机的 IO 输出高电平对应电机绕组被拉到 GND电流从 5V 经绕组流到 ULN2003 的输出端。所以相序表里的 1 表示该相通电逻辑上好像是高电平驱动实际上 ULN2003 干的是反相的活。很多例程的相序表写反电机只能震动不转圈就是这个细节。28BYJ-48 是四相电机常用的驱动方式是八拍也称半步相序为A → AB → B → BC → C → CD → D → DA。对应到 IO 的数组就是// 假设 IN1-IN4 接 P1.0-P1.3 unsigned char code step_table[8] { 0x08, 0x0C, 0x04, 0x06, 0x02, 0x03, 0x01, 0x09 };这个数组里每个值的二进制位对应四个绕组的通电状态例如0x0C是 0000 1100表示 B、C 两相通电。按数组索引递增走电机正转索引递减电机反转。28BYJ-48 减速比是 1:64输出轴转一圈需要 4096 个八拍拍子。这个数值在例程里是验收标准写一个转一圈停的例程如果转过了或者没转够先怀疑相序表而不是减速比。4.2 用定时器生成脉冲别再用 delay 卡主循环最原始的电机例程是循环里置 IO、delay、置下一个 IO。这样写的问题很明显电机转的时候 CPU 干不了别的。传感器例程合并的时候这个矛盾会直接爆发。正确做法是把换相放进定时器中断主循环只负责设定目标位置和方向。下面是用 T0 做电机脉冲源的配置#define MOTOR_SPEED 400u // 每秒步数约 0.1 圈/秒 #define T0_RELOAD (65536u - FOSC / 12u / MOTOR_SPEED) volatile unsigned char motor_step 0; volatile bit motor_enable 0; void timer0_init(void) { AUXR 0xBF; // T0 工作在 12T 模式 TMOD 0xF0; TMOD | 0x01; // T0 16 位定时器 TH0 T0_RELOAD 8; TL0 T0_RELOAD; ET0 1; TR0 1; } void timer0_isr(void) interrupt 1 { TH0 T0_RELOAD 8; TL0 T0_RELOAD; if (motor_enable) { motor_step (motor_step 1) 0x07; P1 (P1 0xF0) | step_table[motor_step]; } }这段代码的计算逻辑FOSC 是 11.0592MHz定时器 12T 模式下计数频率是 FOSC/12也就是 921600Hz。要让中断每秒触发MOTOR_SPEED次重装值就是65536 - 921600 / MOTOR_SPEED。400 步/秒时约等于 2304重装值 63232。主循环里想停电机就把motor_enable清零想继续就走完全不需要关心电机正在哪一拍。步进电机启动时不要直接上高速。28BYJ-48 空载能到 800 步/秒左右但带负载或直接全速启动容易失步。最简单的做法是分段加速前 50 个脉冲用 200 步/秒中间 100 个脉冲用 400 步/秒后面再切到目标速度。反转之前务必先停够 10ms让转子稳定下来否则丢步的概率很高。例程里如果电机嘎嘎响但不转第一查相序第二降速度。注意28BYJ-48 堵转电流接近 200mA别把电机 VCC 接到开发板的 3.3V 引脚上。5V 供电要从 USB 或外部电源取否则板载 LDO 会过热。5. 例程合并用定时器时间片替代 delay串口输出调试15 个例程分开跑都正常一旦你想把温度、电机、声控放进一个 main 函数马上会发现DS18B20 转换要等 750ms这期间电机停转声控轮询放在温度等待后面拍手的瞬间主循环还没轮询到那个 IO。问题根源是例程里到处都是delay_ms()CPU 在延时期间是死的。合并例程的第一步是消灭阻塞式延时。5.1 为什么单独跑正常、合在一起就乱阻塞延时的代价DS18B20 的 750ms 转换等待是最典型的阻塞段。单独跑温度例程时delay_ms(800)没问题但你的系统里还有电机在转电机相序靠主循环里的 delay 控制温度等待一进来电机 800ms 没换相失步是必然的。我见过不少人把电机速度调得很慢来解决抖动其实是在给阻塞延时擦屁股。正确思路是电机由定时器中断驱动温度转换状态靠时间差查询主循环只做事件分发。5.2 1ms 时基与差量时间判断取代裸 delay用一个定时器产生 1ms 中断维护一个全局 tick主循环里所有需要延时的逻辑都改成查 tick 差值。下面是一个精简的合并骨架volatile unsigned int sys_tick 0; void timer1_init(void) { AUXR 0x7F; // T1 12T 模式 TMOD 0x0F; TMOD | 0x10; // T1 16 位定时器 TH1 (65536 - FOSC / 12 / 1000) 8; // 1ms 中断 TL1 (65536 - FOSC / 12 / 1000); ET1 1; TR1 1; } void timer1_isr(void) interrupt 3 { TH1 (65536 - FOSC / 12 / 1000) 8; TL1 (65536 - FOSC / 12 / 1000); sys_tick; } #define TEMP_INTERVAL 500u // 每 500ms 启动一次转换 static unsigned int temp_last 0; static bit temp_busy 0; void main(void) { uart_init(); timer0_init(); // 电机 T0 timer1_init(); // 系统时基 EA 1; while (1) { if (!temp_busy (sys_tick - temp_last TEMP_INTERVAL)) { temp_last sys_tick; ds18b20_start(); // 发 0xCC 0x44不等待 temp_busy 1; } if (temp_busy (sys_tick - temp_last 750)) { int t ds18b20_read(); // 读回温度 printf(temp%d.%d\r\n, t / 16, t % 16 * 100 / 16); temp_busy 0; } // 声控、光敏、红外轮询照常执行不再被温度等待阻塞 } }sys_tick是unsigned int减法的结果是模运算所以即使 tick 回绕sys_tick - temp_last依然正确。用这个技巧可以不在中断里清计数省去清零导致别的任务时间基准丢一段的麻烦。温度转换期间光敏 ADC 的读取、声控的触发判断都可以正常跑因为主循环只在特定时机查询temp_busy没有一处空等。5.3 串口打印每个传感器值比看现象可靠得多合并例程时最好先把各路传感器数值通过串口打出来确认单独模块都正常再去做联动逻辑。IAP15W4K61S4 的串口 1 可以用定时器 2 做波特率发生器11.0592MHz 下 9600 波特率的配置如下void uart_init(void) { SCON 0x50; // 串口模式 18 位 UART允许接收 T2L 0xE0; // 9600bps 11.0592MHz T2H 0xFE; AUXR | 0x10; // 启动 T2T2x12 保持 0 即 12T 模式 } char putchar(char c) // 重定向 printf { while (!TI); TI 0; SBUF c; return c; }T2L/T2H的值来自公式65536 - FOSC / 4 / 9600算出来是 0xFEE0。AUXR | 0x10把 T2 的运行位打开同时保持 T2x12 为 0也就是 12T 模式。如果你把主频改了这个初值必须跟着重算否则串口打出来全是乱码。串口接上后DS18B20 的温度值、光敏 ADC 的 10 位读数、声控模块的触发计数全部 printf 出来哪个传感器数据异常一眼就能定位。5.4 硬实时任务放中断软实时任务放查询合并例程的最后一步是分清任务的实时性。步进电机的换相属于硬实时中断晚来 1ms 就可能影响稳定性所以必须放定时器中断里。DS18B20 的 750ms 属于软实时早 100ms 晚 100ms 读结果差别不大用标志查询就行。声控和红外的事件检测轮询频率高于信号最短持续时间即可主循环里放一两个if判断就够。按这个原则分配任务15 个例程合在一起也只是时间片轮询的问题不会互相踩。6. 验证方法与三个必查点示波器、内部时钟、上电 IO 状态例程改完能不能交付不能只看 LED 亮没亮。以下三个检查点是我验证这类项目时固定会过的关。6.1 示波器看三个信号比看现象更靠谱第一处是 DS18B20 的 DQ 引脚。复位脉冲的低电平应该在 480~960us 之间用示波器光标量一下如果只有 300us说明delay_us偏短主频或定时器模式不对。第二处是步进电机某一相的波形四路波形应该有固定的相位差频率和设定的MOTOR_SPEED一致。第三处是串口 TX 引脚的波形量一个字节的位宽10 位起始位 8 数据位 停止位的时长应该接近 1.04ms9600 波特率。没有示波器就用逻辑分析仪几十块钱的也能看清时序。6.2 内部时钟误差所有怪现象的第一嫌疑人IAP15W4K61S4 的内部 RC 振荡器精度大约在 ±1% 左右这个误差对 LED 闪烁没影响但累积到 DS18B20 的时序上就可能出问题。如果串口乱码和 DS18B20 读 85 同时出现先查 STC-ISP 里选的主频和代码里FOSC宏是否一致。更隐蔽的情况是主频选对了但例程的延时函数是按 12T 的 8051 写的IAP15W4K61S4 是 1T 内核同样的for循环延时快了近 8 倍。判断方法很简单写一个翻转 LED 的循环用逻辑分析仪量翻转周期算出来的主频和预期差一个数量级那就是 1T/12T 混了。6.3 上电瞬间 IO 电平外部电路比代码先动IAP15W4K61S4 复位期间 IO 默认是准双向口高电平。如果例程里的声控或红外模块是高电平触发上电瞬间可能误触发一次动作如果接了有源蜂鸣器甚至会响一下。处理办法是在 main 函数最开头、任何外设初始化之前先把相关 IO 写成安全电平。对步进电机初始化函数第一步就输出step_table[0]让四相处于确定状态避免复位期间相序随机抖动。对于继电器初始化先写 0 再延时 50ms 再进入主循环。最后留一个实用技巧排查时序问题时把delay_us里的参数换成一个固定的 NOP 计数用逻辑分析仪量实际宽度反推这颗芯片在当前配置下的真实指令周期之后所有延时计算都按这个实测值走比翻手册猜参数快得多。本文还有配套的精品资源点击获取
