ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AT89C51串口显示光照值:无ADC老芯片的模拟量采集与通信实现

AT89C51串口显示光照值:无ADC老芯片的模拟量采集与通信实现 简介面向单片机与嵌入式初学者的AT89C51实战资源解决通过BH1750数字光照传感器采集环境光强并经串口在终端显示的问题。项目涉及I2C协议时序、UART串口配置、中断处理及寄存器操作等关键知识点适合课程设计、毕设或入门智能环境监测项目参考。压缩包共42个文件约80KB包含C源码、头文件、Keil工程文件、编译生成的Hex烧录文件、列表与日志文件等。其中针对同一功能提供“网上版”和“自己编写版”两套工程便于对照学习两种代码组织方式理解I2C读写BH1750、串口发送数据和主循环架构的差异。资源已有1545人学习对想要快速上手单片机串口通信与传感器数据采集的读者有直接借鉴价值。1. AT89C51串口显示光照值没有ADC的老芯片怎么做模拟量采集用AT89C51做光照值串口显示对刚接触单片机的人来说第一反应是“串口好办光照值怎么办”。AT89C51作为8051内核的经典芯片片内没有ADC模块光敏电阻输出的模拟电压它读不了而串口通信本身又依赖定时器产生精确波特率。这两个问题叠在一起恰好构成了这个选题最完整的练习价值外接ADC采集模拟量、用定时器配置UART、再组织数据通过串口调试助手显示。整个链路做完等于把51单片机最常用的几个外设全部过了一遍。下面按采集端、串口端、PC端接收、再到帧格式升级的顺序把这套方案完整写一遍。2. 光照采集端设计光敏电阻分压与ADC0831读取时序2.1 为什么选光敏电阻加ADC0831而不是直接上数字传感器光照采集的常见方案有两种用BH1750这类I2C接口的数字光照传感器或者用光敏电阻配合ADC芯片做模拟采集。BH1750精度高、直接输出lux数值但51单片机没有硬件I2C外设需要软件模拟I2C时序代码量和调试成本都不小。光敏电阻加ADC的方案虽然精度一般但胜在原理直观光敏电阻阻值随光照变化通过分压电路转换成电压变化再用ADC读出数字量。ADC芯片选型上ADC0831是比较合适的选择。它是8位串行ADC只需要CS、CLK、DO三根线就能和单片机通信比ADC0809那种并行接口省太多IO口。8位分辨率对应256个等级做光照值的百分比显示或者粗略lux估算足够用。另一个常见选择是PT100之类传感器专用的ADC但那些面向工业测温场景对纯光照采集来说有点杀鸡用牛刀。2.2 硬件连接与分压电路参数光敏电阻选GL5528亮电阻10到20K暗电阻1M以上。分压电路这样接5V电源串联光敏电阻再到固定电阻10K最后接地中间抽头接ADC0831的VIN输入。光照增强时光敏电阻阻值下降抽头电压上升ADC读数随之增大方向直观。固定电阻用10K是因为GL5528在常见室内光照下阻值处于10K到100K区间10K分压能让电压变化范围尽量铺满ADC的量程。表格ADC0831与AT89C51引脚连接ADC0831引脚连接目标说明CSP3.5片选低电平有效CLKP3.6时钟输入由单片机产生DOP3.7数据输出转换结果串行读出VIN分压节点光敏电阻与10K电阻的抽头VIN-GND参考地VREF5V参考电压决定量程VREF接5V时分辨率是5V除以256约19.5mV每LSB。如果对精度有更高要求可以把VREF接到2.5V基准源上分辨率提升到约9.8mV每LSB代价是测量范围被压缩到2.5V以内。这里需要注意VREF电压不能超过芯片供电电压否则转换结果会异常。2.3 ADC0831驱动代码与时钟时序ADC0831的读取时序不复杂核心是拉低CS启动转换然后连续给8个时钟脉冲在每个时钟周期里把DO引脚的数据位读出来。下面这段代码是完整的读取函数在Keil C51环境下编译通过。sbit ADC_CS P3^5; sbit ADC_CLK P3^6; sbit ADC_DO P3^7; unsigned char ADC0831_Read(void) { unsigned char i, dat 0; ADC_CS 0; // 片选拉低启动一次转换 for (i 0; i 8; i) { ADC_CLK 1; // 时钟上升沿ADC内部移位输出 ADC_CLK 0; // 时钟下降沿数据稳定 dat 1; // 左移一位准备接收下一位 if (ADC_DO) // 读取当前DO引脚电平 dat | 0x01; } ADC_CS 1; // 片选拉高结束本次转换 return dat; }这段代码的逻辑不算难懂每次循环先拉高CLK再拉低模拟出一个完整的时钟脉冲ADC0831会在时钟边沿把每一位数据依次送到DO引脚上。dat每次左移一位把新读到的位放到最低位循环8次后dat里就是完整的8位转换结果。注意读取顺序第一次循环读出的是最高位D7最后一次读出的是最低位D0。实际调试时最容易遇到的问题是无条件读到0xFF或0x00。读到0xFF大概率是DO引脚配置成了推挽输出而不是输入或者DO引脚虚接被上拉电阻拉高读到0x00先检查VIN引脚是否有实际电压变化拿万用表量抽头对地电压如果恒定0V多半是光敏电阻和10K电阻之间断路或者分压节点搭错位置。2.4 光强读数需要标定不能拿ADC值当lux直接输出ADC0831返回的是0到255的数字量对应VIN端口的电压不是光照度的绝对数值。直接把ADC值当光照度显示给用户在很多场景下也能用——比如判断“天亮了”“天黑了”——但如果要输出实际的lux值必须做标定。标定的做法是找两个已知光照点。比如拿手机上的照度App或者照度计在室内正常灯光下记录实际lux值和ADC读数再在暗处记录一组得到两点后用线性插值算出系数。公式是 lux (adc_value - adc_dark) * (lux_bright - lux_dark) / (adc_bright - adc_dark) lux_dark。标定后的精度受限于光敏电阻的响应曲线在强光段误差会偏大但应付课程设计级别的“显示光照值”要求已经足够。低成本的替代方案是弄几个固定衰减的遮光罩把量程分段标定效果比单纯线性拟合好一些。3. 用定时器T1产生9600波特率AT89C51串口初始化与发送实现3.1 串口配置的核心SCON寄存器、TMOD和波特率公式AT89C51自带硬件UART这一点比某些需要完全模拟串口的方案省心很多。串口要跑起来需要配置三个部分SCON寄存器选择串口工作模式TMOD寄存器把定时器T1设置为波特率发生器最后计算TH1和TL1的初值。SCON设置为模式1也就是8位UART、波特率可变的通信模式对应的SCON值为0x50。这个值的含义是SM0和SM1这两位为01REN位置1使能接收。如果只需要发送REN可以不置位但保留接收功能方便后续扩展。TMOD要把T1配置成模式2也就是8位自动重装模式高8位和低8位由TH1和TL1决定溢出后自动从TH1重新加载初值省去中断里手动重装的麻烦。3.2 11.0592MHz晶振下9600波特率的TH1初值计算波特率计算是串口通信最容易翻车的地方。串口模式1的波特率公式波特率 (2^SMOD / 32) × fosc / (12 × (256 - TH1))SMOD是PCON寄存器的第7位默认0。用11.0592MHz晶振跑9600波特率把公式代入11059200 / (12 × 32 × (256 - TH1)) 9600算出来256 - TH1 3TH1 0xFD。所以典型的初始化代码长这样void UART_Init(void) { TMOD 0x0F; // 清零T1的模式位不影响T0的配置 TMOD | 0x20; // T1工作在模式28位自动重装 TH1 0xFD; // 9600波特率初值高字节 TL1 0xFD; // 低字节首次装载用 SCON 0x50; // 串口模式18位UART使能接收 TR1 1; // 启动定时器T1 }这里有个关键点容易被忽略TMOD 0x0F这一行。TMOD寄存器的高4位控制T1低4位控制T0如果项目里T0也在用直接赋值TMOD 0x20会把T0的配置清掉。先清零再置位只动T1相关的位这是多人协作或复用代码时的好习惯。TH1 0xFD这个值只在晶振精确等于11.0592MHz时才成立换成12MHz晶振就不是这个数。12MHz晶振下与9600波特率的误差约为2.1%短时间收发不会出错但连续收发大量数据帧时累积误差就会暴露成乱码这也是为什么串口通信场景下11.0592MHz晶振是绝对主流。3.3 发送单个字节与发送字符串的代码实现串口发送的逻辑很简单向SBUF写入一个字节就会自动启动发送TI位在发送完成后由硬件置1查询TI的状态就可以判断是否发送完毕。void UART_SendByte(unsigned char dat) { SBUF dat; // 写入SBUF启动串口发送 while (!TI); // 等待发送完成TI硬件置1 TI 0; // 软件清零TI准备下一次发送 } void UART_SendString(unsigned char *str) { while (*str) { UART_SendByte(*str); // 逐个发送字符遇\0结束 } }需要注意的是TI清零的时机。TI置1后必须软件清零否则下一次查询while (!TI)时直接跳过字节还没发完就继续执行下去了。发送字符串函数的循环条件判断的是ASCII码的\0所以传入的字符串末尾必须带结束符用数组定义字符串时编译器会自动加上但如果手动拼接数据比如先放一个光照值再放回车换行就要自己把\0补上。3.4 主循环里组装光照值字符串避开sprintf这个Flash吞噬者主程序的逻辑是循环读ADC、拼接字符串、串口发送。很多初学者第一反应是用sprintf来做格式化输出这在AT89C51上行不通。AT89C51只有4KB Flashsprintf这类格式化函数会把printf全家的代码都拉进来编译出来动不动占掉1.5KB以上留给主程序的代码空间就非常紧张了。常见的做法是自己拼数字转字符串。ADC值是0到255的整数拆成百位、十位、个位加上字符0就变成ASCII字符手动拼进缓冲区。void main(void) { unsigned char adc_val; unsigned char buf[10]; UART_Init(); while (1) { adc_val ADC0831_Read(); buf[0] A; buf[1] D; buf[2] C; buf[3] ; buf[4] 0 adc_val / 100; // 百位 buf[5] 0 (adc_val / 10) % 10; // 十位 buf[6] 0 adc_val % 10; // 个位 buf[7] \r; buf[8] \n; buf[9] \0; UART_SendString(buf); Delay_MS(500); // 每500ms刷新一次 } }这种手动拼字符串的方式代码量小执行也快在8位单片机上是主流做法。Delay_MS(500)控制刷新频率500ms刷一次适合肉眼观察如果想做连续采样的数据记录可以去掉延时或者改成10ms间隔但要注意串口发送本身有耗时007秒级别的时间开销对整体影响不大。延时函数用两层嵌套循环实现一个空循环大概跑几十微秒具体延时精度不需要太讲究因为这里只是控制刷新频率不是通信时序。4. 上位机接收与串口调试实操CH340驱动、接线检查与乱码排查4.1 USB转TTL模块与AT89C51的电平匹配和接线单片机串口输出的电平是TTL电平电脑USB口是USB差分信号中间需要一个USB转TTL模块来做桥接。市面上最常见的方案是CH340芯片做的模块几块钱一个Windows 10以上系统基本免驱插上就能在设备管理器里看到COM口。如果是老系统或者精简版系统需要手动装驱动设备管理器里看到Unknown Device多半就是驱动没装好。接线方式没有太多花样关键在于别接反和必须共地CH340模块AT89C51说明RXDTXDP3.1模块接收单片机发送TXDRXDP3.0模块发送单片机接收GNDGND必须共地否则通信不稳5V5V可接可不接看模块是否带输出最容易踩的坑是把TXD接TXD、RXD接RXD这是典型的“直连思维”错误。串口的发送要和对方的接收对接相当于两个人面对面说话嘴对耳而不是嘴对嘴。如果发出去没反应第一件事就是交换TX和RX两根线再试。另外一个隐蔽问题是部分CH340模块的TXD引脚电平不是完整的0到5V而是0到3.3VAT89C51的RXD引脚识别高电平的门限是2V左右3.3V的高电平也能识别一般不会出问题但此时如果共地不良抗干扰能力会明显下降。4.2 串口调试助手的参数设置与数据解读串口调试助手的参数要和单片机侧完全一致。AT89C51这边配置的是9600、8位数据、1位停止位、无校验那么上位机也要按下表设置参数设置值说明波特率9600与TH1初值对应不能错数据位8SCON配置的是模式18位UART停止位1标准配置校验位None没有做硬件奇偶校验流控无51串口不支持硬件流控显示方式ASCII才能看到“ADC123”字样打开串口前先在设备管理器确认COM口号调试助手打开对应COM口。如果打开串口瞬间单片机复位重启多半是调试助手默认勾选了DTR和RTS这两个信号在CH340模块上通常连接到单片机复位电路电平跳变就触发复位。解决办法是在调试助手里把DTR和RTS的勾选去掉或者把模块上对应的跳线帽拔掉。4.3 常见故障排查表串口通信出了问题逐一排查是最有效率的做法。下表是实际调试中遇到频率最高的几类问题现象可能原因排查步骤完全无数据TX/RX接反未共地交换TX/RX补接GND满屏乱码波特率不匹配晶振误差大确认是否11.0592MHz检查TH1值打开串口单片机重启DTR/RTS触发了复位取消勾选DTR/RTS数据间歇性丢失共地不良线材过长缩短杜邦线检查连接牢固度显示固定值不变分压电路断开光敏电阻坏了万用表量VIN电压是否变化另一个容易混淆的点是串口烧写和串口通信的关系。很多人用STC下载器给51单片机烧程序烧写用的也是串口但下载器和USB转TTL模块不是一回事。下载器烧完程序后如果要继续用同一根线做调试通信需要把下载器切换到正常工作模式STC下载器一般有运行/下载的状态切换没有切换开关的模块可能需要重新插拔。还有一点烧写失败和通信乱码是两码事烧写失败优先检查下载器驱动和单片机供电通信乱码优先检查波特率和接线。5. 进阶把裸ASCII数据升级成带帧头校验的二进制通信格式裸的ASCII字符串“ADC123”调试时肉眼看着方便但真要做一个完整的项目——比如光照数据记录仪、多节点环境监控这种格式有几个隐患接收方解析字符串时一旦丢一个字节整条数据就对不齐没有校验光敏电阻附近有电机或者继电器干扰时收到错数也无法识别。通用做法是设计一个带帧头、数据和校验的二进制帧。帧格式可以这样定帧头1帧头2类型数据校验和帧尾0xAA0x550x01ADC值类型加数据累加0x0A帧头用两个字节0xAA 0x55是为了降低误同步概率接收端只有连续收到这两个字节才认为是一帧开始。校验和的计算方式是类型字节加数据字节取低8位。发送端的代码是这样void UART_SendFrame(unsigned char adc_val) { unsigned char frame[6]; unsigned char sum; frame[0] 0xAA; // 帧头1 frame[1] 0x55; // 帧头2 frame[2] 0x01; // 数据类型 frame[3] adc_val; // 光照值 sum frame[2] frame[3]; // 校验和 frame[4] sum 0xFF; // 取低8位 frame[5] 0x0A; // 帧尾 UART_SendBytes(frame, 6); // 连续发送6个字节 }接收端的上位机脚本按帧解析对照校验和就能判断数据是否完整。如果要做长时间的光照数据记录靠人盯着串口调试助手肯定不现实一般做法是写一个简单的Python脚本读取串口数据、按帧解析、写入文件或SQLite。用pyserial库几十行就能实现核心是判断两个帧头然后按固定长度取后面的字节校验通过再入库。这里还有一个实用技巧如果接收端发现帧头0xAA之后跟的不是0x55大概率是上一帧数据中途被干扰断了此时不能把当前字节丢弃了事而是要把0xAA当作新帧的起点继续等待0x55这样才能在连续噪声中快速恢复同步。如果怕0xAA本身出现在数据段里造成误判可以把数据字节做转义处理帧头0xAA出现的位置全部替换成0xAA 0x00之类的双字节序列解析时再还原代价是数据段长度不固定但同步可靠性会更好。本文还有配套的精品资源点击获取
返回列表