资讯动态

IAP15W4K61S4外设例程剖析:DS18B20、DHT11与步进电机

发布时间:2026/9/16 18:52:42 来源:尧图企业网站定制
简介面向IAP15W4K61S4单片机开发者和学生的传感器实验例程源码包涵盖15个典型实验覆盖声控、红外、DS18B20温度、DHT11温湿度、光敏、触摸、矩阵按键、继电器、四相步进电机、MQ-2等常见外设。资源共243个文件核心代码以C语言源文件(.c/.h)和Keil工程文件(.uvproj)为主附有编译生成的hex固件、调试中间文件(lst/obj/m51)及备份记录压缩包仅671KB便于直接导入工程学习或烧录验证。已有1182人学习下载。每个实验均提供完整KEIL工程包含初始化、驱动和主逻辑代码部分通信类实验如DS18B20、DHT11带有串口调试助手显示示例步进电机实验同时给出定时器驱动与延时函数驱动两种实现可对照学习编程思路快速迁移到实际项目中。1. IAP15W4K61S4 例程包的组成与 KEIL 工程冷启动这个压缩包更像是一套围绕 IAP15W4K61S4 单片机整理的外设验证手册15 个 KEIL 工程覆盖了 4 位数码管、LED、光敏传感器、触摸传感器、4x4 矩阵按键、DS18B20 温度、DHT11 温湿度以及四相步进电机的定时器和延时两种驱动。目录里能看到 MotorDrive、TouchSensor、KeyBroad_4x4、DS18B20 这些独立工程也有 STARTUP.A51 和多个 uvgui 配置残留文件。对正在做单片机课程设计、蓝桥杯单片机备赛或者从 51 单片机转向 STC15 系列的读者来说这套例程源码最直接的价值是每个实验都能独立编译烧录不需要自己拼依赖关系。拆包后第一件事是核对型号。IAP15W4K61S4 属于 STC15 系列61KB 程序空间加 4KB SRAM板载时钟可省去外部晶振Keil 里选 C51 编译器配合 STC-ISP 串口下载即可。标题里提到的声控、红外传感器在这套包里多是以数字电平触发为主本文选择对时序要求最严格和最常拿来扩展实验的五个模块拆开分析下面从 DS18B20 开始。2. DS18B20 温度采集单总线时序与串口输出验证2.1 存在脉冲检测复位决定后续一切DS18B20 是单总线器件数据线只有一根所有通信都必须从主机发送复位脉冲开始数据线拉低 480~960us释放后等待 DS18B20 回送存在脉冲。这个 60~240us 的低电平信号就是器件对主机的应答。如果复位阶段就没等到后面发送的跳过 ROM、启动转换、读暂存器命令都会被器件直接忽略。我先读 DS18B20 工程里的 reset 函数因为它的延时参数直接决定了代码能不能在 IAP15W4K61S4 上跑起来。常见做法是用 650us 作为复位脉冲宽度比下限值 480us 留出余量对批次差异更大。同时需要外部上拉电阻否则释放总线后电平不确定存在脉冲即使回来也会被上拉到高电平。#include STC15W4K.H #include intrins.h sbit DQ P1^0; bit ds18b20_reset(void) { bit presence; DQ 1; _nop_(); DQ 0; // 拉低总线开始复位 delay_us(650); // 复位脉冲 480~960us650us 留一点余量 DQ 1; // 释放总线等待器件应答 delay_us(30); // 存在脉冲一般在释放后 15us 出现 presence ~DQ; // 这时读到低电平表示 DS18B20 在线 delay_us(60); // 等存在脉冲结束再进入读写时隙 return presence; // 1 表示检测到器件0 表示超时 }这里的delay_us是 STC-ISP 工具按主频生成的延时函数比手工写空循环要可靠。需要注意的是如果你实际跑的是内部 11.0592MHz 时钟却用 12MHz 生成的延时函数整个时序会整体偏移现象就是“偶尔能读到温度偶尔超时”先用逻辑分析仪看波形比改代码更快。2.2 读写时隙“0”和“1”靠采样窗口区分单总线读写都以时隙为单位一个时隙至少 60us。写“0”时主机把总线保持低电平 60us 以上写“1”时主机拉低 1~15us 后立刻释放靠上拉电阻把总线拉回高电平。读时隙则不同主机拉低 1us 左右就释放数据由 DS18B20 决定它要发“0”就继续拉低总线要发“1”就保持高电平不动。难点在采样时间。DS18B20 只在 15us 的有效窗口内驱动总线采样晚了会读到下一个时隙的电平。我在移植时习惯在释放总线后延时 2~3us 就读比教科书上常见的 15us 采样更稳这也是例程里能保持一整天读数不跳的关键。unsigned char ds18b20_read_bit(void) { unsigned char dat; DQ 0; _nop_(); // 拉低约 1us DQ 1; // 释放总线 _nop_(); _nop_(); // 等 2~3us 再采样 dat DQ; delay_us(60); // 凑满一个时隙 return dat; }这段代码能不能正常工作的前提有两个一是 Keil 编译优化不能改变_nop_()的相对位置二是单片机的时钟模式。_nop_()在 12T 模式下才接近 1us如果把 IAP15W4K61S4 切换成 1T 模式这条指令只剩下 1/12 的时长采样时机全部提前读回来的数据会变成全 1 或全 0。例程默认跑 12T如果你在主程序里改了AUXR寄存器的 T1x12/T0x12 位DS18B20 这部分的时序基本就废了。写一个字节同样要按位操作逐位把数据送到 DQ 引脚void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DQ 0; _nop_(); if (dat 0x01) DQ 1; // 写1拉低后释放总线被上拉 _nop_(); delay_us(60); // 保持整个时隙长度 DQ 1; dat 1; } }写时序里也用到delay_us(60)这是为了保证总线低电平或高电平的持续时间足够让 DS18B20 识别。低位先发是 DS18B20 的固定要求dat 1之后每次把最低位送到总线最后总线恢复到高电平为下一个时隙做准备。2.3 温度转换读取与数据格式换算完成温度转换后DS18B20 会把 12 位结果放在暂存器的第 0 和第 1 字节。低字节的低 4 位是小数部分高字节的高 5 位是符号位正温度直接把原始值乘以 0.0625。常规读取流程是复位、跳过 ROM、启动转换、等待、再复位、跳过 ROM、读暂存器int ds18b20_get_temp(void) { unsigned char low, high; int raw; bit minus 0; if (!ds18b20_reset()) return 0xFFFF; // 没检测到器件 ds18b20_write_byte(0xCC); // 跳过 ROM单设备场景 ds18b20_write_byte(0x44); // 启动温度转换 while (!DQ); // 转换期间总线被拉低完成才释放 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器从地址 0 开始 low ds18b20_read_byte(); // 低字节bit3~bit0 是小数位 high ds18b20_read_byte(); // 高字节bit7 是符号位 raw (high 8) | low; if (raw 0x8000) { // 负温度补码转原码 raw ~raw 1; minus 1; } return minus ? -raw : raw; // 返回值再除以 16 就是实际温度 }while (!DQ)是等待转换完成。DS18B20 在温度转换过程中会把总线拉低转换结束后释放。这里需要主函数配置好外部上拉否则这个等待循环会一直卡住。读完两个字节后组合出的raw是带符号的补码负温度必须取反加一。DS18B20 部分原始值与温度对应关系原始值十进制十六进制温度℃00x00000.004010x019125.065000x01F431.257200x02D045.00小数位的换算规则是每个 LSB 对应 0.0625℃也就是 1/16℃所以raw / 16得到整数温度raw % 16再乘以 6.25 得到小数部分。例程用了更直观的方式直接乘 0.0625 存在浮点变量里。51 内核上浮点运算偏慢但在 1s 一次的采样周期里完全够用。2.4 串口输出路径与常见故障定位DS18B20 实验是配合串口调试助手显示温度IAP15W4K61S4 的 UART1 初始化后把printf重定向到串口即可。打印时把整数和小数拆开避免 Keil C51 的浮点格式化库把程序空间撑大printf(Temp:%d.%02d C\n, (int)(raw / 16), (int)((raw % 16) * 6.25));串口助手显示全 0xFF 时先查读时序的采样点再看单片机时钟模式是否被切换成 1T。第一次读到正常值、第二次开始超时返回 0xFFFF基本是复位之后没给总线留出释放时间紧接着就进入了读操作。在两次复位命令之间补 10us 左右的延时问题通常会消失。另外注意读取间隔不要太短DS18B20 转换 12 位数据需要约 750ms连续调用时间隔至少 1s。3. DHT11 温湿度读取与串口调试助手联调3.1 DHT11 与 DS18B20 的协议差异DHT11 也走单总线但和 DS18B20 的“主机指挥、从机执行”不同DHT11 在收到主机起始信号后会主动吐出 40 位数据。前 16 位是湿度整数和湿度小数中间 16 位是温度整数和温度小数最后 8 位是校验和。DS18B20 的温度分辨率是 12 位DHT11 的温度只有 8 位整数加 8 位小数精度低不少优点是直接拿到了温湿度两个值省去一路传感器。另一个容易忽略的点是工作模式。DS18B20 的每一次读写都要主机先发命令而 DHT11 是单次“触发-接收”流程整个读周期结束后总线归空闲。这意味着 DHT11 不适合频繁连续读取两次读取间隔至少 1s否则传感器内部还在睡眠状态起始信号会被忽略。3.2 起始信号与 40bit 数据读取DHT11 读取之前主机需要把总线拉低至少 18ms再释放并延时 20~40us。随后 DHT11 会把总线拉低 80us 响应再拉高 80us告诉主机数据要来了。每个数据位由 50us 低电平开始之后是 26~28us 的高电平表示“0”70us 的高电平表示“1”。采样点放在低电平结束后 40us 的位置因为 0 和 1 的高电平宽度差异足够大。sbit DHT P1^1; unsigned char dht11_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { while (!DHT); // 等 50us 低电平结束 delay_us(40); // 40us 处采样落在 0/1 判定区 if (DHT) dat | (0x80 i); // 高位在前逐位填充 while (DHT); // 等当前位高电平结束 } return dat; }高位在前这一点特别容易翻车。DHT11 的 bit7 最先出现在总线上如果顺手写成dat | (1 i)读出来的湿度值就会左右颠倒可能得到完全离谱的数据。0x80 i让第一个到达的 bit 直接进入结果的最高位后面依次右移最终形成正确的字节顺序。3.3 校验和判断与完整读帧40 位数据组装后一共 5 个字节。前 4 个字节是湿度和温度的整数、小数部分第 5 字节是校验和要求前 4 字节相加后取低 8 位与校验值相等。完整读取函数如下unsigned char dht11_data[5]; void dht11_read_frame(void) { unsigned char i, sum; DHT 1; DHT 0; delay_ms(20); // 起始信号主机拉低至少 18ms DHT 1; delay_us(30); // 释放总线 20~40us while (!DHT); // 等待 DHT11 响应低电平 while (DHT); // 等待响应高电平结束 for (i 0; i 5; i) dht11_data[i] dht11_read_byte(); sum dht11_data[0] dht11_data[1] dht11_data[2] dht11_data[3]; if ((sum 0xFF) ! dht11_data[4]) return; // 校验失败本次数据不更新 }while (!DHT)和while (DHT)的顺序一旦写反会直接跳过响应阶段读回来的一整帧都是垃圾。出现这种问题先用逻辑分析仪确认 80us 响应低电平是否存在再检查代码顺序。数据更新策略是校验失败就保留上一次结果显示端不会出现“湿度突然变成 0”的跳变这是工业上位机里常见的防抖手法。DHT11 关键时序参数与建议设置值参数最小典型建议设置主机拉低时间18ms20ms20ms释放后延时20us30us30us响应低电平80us80us等待电平跳变数据位采样点26us40us低电平结束后 40us3.4 双单总线传感器共存与调试顺序例程包里 DS18B20 和 DHT11 是两个独立工程想合到同一个程序里建议一个接 P1.0、一个接 P1.1分别封装成模块并且不要在定时器中断里去读传感器。我第一次把两者合在一起时DHT11 的湿度偶尔跳变最后定位到是中段里有串口中断抢占把 DHT11 的 50us 低电平读成了别的值。解决方法是读取过程中关闭 EA 总中断读完整帧再打开。串口调试助手这边有个操作顺序先把波特率固定 9600单独跑 DHT11看输出帧是否稳定再打开 DS18B20 模块。两个单总线传感器同时初始化时先让 DHT11 完成一次读帧再初始化 DS18B20可以避免上电瞬间两条总线同时被拉低造成的电平竞争。4. 四相步进电机驱动方式对比与光敏传感器 ADC 联动4.1 四相八拍控制字与正反转四相步进电机在例程里搭配 ULN2003 驱动板使用MotorDrive 工程给出定时器驱动和延时函数驱动两个版本。电机四相分别为 A、B、C、D采用八拍方式通电顺序是 A→AB→B→BC→C→CD→D→DA八个状态循环一次。相比四拍八拍是半步驱动转子转动更平滑启停时的抖动也更小。控制字就是送给 P2.0~P2.3 的二进制组合unsigned char code motor_table[8] { 0x01, 0x03, 0x02, 0x06, 0x04, 0x0C, 0x08, 0x09 };0x01 只有 A 相通电0x03 是 A、B 两相同时通电0x06 对应 B、C 相通电。ULN2003 的 IN1~IN4 接在 P2.0 到 P2.3 上每切换一次控制字电机走出半步。正反转的实现很简单把查表方向反过来从数组末尾向前扫描电机就反转了。4.2 延时函数驱动主循环被占用的代价延时驱动版本在主循环里反复查表然后调用延时函数控制换相间隔。这种写法直接有效但付出的代价是这段延时期间 CPU 完全被占用无法扫描矩阵键盘也没法刷新数码管。如果工程里还有串口中断中断处理时间也会被算进延时里换相间隔被拉长转速变慢且不均匀。我用这类例程做电机控制时一般会先把最小换相时间单独测出来。从 2ms 开始递减直到电机出现抖动或丢步再取抖动前那个值作为该负载下的极限。对常见的 28BYJ-485V 供电下大约能到 1.8ms 左右。低于这个值电机反转或停转不是什么程序 bug而是电磁转矩不够。4.3 定时器中断驱动硬件节拍如何解耦定时器驱动的优势在中断里换相主循环可以同时处理按键、显示和传感器读取。下面的代码用 16 位定时器产生 2.5ms 节拍每进一次中断换一相unsigned char motor_step 0; void timer0_isr(void) interrupt 1 { motor_step (motor_step 1) 0x07; // 八拍循环索引 P2 (P2 0xF0) | motor_table[motor_step]; } void timer0_init(void) { TMOD 0xF0; TMOD | 0x01; // 定时器016位模式 TH0 (65536 - 2500) / 256; // 12MHz 12T 下约 2.5ms TL0 (65536 - 2500) % 256; ET0 1; TR0 1; EA 1; }注意 2500 这个数不是随便填的。12MHz 主频、定时器工作在 12T 模式时一个计数周期恰好 1us2500 个计数就是 2.5ms。如果换到 11.0592MHz计数值要改成 2304如果切到 1T 模式要再乘 12。例程注释里标了“按实际主频修改”这种改动一定要同步检查中断服务函数里的耗时保证中断处理时间远小于定时周期。定时器驱动与延时驱动的差异对比项延时函数驱动定时器中断驱动CPU 占用100%阻塞主循环中断执行主循环继续换相误差受串口等中断干扰硬件定时器周期稳定多外设协同不适合可并行扫描按键和刷新显示代码复杂度低中需注意中断优先级中断服务函数里不要放 printf、不要读 ADC、不要做除法只做控制字切换和索引更新。否则中断处理时间一长节拍就不准了。实测中把步进电机控制放在中断里后主循环跑 4 位数码管扫描没有任何卡顿这就是定时器驱动带来的最明显收益。4.4 光敏传感器 10 位 ADC 采集与阈值标定光敏传感器模块通常有数字和模拟两路输出。数字输出端接在比较器后用可调电位器设置阈值模拟输出直接连光敏电阻分压更适合用单片机 ADC 读取。IAP15W4K61S4 的 ADC 是 10 位逐次逼近型对应数值范围 0~1023。把模拟输出接到 P1.2初始化时先配置 P1ASFvoid adc_init(void) { P1ASF 0x04; // 选择 P1.2 为模拟输入通道 ADC_CONTR 0x80 | 0x02; // 打开 ADC 电源选择通道2 delay_ms(2); // 等待 ADC 电源稳定 } unsigned int adc_get_value(void) { unsigned int val; ADC_CONTR | 0x08; // 启动一次转换 while (!(ADC_CONTR 0x10)); // 等待 ADC_FLAG 置位 ADC_CONTR ~0x10; // 软件清除标志位 val ((unsigned int)ADC_RES 2) | (ADC_LOW2 0x03); return val; }最后一行的拼接要特别留意ADC_LOW2里只有低 2 位是有效数据如果直接| ADC_LOW2会把预留的无效位也拼进结果造成读数整体偏大。光敏模块和步进电机的联动逻辑很直观光线变暗时光敏电阻阻值增大ADC 读数升高超过阈值就启动电机或点亮指示灯。阈值怎么确定把 ADC 原始值用串口打印出来分别记录亮环境和暗环境的读数取两者中间值作为阈值。比如亮时读 200暗时读 850阈值可以设在 500 附近而不是直接抄例程的常数。电位器位置不同模块出来的基准电压也不同这个阈值必须在实际环境里重新标定。5. 触摸传感器与 4x4 矩阵按键扫描状态机的实现技巧5.1 触摸传感器边沿触发代替电平判断触摸传感器模块大多基于 TTP223 方案输出是简单的数字电平手指触碰时 GPIO 为高松开恢复低。硬件已经完成了触摸检测单片机如果再按“读电平直接执行动作”的写法会发现手放在上面的整个过程中动作一直重复执行。正确做法是做一次边沿触发判断用上一次状态和当前状态组合出上升沿bit last_touch 0; if (TOUCH_PIN !last_touch) { // 一次触碰只触发一次动作 execute_action(); } last_touch TOUCH_PIN;TTP223 模块在触摸瞬间输出有轻微抖动更保险的写法是检测到高电平后 delay 20ms 再读一次两次都是高才确认触发。不过加延时之前先想清楚一件事20ms 的确认延时不能放在中断里只能在主循环轮询时用否则中断服务函数里卡了 20ms定时器中断的换相节拍就乱了。5.2 4x4 矩阵按键的行列扫描实现4x4 矩阵键盘把 8 根引脚分成 4 行 4 列扫描时逐行拉低然后读列线。如果某列被拉低说明该行该列的交叉点有按键闭合。KeyBroad_4x4 工程里的核心函数可以做得很紧凑unsigned char key_scan(void) { unsigned char row, col; for (row 0; row 4; row) { P3 ~(0x01 row); // 把第 row 行拉低其余行输出高 delay_ms(1); // 等电平稳定 for (col 0; col 4; col) { if (!(P3 (0x10 col))) { // 第 col 列被拉低表示有键按下 delay_ms(10); // 消抖确认 if (!(P3 (0x10 col))) return row * 4 col; // 返回按键编号 0~15 } } } return 0xFF; // 无按键 }这里两次判断之间隔着 10ms 消抖机械按键的抖动时间一般在 5~10ms这个间隔能过滤掉大部分抖动。列线从 P3.4 开始所以用0x10 col去对应 P3.4 到 P3.7。如果硬件接线把行和列反接只需要在代码里交换行扫描和列读回的端口映射扫描逻辑不用变。5.3 合并统一按键事件表同时使用触摸传感器和矩阵键盘时我会把它们统一成一个输入抽象层。触摸传感器看成一个特殊按键矩阵键盘每个键给出编号放进一张“按下/抬起”状态表里主循环每 10ms 扫描一次。这样做的好处是后续想扩展 2x4 矩阵按键、换成旋转编码器只需要替换底层读取函数上层逻辑完全不动。最后留一个亲测有效的验证方法在 4x4 矩阵按键上长按某个键观察串口输出的键值是否重复出现。如果长按时键值像连发一样反复返回说明消抖窗口不够长把 10ms 改成 20ms 重试。触摸传感器和矩阵按键混用的工程里这个检查务必先做因为它能一次性暴露 GPIO 配置、消抖参数和状态机逻辑三类问题。串口输出格式建议统一成KEY:%02d按下一次输出一次松开时不输出这样从日志上扫一眼就能看出按键事件有没有被重复触发。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价