一、简介:

Proteus软件是一款集电路设计、电路仿真及电路板设计为一体的EDA工具。在电压检测系统的仿真中,Proteus能够提供丰富的电子元件库和强大的仿真环境,帮助我们更直观地理解电压检测系统的工作原理和性能。电压检测系统通常包括电压传感器、模拟数字转换器(ADC)、微控制器(MCU)、显示设备等主要部分。在Proteus中,这些元件均可以在元件库中找到。

二、设计思路:

设计一个电压检测系统在Proteus仿真软件中,往往需要以下几步

2.1 确定电压检测范围和精度

首先,你需要确定你的电压检测系统需要支持的电压范围(例如0-5V或0-12V等)以及所需的检测精度。这将直接影响你选择传感器和ADC的决策。

2.2 设计电压传感器部分

电压传感器通常是一个分压器,由一对电阻组成,用于将较高的检测电压降低到ADC能够安全接受的水平。选择合适的电阻值以确保电压在ADC的输入范围内。

2.3 选择适当的ADC

根据所需的分辨率和数据位宽选择适当的ADC。例如,如果你需要10位的分辨率,你可能会选择一个10位的ADC。

2.4 微控制器的选择与编程

选择一个适合的微控制器来读取ADC的输出,并将其转换成实际的电压值。在Proteus中,你可以使用像Arduino这样的微控制器,并编写代码来读取ADC值并进行处理。

2.5 输出显示方案

选择一个显示方案,比如LCD或LED显示屏,用以展示检测到的电压值。在Proteus中,你可以直接模拟这些显示组件,并将它们连接到微控制器的相应引脚。

2.6 模拟和测试

在Proteus中构建整个电路图,然后运行仿真。检查电路的运行是否符合预期,检测的电压值是否准确。如果有必要,调整电路设计并重新仿真。

2.7 安全和保护措施

为了保护ADC和微控制器免受过高电压的损害,可能需要设计电路保护措施,如使用稳压二极管、电压钳位电路等。

2.8 电源管理

考虑整个系统的电源管理,确保所有组件的电源需求得到满足,并且电压稳定。

2.9 电路板设计

虽然在仿真阶段不是必需的,但为将来的实际应用设计一个电路板可能是一个好主意。在Proteus中,你可以使用PCB Layout工具来完成这一步。

2.10 文档化

在整个设计和仿真过程中,保持详细的文档,记录你的设计决策、电路参数以及仿真结果,以供进一步分析和参考。比如我们这篇博客就是对这这个设计过程的详细说明与分析。

三、仿真电路设计:

在Proteus中进行电压检测系统的仿真设计,可以按照以下步骤进行:

3.1 启动Proteus并创建新项目

打开Proteus软件,创建一个新的项目,并为你的电压检测系统仿真设计命名。

3.2 绘制电路原理图

在Proteus的原理图编辑器中,开始绘制电压检测系统的电路原理图。这包括:
  • 电压传感器:选择合适的电阻来创建分压电路,或者使用专用的电压传感器IC。
  • ADC:从元件库中选择一个ADC,并将其连接到电压传感器的输出。
  • 微控制器:选择一个微控制器(如Arduino、PIC、STM32等),并将其连接到ADC的输出。
  • 显示设备:选择一个LCD或LED显示屏,并将其连接到微控制器的相应引脚。
  • 电源和地:为整个电路提供适当的电源和接地。

3.3 配置元件参数

双击每个元件,配置其参数。例如,为ADC设置参考电压和采样率,为微控制器设置时钟频率等。

3.4 编写微控制器代码

使用Proteus中的代码编辑器或外部IDE(如Arduino IDE)编写微控制器的代码。代码应包括读取ADC值、转换为实际电压值、以及将结果发送到显示设备的逻辑。

3.5 添加仿真模型

确保所有元件都有相应的仿真模型。如果某些元件没有内置的仿真模型,你可能需要从外部导入或创建自定义模型。

3.6 运行仿真

在原理图编辑器中,点击“运行仿真”按钮开始仿真。观察ADC的输出是否正确,微控制器是否能够正确处理数据,以及显示设备是否能够准确显示电压值。

3.7 调试和优化

如果仿真结果不符合预期,使用Proteus的调试工具来检查电路和代码中的问题。可能需要调整电阻值、ADC参数或微控制器代码。

3.8 记录仿真结果

在仿真过程中,记录关键的仿真结果和观察到的行为,以便于分析和改进设计。

3.9 设计PCB布局(可选)

如果需要,可以使用Proteus的PCB布局工具来设计电路板的物理布局。这包括元件放置、布线和生成制造文件。

3.10 文档化

最后,整理所有设计文档,包括原理图、代码、仿真结果和任何其他相关信息,以便于未来的参考和分享,你可以在Proteus中有效地设计和仿真电压检测系统,确保其在实际应用中的性能和可靠性。

四、电路图

五、下位机程序

