使用 C# 和 ATmega16 微控制器制作数字温度计






4.90/5 (90投票s)
通过 C# 进行串行端口硬件接口通信。
引言
我们家里都有温度计,但是如果想在电脑上以数字方式查看它或者在电脑中保存记录,那就非常困难了。为了提供这种便利,我制作了一个硬件来检测温度,并将其与电脑连接,以便在电脑上显示或记录温度。
注意:此应用程序需要一些硬件通过串口向应用程序发送相关数据。如果没有此类设备,它将无法工作。
目录
- 操作步骤
- 硬件详情
- 软件详情
- 配置串口
- 接收数据
- 采样数据
- 创建图形
- 显示数据
操作步骤
温度传感器 ----> Atmega16 微控制器 ------> 电脑串口
(LM35) (发送方) (接收方)
这里我试图解释的是,温度传感器 LM35 检测温度并将相应的按比例缩放的电压传递给微控制器,微控制器将其转换为数字数据。
这些数字数据是我们的温度读数,然后通过微控制器和电脑之间的异步串行传输,通过我们电脑上的串口传输到我们的应用程序。

这是温度传感器。

这是我们的 Atmega16 微控制器,它创建数字温度读数并将其传输到电脑。它是一个 8 位微控制器,具有 16KB 闪存,足以运行长程序。为了编程我的微控制器,我使用了 AVR Studio 4 平台和 C 语言。
微控制器的代码只包括连续读取传感器并将数据通过串口传输到我们的 C# 应用程序。
微控制器的代码如下:
#include<avr/io.h>
#include<avr/interrupt.h>
#define FOSC 12000000// Clock Speed
#define F_CPU 12000000ul
#define BAUD 9600
#define MYUBRR (FOSC/16)/BAUD -1
//#include<util/delay.h>
//initialise USART
void USART_Init( unsigned int ubrr)
{
//Set baud rate
UBRRH = (unsigned char)(ubrr>>8);
UBRRL = (unsigned char)ubrr;
//Enable receiver and transmitter
UCSRB = (1<<RXEN)|(1<<TXEN);
//Set frame format: 8data, 2stop bit, NO parity
UCSRC = (1<<URSEL)|(1<<USBS)|(3<<UCSZ0);
}
//Initialise A to D converter
void ADC_Init()
{
//enable adc
ADCSRA |= (1<<ADEN);
//enable interrupts
ADCSRA |= (1<<ADIE);
//set reference selection to Vcc;
//left adjust result
//set voltage selection to bit 7 of portA
ADMUX |= (1<<REFS0) | (1<<ADLAR) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0);
//set prescalar to 128
ADCSRA |= (1<<ADPS0) | (1<<ADPS1) | (1<<ADPS2);// | (1<<ADATE);
}
//Transmit Data
void USART_Transmit( unsigned char data )
{
/* Wait for empty transmit buffer */
while ( !( UCSRA & (1<<UDRE)) )
;
/* Put data into buffer, sends the data */
UDR = data;
}
//Interrupt A to D converter reading
ISR(ADC_vect)
{
unsigned char c;
//variable c stores data from ADCH register
c=ADCH;
USART_Transmit(c);
//next statement starts a new ADC conversion
ADCSRA |= (1<<ADSC);
}
int main( void )
{
USART_Init ( MYUBRR );
ADC_Init();
sei();
//start an adc conversion
ADCSRA |= (1<<ADSC);
while(1);
}
同时监控多个传感器
注意:如果您是微控制器新手,并且这是您的第一个项目之一,那么我建议您只实现上述代码并使用一个传感器。如果您认为您可以做到,那再好不过了,请继续。
这是某人(我猜是 Joel 先生)要求实现的,所以我将其添加到这里。
这展示了如何监控多达 8 个温度传感器并将它们的数据传递到您的电脑。
在 Atmega16 微控制器的 ADMUX 寄存器中,MUX4.....MUX0 位(5 位)控制 PORT A(8 个引脚中)的哪个引脚的数据将用于数字转换,以便将其传输到电脑。
这里我们的基本思想是,每次转换后,我们都会不断改变要读取的引脚。这样,我们将依次读取 PIN0 到 PIN7(所有 8 个),然后再回到 PIN0。
下面我将展示 ADMUX 寄存器中 MUX4....MUX0 位的值如何选择通道(PORT A 的引脚)。
MUX4....MUX0 | 将被读取的 PORTA 引脚 |
00000 | PIN 0 |
00001 | PIN 1 |
00010 | PIN 2 |
00011 | PIN 3 |
00100 | PIN 4 |
00101 | PIN 5 |
00110 | PIN 6 |
00111 | PIN 7 |
现在为了在代码中正确实现它,我们的想法是,一旦转换完成并调用中断服务例程 (ISR),我们将更改下一个转换的引脚。具体做法如下:
ISR(ADC_Vect)
{
//code for reading and transmitting conversion ( shown above as well)
unsigned char c;
c=ADCH;
USART_Transmit(c);
//Now read the value of MUX4,MUX3, MUX2, MUX1 & MUX0 bits of ADMUX register
//and increment it by one if less than 8 else make it 0.
unsigned char d;
d = ADMUX & 0x1F;
if(d < 7)
{ d = d + 1;
ADMUX |= d;
}
else
{ ADMUX |= 0;
}
//Now start a new conversion which will read the next PIN now
ADCSRA |= (1<<ADSC);
}
原理图
温度传感器仅连接到微控制器。
这里很多人要求提供原理图,所以我在这里添加了连接图。希望这能达到目的。
下图是 Atmega16 微控制器与传感器 LM35(右侧)的连接图。图像左侧是晶体振荡器,如果有人需要比微控制器内部 1MHz 更高的工作频率(它能达到目的),则需要它。如果您对内部频率满意,则无需添加此晶体振荡器。
现在是如何通过串口将硬件连接到电脑的问题。为此,您应该知道电脑串口的电压约为 10v,而微控制器的工作电压约为 5v。因此,为了有效通信,我们需要一个电平转换器(它可以根据两端的设备转换正在进行的电压)。IC MAX232 就能满足这个目的,它是一个通用且廉价的 IC,连接方式非常简单,如下所示:

