
简介针对STC单片机、ST7735 1.8寸TFT液晶屏与DS18B20数字温度传感器的组合应用面向嵌入式初学者及进阶开发者提供了一套完整的环境温度实时监测方案可应用于智能家居、温室监控、实验室设备等场景。压缩包共111个文件包含C语言源码、头文件、Keil工程文件、编译中间文件、hex烧录文件及mp4演示视频整体大小约38.96MB源码模块涵盖GUI显示、按键扫描、PCA、定时器、USART串口等常用功能便于对照学习工程配置与驱动实现。资源重点演示了STC单片机通过1-Wire协议读取DS18B20温度数据并驱动ST7735屏完成彩色文本与图形显示的全过程附带库函数与示例程序可辅助快速移植。已有631人学习使用适合希望掌握单总线通信、TFT液晶驱动及嵌入式显示系统集成的读者参考。1. 为什么这块1.8寸屏最常栽在DS18B20的时序上把DS18B20接上STC单片机再拿ST7735把温度画到屏幕上听起来是三段式拼积木一个传感器、一颗驱动IC、一块屏。但实际调试过这套组合的人都知道真正卡人的不是ST7735的初始化命令也不是DS18B20的ROM读取而是两个底层逻辑完全不同的外设挤在同一颗MCU上ST7735走的是SPI只要时钟沿对、命令字对就能出图DS18B20走的是1-Wire信号线上既传时钟又传数据而且STC单片机在1T模式下执行速度远快于传统8051延时函数稍微写错一个数量级读回来的温度就是固定的85℃假值——这是DS18B20上电默认状态也是最容易让人误判“传感器坏了”的坑。另一个高发问题则是ST7735初始化不完整屏幕上只有白色或花点根源多半是复位时序和MADCTL扫描方向没配好。这篇文章直接给出一套可复现的STC ST7735 DS18B20驱动方案覆盖1-Wire时序实现、ST7735寄存器配置和三层代码组织方式适合正在做温湿度显示、桌面气象站或小体积监控面板的开发者参考。2. STC单片机IO特性与DS18B20 1-Wire时序实现2.1 STC的准双向口先弄对否则1-Wire连读都读不了STC单片机默认IO模式是准双向口输出高电平时靠内部上拉驱动能力有限而DS18B20的数据线是开漏结构需要外部上拉电阻才能把总线拉高。这里有两层问题要同时解决第一STC的IO口如果不切换模式直接用准双向口去读DS18B20的低电平脉冲会导致电平判定错误第二1-Wire总线对时隙宽度极其敏感尤其是STC15和STC8系列运行在1T模式时主频等效于12倍的传统8051原来用在STC89C52上的延时函数不能照搬。我建议把DS18B20接到STC的P3.7或P1.0这类普通IO上并把该引脚配置为开漏模式外部接4.7kΩ上拉电阻到3.3V或5V。STC15系列的PnM0和PnM1寄存器控制每个引脚的工作模式配置方法如下// 将P1.0配置为开漏模式 P1M0 | 0x01; // P1.0的M0位为1 P1M1 | 0x01; // P1.0的M1位为1组合为开漏输出代码中P1M0和P1M1对应STC15系列的端口模式寄存器两个寄存器同位置1代表开漏模式此时引脚只能主动拉低释放后由外部上拉电阻拉高。准双向口模式下虽然也能读但高电平驱动能力过强与DS18B20的输出特性配合不好容易在读时序边缘处产生误判。另外要注意的是STC15和STC8系列支持内部4kΩ上拉但1-Wire规范建议总线上拉电阻在2.2kΩ到4.7kΩ之间内部上拉加上外部上拉并联后阻值偏低会拉高总线功耗也会让低电平脉冲的下降沿变脏。我一般只在外部上拉电阻忘记焊接时用内部上拉应急正常设计一律外部接。2.2 1-Wire初始化时序480μs的复位脉冲DS18B20的复位时序是通信开始的标志主机把总线拉低至少480μs然后释放并等待60μs到240μsDS18B20会在这段时间内拉低总线60μs至240μs作为应答脉冲。这个应答脉冲是否存在决定了后续是否可以执行ROM命令。STC单片机在1T模式下执行_nop_()只能延迟一个时钟周期因此延时函数必须用循环计数并校准。以下是基于STC15系列工作频率为12MHz的复位函数// DS18B20复位返回1表示检测到设备 unsigned char ds18b20_reset(void) { unsigned char presence 0; DS18B20_PIN 0; // 拉低总线 delay_us(500); // 至少480us复位脉冲 DS18B20_PIN 1; // 释放总线 delay_us(70); // 等待应答窗口 presence DS18B20_PIN; // 采样总线电平 delay_us(200); // 等待应答脉冲结束 return (presence 0) ? 1 : 0; }这段代码的关键在于采样点释放总线后70μs读取电平如果读到低电平说明DS18B20拉低了总线即应答成功。注意delay_us()函数的精度直接决定通信成败推荐用逻辑分析仪校准后面会专门说校准方法。读取变量presence前要确保引脚已配置为输入态由于之前配置的是开漏模式释放总线后引脚并不会主动输出高而是通过外部上拉恢复高电平这时读取到的低电平就是DS18B20的应答信号。2.3 写时隙和读时隙15μs以内的精确窗口DS18B20的写时序分为写0和写1两种写0时隙要求主机拉低总线并保持60μs至120μs写1时隙要求主机拉低总线后必须在15μs内释放由外部上拉把总线拉高。读时隙则要求主机拉低总线最少1μs后释放并在释放后15μs内采样总线状态。这里最容易出问题的是STC的IO操作速度太快写1时隙时如果不加延时引脚拉低后立即释放总线上看到的脉冲宽度可能只有几百纳秒DS18B20根本识别不了。以下是写入一个字节的实现void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS18B20_PIN 0; // 起始低脉冲 delay_us(2); if (dat 0x01) // 低位在前 { DS18B20_PIN 1; // 写1提前释放 } delay_us(60); // 维持时隙 DS18B20_PIN 1; // 恢复高电平 dat 1; } }代码先拉低总线2μs然后根据数据位决定是否释放。写1时数据线提前变高写0时数据线保持低电平直到时隙结束。延时60μs后统一拉高保证每个时隙的总长度在60μs到120μs之间。参数delay_us(2)是因为STC的GPIO 0到GPIO 1之间需要一点过渡时间如果完全不加延时引脚电平翻转太快示波器上能看到毛刺DS18B20会把这当作无效脉冲忽略掉。读时序的代码逻辑类似区别在于读操作需要主机主动释放总线后读取引脚状态unsigned char ds18b20_read_byte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; DS18B20_PIN 0; delay_us(1); // 低脉冲触发 DS18B20_PIN 1; // 释放总线 delay_us(8); if (DS18B20_PIN) // 采样数据 { dat | 0x80; } delay_us(60); // 保证时隙长度 } return dat; }这里采样时间设置在释放总线后8μs左右处于DS18B20规定的15μs采样窗口之内。延时delay_us(8)不能改成更大的值因为在30μs之后DS18B20会释放总线并恢复高电平采到的就永远是1了。STC单片机的引脚读取速度很快delay_us(8)的误差容忍度较大但低于5μs时可能赶上DS18B20内部的输出切换时间读取不稳定。2.4 延时函数校准是STC上最容易翻车的环节上面的代码里多次出现delay_us()这个函数在不同型号的STC单片机上有完全不同的行为。STC15W、STC8A系列默认是1T模式一条DJNZ指令的执行时间大约是主频周期的几个周期但编译器优化等级不同循环被展开或合并后实际延时会长短不一。我习惯用最笨也最可靠的方式直接在Keil里写一个简单的延时函数然后编译后用示波器或者逻辑分析仪测量引脚翻转频率根据实测值调整循环次数。// 12MHz主频下的粗略延时 void delay_us(unsigned int us) { unsigned int i; while (us--) { i 13; // 实测校准值 while (i--); } }i 13这个数值是我在STC15W408AS上实测出来的对应12MHz主频、Keil C51默认优化等级。如果你的板子用的是STC8H系列或者外部晶振频率不同这个值必须重新校准。校准方法是写一个空循环翻转IO口对比逻辑分析仪读出实际频率然后调整i的值减小误差。DS18B20对时序的容忍范围其实比网上说的宽松一些——写时隙允许60μs到120μs读采样窗口约15μs只要不偏得太离谱都能通信。但STC从模式切换、中断抢占这些因素会让时隙波动所以实际项目中我会在读取温度之前关全局中断确保1-Wire时序不被ST7735的刷新中断打断。3. ST7735初始化序列与SPI驱动寄存器配置3.1 硬件连接和SPI模式选择ST7735 1.8寸TFT屏的驱动可以走硬件SPI也可以用IO模拟SPI。STC15系列自带硬件SPI模块最高速率可以跑到主频的四分之一左右对于128x160分辨率的屏来说足足够用。硬件SPI的好处是传输由外设模块自动完成CPU可以抽空处理其他任务坏处是引脚复用固定有时需要跟其他外设抢IO口。考虑到这个项目的核心还是温度读取和显示数据量不大我倾向于用STC的硬件SPI因为可以配合中断逐帧刷新避免阻塞式地刷屏导致DS18B20的时序被打乱。以下是STC15系列SPI引脚的映射关系和初始化代码// STC15系列SPI引脚SCLKP1.7, MOSIP1.3, CSP1.2 // 附加控制引脚A0(RS)P1.1, RESETP1.0, 背光P1.4 void st7735_gpio_init(void) { P1M0 0x98; // P1.3/P1.4/P1.7推挽输出 P1M1 0x00; SCLK 1; CS 1; RES 0; delay_ms(50); // 硬件复位保持 RES 1; delay_ms(50); A0 1; }P1M0的值0x98对应二进制10011000即P1.3、P1.4、P1.7配置为推挽输出其中P1.3是MOSI、P1.7是SCLK、P1.4是背光。A0和CS引脚配置为准双向口即可。硬件复位序列是先拉低RES保持50ms再拉高等待50ms这个时间比数据手册上要求的10ms更充足主要是为了兼容ST7735S和ST7735R这些不同批次芯片对复位延时的差异。3.2 初始化命令序列ST7735S和ST7735R的差异ST7735的初始化命令序列在网上有很多版本但关键是两条一个是软件复位和退出睡眠模式的顺序另一个是MADCTL扫描方向设置。不同封装和批次的屏驱动IC可能是ST7735R或ST7735S两者在Gamma校正曲线的寄存器地址上有细微差别乱用会偏色。下面这段是经过验证的初始化序列重点在于每条命令后的注释void st7735_init(void) { write_command(0x01); // SWRESET 软件复位 delay_ms(120); // 等待复位完成 write_command(0x11); // SLPOUT 退出睡眠模式 delay_ms(120); // 等待稳定 write_command(0x3A); write_data(0x05); // 设置16位色深 RGB565 write_command(0x20); // INVOFF 关闭反显 write_command(0x36); write_data(0x00); // MADCTL: 从左到右、从上到下扫描 write_command(0x2A); // 设置列地址 write_data(0x00); write_data(0x00); write_data(0x00); write_data(0x7F); // 列范围0~127 write_command(0x2B); // 设置行地址 write_data(0x00); write_data(0x00); write_data(0x00); write_data(0x9F); // 行范围0~159 write_command(0x29); // DISPON 开启显示 }这段初始化里有几个细节常被忽略write_command(0x20)是关闭反显如果把屏幕上的红色显示成蓝色多半是这里没设置或者设反了MADCTL的0x00对应横屏扫描方向如果你的屏幕物理方向是竖屏或者插上排线后画面旋转了90度改这个值可以直接翻转图像不需要改代码里的坐标换算逻辑delay_ms(120)看起来保守但ST7735从上电到真正可以接受显示数据至少要等120ms的时间。3.3 像素写入与矩形填充的寄存器组合ST7735没有帧缓冲写入方式类似串行点阵屏先设置列地址和行地址范围然后连续写入RGB565格式的像素数据。一个关键细节是ST7735在写完地址范围后如果连续写入的数据超过了设定范围会自动把地址回绕到起始位置所以即使写入代码有误差也不会立即花屏但一旦切换显示方向这个问题就会被放大。写像素函数本质上就是一次SPI传输但要注意write_command和write_data的区别。A0引脚的断言时序在ST7735上至关重要void write_command(unsigned char cmd) { A0 0; // 命令模式 CS 0; spi_write_byte(cmd); CS 1; A0 1; // 恢复数据模式 } void write_data(unsigned char dat) { A0 1; // 数据模式 CS 0; spi_write_byte(dat); CS 1; }A0在命令模式下必须为0数据模式下必须为1这个引脚和SPI时钟线不同它决定ST7735芯片将当前字节解释为命令还是参数。很多初始化失败的问题出在A0状态保持不当——比如在一次SPI传输中命令和参数交替发送A0没有在中间切换。矩形填充函数可以优化显示速度。填充一块区域时只需要设置一次地址窗口然后连续写数据void st7735_fill_rect(unsigned char x0, unsigned char y0, unsigned char x1, unsigned char y1, unsigned int color) { unsigned int i, total; write_command(0x2A); write_data(0x00); write_data(x0); write_data(0x00); write_data(x1); write_command(0x2B); write_data(0x00); write_data(y0); write_data(0x00); write_data(y1); write_command(0x2C); // RAMWR 开始写入显存 total (x1 - x0 1) * (y1 - y0 1); A0 1; CS 0; for (i 0; i total; i) { spi_write_byte(color 8); // 先写高字节 spi_write_byte(color 0xFF); // 再写低字节 } CS 1; }ST7735在RGB565模式下约定先发送高8位后发送低8位这个顺序不能调换否则显示的色块会整体偏色。spi_write_byte如果用的是STC硬件SPI发送完成后要查询SPIF标志位确认上一字节发送完成再写入下一字节否则连续传输时吞吐率会下降并且可能出现错位。用DMA的话另说但STC15系列的DMA资源有限这类小屏项目我通常直接采用阻塞式写显存刷一帧128x160分辨率的纯色屏大约在40ms左右温度显示这种低频刷新场景完全不影响体验。3.4 显示坐标与屏幕方向换算ST7735的物理分辨率是128x160但很多1.8寸模组默认的像素矩阵方向是竖屏插上排线后屏幕显示方向可能左转或右转90度。MADCTL寄存器的低三位控制扫描方向MY行方向翻转、MX列方向翻转、MV行列交换组合情况如下MADCTL值扫描方向适用场景0x00左上到右下出厂默认竖屏0xA0行方向翻转左右镜像0x60列方向翻转上下镜像0xC0行列交换横屏显示0x08附带RGB/BGR交换红蓝配色反了时排查如果画面出现镜像但颜色正常调MADCTL的MX或MY位即可解决如果同一个颜色代码显示出来的颜色跟预期不同比如红色显示成蓝色需要检查0x3A设置的色深是否生效以及是否有额外命令设置RGB滤波顺序。ST7735S和ST7735R在部分版本上颜色格式的设置命令地址不同初始化失败时先在0x36寄存器后面追发一条0x08看看颜色是否归位。4. 温度显示刷新框架与模块间时序协商4.1 数据流设计把1-Wire和SPI做解耦把三个子系统拼在一起的时候最容易出现的问题不是单个模块跑不通而是DS18B20读取和ST7735刷屏互相抢时间。ST7735刷整屏如果走阻塞传输一次刷新要占掉40ms而DS18B20的转换时间是750ms如果在这期间读时序被SPI传输拉扯读回来的数据就会变成0xFF或0x85。我的做法是把读取和刷新拆成独立的时序窗口整个主循环按100ms的节拍运行在读取温度的时间窗口内关闭所有中断在显示刷新的窗口内不碰1-Wire引脚。这个策略的核心代码如下void main(void) { unsigned int raw_temp; unsigned char temp_high, temp_low, err; unsigned char temp_str[16]; sys_init(); // 时钟、GPIO、SPI、串口初始化 st7735_init(); temp_high 0; while (1) { // 时间窗口1: 读取温度关闭中断保护时序 EA 0; // 关闭全局中断 ds18b20_start_convert(); // 启动转换 delay_ms(750); // 等转换完成 err ds18b20_read_temp(temp_high, temp_low); EA 1; // 恢复中断 // 时间窗口2: 更新显示 if (err 0) { // 将原始值换算为带一位小数的温度字符串 raw_temp (temp_high 8) | temp_low; format_temperature(raw_temp, temp_str); st7735_draw_string(10, 30, temp_str); } else { st7735_draw_string(10, 30, ERR); } delay_ms(200); // 显示保持一段时间 } }这里有两个细节值得解释EA 0关闭的是总中断开关在STC上它不会影响定时器计数但会屏蔽所有中断请求包括定时器中断和串口中断。DS18B20的转换时间实际在750ms以内如果系统里还有其他任务需要实时响应可以改为启动转换后先刷屏刷完再读温度而不是死等750ms。ds18b20_read_temp内部包含了跳过ROM指令和读取暂存器的完整时序返回0表示CRC校验通过。4.2 温度值换算12位分辨率的符号位处理DS18B20的默认分辨率是12位对应0.0625℃的LSB。读回来的16位数据中低4位是小数点部分高5位是符号位。温度值的计算公式为温度值 (原始值 补码) × 0.0625负数温度需要用补码转原码STC单片机没有硬件除法指令直接浮点运算是可以的但由于C51的浮点库很占资源通常用整型运算代替。以下是我常用的换算方式void format_temperature(unsigned int raw, unsigned char *buf) { signed int temp; unsigned char sign 0; // 处理负温度 if (raw 0x8000) { raw ~raw 1; sign 1; } temp (signed int)raw * 100; // 扩大100倍避免浮点 temp temp / 16; // 除以16等价于乘以0.0625 if (sign) { buf[0] -; buf[1] 0 (temp / 1000) % 10; } else { buf[0] 0 (temp / 1000) % 10; } buf[1] .; buf[2] 0 (temp / 100) % 10; buf[3] 0 (temp / 10) % 10; }raw ~raw 1是标准的取补码操作把负温度转换成正数后再统一计算。扩大100倍并除以16是为了把0.0625的小数乘法转化为整数运算。如果你只显示整数温度可以直接把除以16的结果丢给整数部分但显示小数后用户能直观看到温度波动尤其是测试DS18B20放在手指上时能观察到0.1℃级别的变化这个细节对验证传感器工作状态很有帮助。4.3 画面布局与局部刷新策略ST7735没有硬件字符发生器显示文本需要自己维护字库。温度显示场景只需要数字、小数点、负号和几个字母字模数据量很小。推荐用8x16点阵的数字字模显示效果清晰且占用Flash少。局部刷新的意义在于如果每次读数都整屏重绘背光下能看到明显的闪烁和刷新痕迹。做法是只更新温度数值所在的区域// 先清除温度数值区域再绘制新值 st7735_fill_rect(15, 30, 50, 46, WHITE); st7735_draw_string(15, 30, temp_str);这里的坐标需要跟4.1节中的st7735_draw_string调用位置一致否则会出现在旧数据旁边叠新数据的视觉残留。填充白色的操作相当于橡皮擦然后再把新的温度字符串画上去。这种方式下屏幕刷新率不需要很高200ms的刷新间隔下肉眼看不到闪烁。ST7735在写RAMWR命令之后如果还没有写完设定的像素数量芯片会一直等待数据。如果中途切换到其他SPI设备比如读取外部Flash或者SD卡会造成屏幕内容错乱。因此在SPI总线上如果并行接了多个从设备每次切换前要确保当前事务完整结束。5. 验证手段与排错清单从花屏到85℃假读数的排查路径5.1 用串口输出原始数据替代肉眼判断嵌入式里最有效的验证手段不是盯屏幕而是把原始数据通过串口发出来。STC15系列自带UART模块把DS18B20读取到的原始16位值和换算后的温度值同时打印到串口用电脑终端观察。这样能区分问题出在传感器通信还是显示模块。串口初始化代码不做过多解释但需要保证波特率误差小于2%。在代码里加一个测试分支每次读完后发送原始帧unsigned char uart_buf[8]; uart_buf[0] temp_high; uart_buf[1] temp_low; uart_send_bytes(uart_buf, 2);用STC官方ISP下载工具烧录程序时同样可以利用ISP串口作为调试口。如果串口输出0x05 0x01这类数据说明DS18B20通信正常0x0501对应的温度是80.0625℃这个值明显偏高但并非不可能——传感器如果紧贴PCB上的大电流走线读数确实会高于环境温度这是硬件布局问题而非代码逻辑问题。5.2 常见现象排查表下面这张表总结了这套项目里最常遇到的故障现象和对应排查方向排错时按行从上往下检查现象可能原因排查步骤屏幕全白/全黑初始化序列未执行完或RESET时序太短确认delay_ms(120)是否真实延时逻辑分析仪抓RES引脚显示颜色反了MADCTL的RGB/BGR位设置错误将0x36寄存器值异或0x08测试温度固定在85℃DS18B20转换失败读到上电默认值检查1-Wire延时函数换算代码是否执行了启动转换命令温度跳动剧烈采样时间过早读到转换中的中间值确认启动转换后等待是否超过750ms读到的温度全是0xFFIO模式或上拉电阻问题用万用表测DS18B20数据线空闲电平是否接近VCC画面上下颠倒MADCTL扫描方向没调修改0x36寄存器值重新初始化字体重影局部刷新时矩形坐标和文字坐标不一致统一坐标基准打印坐标值比对这里特别解释一下85℃这个现象DS18B20在出厂或复位后暂存器默认填充0x0550对应85℃。如果通信失败读到的数据可能是这个初始值也可能读到0xFFFF或0x0000但所有引脚配置正常且上拉电阻正确的情况下80%的失败会表现为读取到0x0550。所以程序里可以把0x0550和0xFFFF当作异常值直接丢弃再配合CRC校验增强可靠性。5.3 用逻辑分析仪校准1-Wire时隙逻辑分析仪是排查这套硬件最有力的工具。把DS18B20数据线接到逻辑分析仪的通道上设置采样率为10MHz以上然后观察复位脉冲和单个读时隙的实际宽度。我比较推荐的校准步骤是这样的先不连接ST7735单独运行DS18B20的测试代码用逻辑分析仪抓一段复位ROM命令读暂存器的完整波形。重点检查三个指标复位低电平脉冲宽度是否在480μs到960μs之间写1时隙中高电平出现的时间是否在15μs内读时隙中采样点是否落在15μs窗口内。如果波形里出现读时隙脉冲只有几百纳秒宽说明释放总线后的延时被编译器优化掉了需要把延时循环里的变量加上volatile修饰或者把延时函数改为汇编NOP指令填充。汇编级延时的写法如下// 精确延时假设主频为12MHz void delay_us_asm(unsigned char us) { unsigned char i; i us; while (i--) { _nop_();_nop_();_nop_();_nop_();_nop_(); _nop_();_nop_();_nop_();_nop_();_nop_(); } }当改用汇编级延时后1-Wire误码率会明显下降。这种方法基于Keil C51编译器对_nop_()的展开行为不同优化等级下循环体可能被改动因此编译时把优化等级设为固定值比如Level 0或Level 4保持行为一致。5.4 ST7735刷屏时的干扰处理ST7735刷新时SPI时钟线的高频翻转会产生一定的电源噪声如果DS18B20的供电和背光供电共用一个LDO在刷屏瞬间读取温度偶发读数偏差是正常现象。解决方法是把屏幕背光改为PWM控制刷新期间降低背光亮度或者在读取DS18B20数据时停止SPI传输并等待几个毫秒让电源稳定后再读信号。另一种做法是将温度读取频率降低到每2秒一次而显示刷新频率维持在高位这样SPI的噪声窗口和1-Wire的采样窗口大概率不会重叠。整个系统对实时性要求不高2秒的刷新周期在温室监控和桌面设备上完全够用。这个策略实际上就是在调度层面把SPI和1-Wire放在了不同的时隙里执行STC单片机没有RTOS也没关系主循环配合标志位即可实现。本文还有配套的精品资源点击获取