;ADCS BIT P3.5 ;使能接口
;ADCLK BIT P3.6 ;时钟接口
;ADDO BIT P3.7 ;数据输出接口(复用)
;ADDI BIT P3.7 ;数据输入接口
ORG     0000H
LJMP INITOUT
ORG 0023H
LJMP SERVE
ORG 30H
INITOUT:
MOV SP,#60H   ;栈顶地址
MOV TMOD,#20H  ;定时器1初始化
MOV TH1,#0F3H  ;设置波特率为1200MHz,6MHz的晶振
MOV TL1,#0F3H  ;
MOV SCON,#50H  ;串口初始化为可以接收
MOV PCON,#00H  ;波特率不倍增
SETB TR1   ;启动定时器
SETB EA   ;开中断
SETB ES   ;允许串口中断
MainProgram:    
NOP    ;主程序主要是等中断
NOP
LCALL CONV
SJMP Mainprogram
;=======================================================
SERVE:
PUSH PSW        ;将程序状态字压入堆栈 
PUSH ACC        ;将累加器压入堆栈 
CLR EA        ;关闭系统中断          
CLR RI          ;清除中断标志位 
MOV A,SBUF
CJNE A,#01,NEXTI;判断,01号单片机

MOV SBUF,31H


wait:jnb ti,wait
     clr ti

CLR RI          ;清除中断标志位 
SETB EA         ;打开系统中断 
POP ACC         ;累加器出栈 
POP PSW         ;程序状态字出栈 
NEXTI:RETI            ;中断程序返回 



;==============================================

CONV:MOV 30H,#02H;方式字选择
     MOV R0,#31H;数据存储首地址
     LCALL ADC0832;调用A/D转换子程序

     RET
;========================================
ADC0832:SETB P3.7 ;初始化通道选择
        NOP
        NOP
        CLR P3.5 ;拉低/CS端
        NOP
        NOP
        SETB P3.6 ;拉高CLK端
        NOP
        NOP
        CLR P3.6 ;拉低CLK端,形成下降沿
        MOV A,30H
        MOV C,ACC.1 ;确定取值通道选择
        MOV P3.7,C
        NOP
        NOP
        SETB P3.6 ;拉高CLK端
        NOP
        NOP
        CLR P3.6 ;拉低CLK端,形成下降沿2
        MOV A,30H
        MOV C,ACC.0 ;确定取值通道选择
        MOV P3.7,C
        NOP
        NOP
        SETB P3.6 ;拉高CLK端
        NOP
        NOP
        CLR P3.6 ;拉低CLK端,形成下降沿3
        SETB P3.7
        NOP
        NOP
    MOV R7,#8 ;准备送下后8个时钟脉冲
ADH:MOV C,P3.7 ;接收数据
    MOV ACC.0,C
    RL A ;左移一次
    SETB P3.6
    NOP
    NOP
    CLR P3.6 ;形成一次时钟脉冲
    NOP
    NOP
    DJNZ R7,ADH ;循环8次
    MOV C,P3.7 ;接收数据
    MOV ACC.0,C
    MOV @R0,A
    MOV R7,#8
ADL:MOV C,P3.7 ;接收数据
    MOV ACC.0,C
    RR A ;左移一次
    SETB P3.6
    NOP
    NOP
    CLR P3.6 ;形成一次时钟脉冲
    NOP
    NOP
    DJNZ R7,ADL ;循环8次
    MOV B,@R0
    CJNE A,B,ADC0832 ;数据校验
    SETB P3.5 ;拉高/CS端
    CLR P3.6 ;拉低CLK端
    SETB P3.7 ;拉高数据端,回到初始状态
    RET
;
这段代码是用汇编语言编写的,针对8051微控制器系列的一部分。代码的主要目的是使用外部ADC芯片(可能是ADC0832)来进行模拟-数字转换,并将转换结果通过串口传输。下面是代码的分析和解释:

5.1 注释部分

;ADCS BIT P3.5 ;使能接口
;ADCLK BIT P3.6 ;时钟接口
;ADDO BIT P3.7 ;数据输出接口(复用)
;ADDI BIT P3.7 ;数据输入接口

这部分定义了与ADC芯片通信时使用的微控制器的端口位。

5.2 初始化代码

ORG     0000H
LJMP INITOUT
ORG 0023H
LJMP SERVE
ORG 30H

这部分代码设置了程序的入口点和中断向量。

5.3 初始化输出和定时器

INITOUT:
MOV SP,#60H   ;栈顶地址
MOV TMOD,#20H  ;定时器1初始化
MOV TH1,#0F3H  ;设置波特率为1200MHz,6MHz的晶振
MOV TL1,#0F3H  ;
MOV SCON,#50H  ;串口初始化为可以接收
MOV PCON,#00H  ;波特率不倍增
SETB TR1   ;启动定时器
SETB EA   ;开中断
SETB ES   ;允许串口中断

在这部分代码中,初始化了堆栈指针、定时器、串口设置和中断。

5.4 主程序循环

MainProgram:    
NOP    ;主程序主要是等中断
NOP
LCALL CONV
SJMP Mainprogram

主程序执行了一个空操作(NOP)和一个长调用(LCALL)到一个名为CONV的子程序,然后无限循环。

5.5 串口中断服务程序

SERVE:
...
NEXTI:RETI            ;中断程序返回