这就是硬件的详细介绍。现在我将讨论一些软件部分。
软件详情
在本节中,我将讨论我的应用程序代码,该代码用于从串口获取温度读数并显示。现在,数据已在我们的电脑串口上可用,我们现在只需要一个应用程序来检索它,这在此处完成。
配置串口
首先要做的是配置串口,即固定波特率,设置奇偶校验位,单个数据包中要接收的数据位数,以及要使用的停止位数。
首先,我们在类定义中创建一个全局的 System.IO.Ports.SerialPort
对象 port
,然后在 Form 的 Load
事件中对其进行初始化。
还有一些变量,用于保存 port
对象的属性值。
portname
保存串口的名称。在我的电脑中,它是 COM4。voltage
保存参考电压,它实际上是微控制器板的 Vcc 电压,在此应用程序中手动设置。这里是 4.65 伏。parity
保存电脑和微控制器之间串行通信中使用的奇偶校验类型,即偶校验、奇校验或无奇偶校验。我没有使用任何奇偶校验。BaudRate
保存串行通信的波特率值。我选择了 9600 bps 的波特率。StopBits
保存要使用的停止位数。可能的值是 1、2 或无。我使用了 2 个停止位。- 最后是
databits
变量,它定义了通信期间要接收的数据位数。使用 8 个数据位总是一个不错的选择,因为它是一个标准的字节大小,因此操作起来更容易。
public partial class Thermometer : Form
{
public Thermometer()
{
InitializeComponent();
}
System.IO.Ports.SerialPort port;
static public double voltage;
static public string portname;
static public Parity parity;
static public int BaudRate;
static public StopBits stopbits;
public int databits;
private void Form1_Load(object sender, EventArgs e)
{
portname = "COM4";
parity = Parity.None;
BaudRate = 9600;
stopbits = StopBits.Two;
databits = 8;
port = new System.IO.Ports.SerialPort(portname);
port.Parity = parity;
port.BaudRate = BaudRate;
port.StopBits = stopbits;
port.DataBits = databits;
port.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived);
port.Open();
}
这些是串口设置。微控制器中也使用了相同的通信设置,以便有效进行通信。
接收数据
调用 port.open()
方法打开串口后,串口即可接收到达的数据。
DataReceived
:为了在应用程序中接收数据,我们需要处理SerialPort
类的DataReceived
事件。port.Read
方法从 COM4 串口读取数据并将其写入单个变量byte
数组bt
。
void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// read one byte data into bt
port.Read(bt,0,1);
// all the code to sample data
}
采样数据
数据采样的基本思想是,由于读数是连续接收的,我们取 100 个值并计算它们的平均值,以便获得更准确的温度读数。这为我们提供了一个要显示的单个温度读数。
为了进行此计算,创建了一个全局 double
变量 sum
和一个 int
变量 count
。
sum
累加后续的温度读数,count
统计读数的数量,以查看它们是否达到 100。
这段代码位于 DataReceived
事件中,在上述代码之前。
// the calculation on the right hand side calculates
// the temperature from the read value
sum += Convert.ToDouble(bt[0]) *voltage*100/255;
//this counts to 100
count++;
现在,一旦计数达到 100,计算出的 sum
值将通过除以 100
进行平均。然后将此采样值存储在 double temp
变量中,sum
和 count
再次设置为 0
。
// single temperature reading
temp = sum / 100;
sum = 0;
count = 0;
现在我们有了温度读数,下一个任务是计算指针的角度,以便计算出的 temp
可以显示在圆形仪表图像上。
角度将存储在变量 angle
中。在计算 angle
之前,请简单看一下圆形仪表。某些值之间的角度变化不均匀,即 20 - 30 之间的角度小于 30 - 40 和 40 - 50 之间的角度(请参阅上图)。
因此,在计算角度之前,需要考虑这一点。
经过一些分析,我发现仪表上从 50 值开始的一些角度为 0 度。
创建图形
基本上,像这样创建图形意味着每当获得温度值时,指针不应该只是立即设置到仪表上的那个读数,而应该通过后续旋转移动到那里(就像某些模拟仪表中,指针从初始值移动到最终值一样)。
为了实现这一点,创建了一个名为 Animate
的方法,该方法将指针从初始读数动画到最终读数。该方法将 CurrentAngle
和 FinalAngle
作为参数。
此方法在每一步中将变量 CurrentAngle
的值增加 1
,并不断递归调用自身,直到值达到 FinalAngle
值。在每一步中,通过代码触发 form1.Paint
事件,将值渲染到屏幕上。
private void Animate(double Currentangle, double FinalAngle)
{
// if Final angle is negative then make it positive
if (FinalAngle < 0)
FinalAngle += 360;
// if final angle is greater than 360 degree then reduce it
if (Currentangle >= 360)
{
Currentangle = Currentangle - 360;
}
// if current angle is not within +0.5 and -0.5 of
// the final angle value then execute if block
if (!(Currentangle > FinalAngle-0.5 && Currentangle < FinalAngle + 0.5))
{
if (Currentangle > FinalAngle)
{
//decrement Currentangle
Currentangle -= 1;
}
else
{
//else Increment Current angle
Currentangle += 1;
}
Form1_Paint(this, new PaintEventArgs(surface, DrawingRectangle));
Animate(Currentangle, FinalAngle);
}
}
现在,我们的动画代码就完成了。接下来是在屏幕上创建图形。
显示数据
为了在屏幕上创建仪表,在创建图形时必须确保屏幕不闪烁,因此请务必使用缓冲图形。
为此,在窗体的 Load
事件中创建并初始化了一个 BufferedGraphics
对象 buff
。还创建了一个 System.Drawing.Graphics
对象 surface
,它只表示绘制图像的图形表面。
// create buffered graphics object for area of the form by using this.Bounds
buff = BufferedGraphicsManager.Current.Allocate(this.CreateGraphics(),this.Bounds);
surface = buff.Graphics;
//ensure high quality graphics to users
surface.PixelOffsetMode = PixelOffsetMode.HighQuality;
surface.SmoothingMode = SmoothingMode.HighQuality;
现在进入窗体的最终 Paint
事件。首先,我们将圆形仪表和仪表指针的图像保存在 Image
类对象 img
和 hand
中。
之后,仪表被绘制在窗体上。指针旋转到计算出的角度并绘制在屏幕上。温度通过调用 DrawString
方法绘制在屏幕上。
所有代码都写在一个 try - catch
块中,并捕获一个 InvalidOperationException
,这确保了如果某些情况下图形无法在屏幕上渲染,也不会导致应用程序崩溃。
private void Form1_Paint(object sender, PaintEventArgs e)
{
try
{
Image img = new Bitmap(Properties.Resources.speedometer, this.Size);
Image hand = new Bitmap(Properties.Resources.MinuteHand)
surface.DrawImageUnscaled(img, new Point(0, 0));
surface.TranslateTransform(this.Width / 2f, this.Height / 2f);
surface.RotateTransform((float)CurrentAngle);
surface.DrawImage(hand, new Point(-10, -this.Height / 2 + 40));
string stringtemp = displaytemp.ToString();
stringtemp = stringtemp.Length > 5 ?
stringtemp.Remove(5, stringtemp.Length - 5) : stringtemp;
Font fnt = new Font("Arial", 20);
SizeF siz = surface.MeasureString(stringtemp, fnt);
surface.ResetTransform();
LinearGradientBrush gd = new LinearGradientBrush(new Point(0,(int)siz.Height + 20),
new Point((int)siz.Width,0), Color.Red, Color.Lavender);
surface.DrawString(stringtemp, fnt, gd, new PointF(DrawingRectangle.Width / 2 -
siz.Width / 2, 70));
surface.DrawEllipse(Pens.LightGray, DrawingRectangle);
surface.Save();
buff.Render();
}
catch(InvalidOperationException)
{
//code to handle exception
}
}
至此,应用程序的编码部分完成。
我还有另一个有趣的项目,即通过 C# 应用程序从电脑控制 230 - 250 伏交流家用电器。
我还计划在这里编写一个纯软件应用程序,即隔离存储。复制或剪切您的文件和文件夹并将其粘贴到此应用程序提供的 GUI 中,然后只需将它们从您的电脑中删除即可。它会将您的数据存储在硬盘上的隔离存储区域中,并且只有通过此应用程序才能看到它。从此应用程序复制并粘贴回您的文件系统。不会丢失任何数据。我计划很快也在这里写下它……
结论
至此,我提供了使用 C# 应用程序通过串口进行硬件接口的思路。任何像我一样喜欢开发个人硬件的人都会喜欢阅读这篇文章。这是我第二次写这篇文章,因为之前由于缺乏解释,它不太受欢迎。我希望这次情况会更好。