当串口接收到数据时,会调用这个中断服务程序。

5.6 A/D转换调用

CONV:...
     LCALL ADC0832;调用A/D转换子程序
     ...

这个子程序调用实际进行A/D转换的子程序。

5.7 ADC0832 A/D转换程序

ADC0832:...

这部分代码是最关键的,它直接与ADC芯片通信,通过P3.5, P3.6, P3.7端口位控制ADC的选通(CS)、时钟(CLK)和数据(DATA)线。代码通过发送适当的时钟信号来启动转换,然后逐位地读取数据,最终进行数据校验,并返回读取的值。

5.8 代码中的几点注意事项:

  • 波特率设置为“1200MHz”实际上是错误的,应该是1200bps。而且6MHz的晶振无法直接生成这样的波特率,这里可能是对波特率设置的一个笔误。
  • 在ADC0832函数中,数据从ADC读取并左移(RL A)或右移(RR A)到累加器A,并存储在寄存器R0指向的地址。
  • 数据校验通过比较两次读取的数据是否一致(CJNE A,B,ADC0832)来实现,如果不一致,则重新进行A/D转换。
  • 代码中使用了大量的NOP指令来生成必要的时序。

    这段代码用于通过8051系列的微控制器来读取ADC0832的转换结果,并通过串口在接收到特定字符时将转换结果发送出去。

    六、电压检测系统

    这里使用了reg52.h头文件,它包含了8051微控制器系列的SFR(Special Function Registers)定义。代码使用随机数生成器来生成一个随机数,并将它输出到P1端口,前提是这个随机数在0到255之间(这是单个8位端口可以表示的范围)。

#include<reg52.h> // 包含51系列单片机的特殊功能寄存器的定义
#include<stdlib.h> // 包含标准库,这里主要用于rand()函数

void main(void) // 主函数
{ 
  int tempnum, i; // 定义两个整型变量,tempnum用于存放随机数,i用于循环延时
  while(1) // 无限循环
  {
    tempnum=rand(); // 调用rand()函数生成随机数并赋值给tempnum
    if (0<=tempnum<=255) // 这里尝试检查tempnum的值是否在0到255之间
    {
      P1=tempnum; // 如果是,将tempnum的值赋给P1端口
    }
    for(i=0;i<30000;i++); // 一个简单的延时循环,没有执行体
  }
}

代码可能出现以下几个问题:

  1. 条件判断的误用:
    在C语言中,条件if (0<=tempnum<=255)并不会像预期的那样工作。这个表达式首先会计算0<=tempnum的结果,这会是1(如果tempnum大于等于0)或0(如果tempnum小于0),然后将这个结果(10)和255比较。为了正确地进行范围检查,应该写为两个独立的条件:

    if (0 <= tempnum && tempnum <= 255)
    
  2. 随机数生成:
    rand()函数返回一个在0RAND_MAX之间的随机数,其中RAND_MAX通常被定义为32767。在这个程序中,尽管rand()生成的数可能超出0-255范围,但错误的条件判断意味着P1总是被赋予rand()返回的值,不论其大小如何。

  3. 缺少随机数种子:
    rand()函数是伪随机数生成器,如果不给它一个种子,它每次在程序启动时都会生成相同的随机数序列。通常需要调用srand()函数,并传递一个种子,比如当前时间time(NULL),但在嵌入式系统中,可能需要找到其他种子生成的方式,例如读取未连接的输入端口或使用内部定时器的值。

除此之外,这个程序似乎目的是不断地将随机数输出到P1端口(可能连接到一些外设,如LED灯),并在每次输出后进行简单的软件延时。

七、运行效果:

7.1 运行整体

检测电压表显示




八、总结

嵌入式编程基础

  • 了解如何使用C语言编写嵌入式系统程序,特别是针对8051系列的微控制器。
  • 学习如何包含头文件,如reg52.h,它定义了特定微控制器的寄存器,这些寄存器用于控制硬件功能。

端口操作

  • 学习如何通过编程来控制微控制器的I/O端口,例如将一个值写入P1端口。

随机数生成

  • 了解如何使用rand()函数生成随机数,以及如何使用srand()函数设置随机数种子以获得不同的随机数序列。

条件判断

  • 学习如何正确地使用条件判断语句,特别是在检查一个值是否在某个范围内时,需要使用逻辑运算符&&来组合两个比较条件。

软件延时

  • 了解如何使用简单的循环来创建软件延时,这在嵌入式系统中很常见,因为它们通常没有操作系统提供的定时器服务。

代码审查和调试

  • 学习如何分析代码并识别潜在的问题,如条件判断的误用和随机数生成器的种子问题。

编程实践

  • 了解编写嵌入式系统代码时应该注意的一些最佳实践,例如确保随机数生成器每次运行时都产生不同的序列,以及确保条件判断语句的正确性。

代码优化

  • 学习如何优化代码,例如避免不必要的循环和确保条件判断的正确性,以提高代码的效率和可靠性。

    通过这些学习点,我们可以更好地理解嵌入式系统编程的基础,以及如何编写、分析和优化这类代码。