Netduino, I2C Bus Driver in VB .NET

Il Bus I2C

Netduino possiede due pin (SDA/SCL) per la gestione di dispositivi via bus I2C (Inter Integrated Circuit). Il Bus I2C è un bus seriale ad alta velocit&agrave utilizzato all’interno delle schede elettroniche per far comunicare i diversi circuiti integrati.Il bus è composto da almeno un master che controlla il bus e uno slave che risponde ai comandi inviati dal master.

 

Il pin SDA trasporta i dati mentre il pin SCL fornisce il clock e, per mezzo dei fronti di salita e di discesa, anche il sincronismo. Per una trattazione piú completa rimando il lettore al seguente link (wikipedia) mentre qui ricordo solo che le due linee del bus hanno bisogno delle resistenze di pull-up essendo di tipo open-drain.

La classe I2CPlug

Dato che ogni sensore ha una suo indirizzo e una frequenza di clock di funzionamento, l’idea è stata quella di sviluppare una classe base che contenga un minimo di metodi e proprietà utili a pilotare un dispositivo generico. Quindi, successivamente, la classe viene utilizzata nella stesura dei driver di dispositivo specifici tramite ereditarietà e arricchita con metodi e proprietà valide solo per il dispositivo in esame.
La classe I2CPlug, una volta istanziata, mantiene sempre due riferimenti attivi, uno ad una variabile di tipo I2CDevice e un altro ad una variabile di tipo I2CDevice.Configuration. La variabile I2CDevice è una variabile Shared (Static in C#), cioè viene istanziata una sola volta (la prima) ed è comune a tutte le classi derivate da I2CPlug mentre la variabile I2CDevice.Configuration è privata e contiene le informazioni di configurazione e funzionamento (del bus) per il dispositivo specifico.


Public Class I2CPlug

	'Default TimeOut in ms
	Private Const TIMEOUT_DEFAULT = 100

	'Clock Default Value, Minimum and Maximum (Clock rate in kHz)
	Private Const CLOCK_DEFAULT = 100
	Private Const CLOCK_MINIMUM = 100
	Private Const CLOCK_MAXIMUM = 3400

	'Reference to the I²C Device
	Private Shared m_Device As I2CDevice = Nothing

	'I²C Configuration. Different Configuration for each device
	Private m_Config As I2CDevice.Configuration = Nothing

	'Transaction timeout
	Private m_TimeOut As Integer = TIMEOUT_DEFAULT

	'Class Creation Method
	Public Sub New(ByVal Address As UShort, Optional ByVal Clock As Integer = CLOCK_DEFAULT, Optional ByVal TimeOut As Integer = TIMEOUT_DEFAULT)
			'ClockRate Checking ...
			If (Clock < CLOCK_MINIMUM) Then Clock = CLOCK_MINIMUM
			If (Clock > CLOCK_MAXIMUM) Then Clock = CLOCK_MAXIMUM
			'Timeout Checking ...
			If (TimeOut < 0) Then TimeOut = TIMEOUT_DEFAULT
			m_TimeOut = TimeOut
			'Configuration Setup
			m_Config = New I2CDevice.Configuration(Address, Clock)
			'First istance of  I²C device ...
			If m_Device Is Nothing Then

					'Problem with SDA/SCL pins, reset the SDA pin with the first instance
					Dim p As New OutputPort(Pins.GPIO_PIN_SDA, True)
					p.Write(False)
					p.Dispose()

					'Create device istance
					m_Device = New I2CDevice(m_Config)
			End If
	End Sub

	...

End Class

Sono state inserite delle costanti generali che riguardano il tempo di timeout (in millisecondi) e i valori minimi, massimi e di default del clock (espressi in kHz). Nella creazione della classe (New) viene obbligatoriamente passato almeno un parametro che è l’indirizzo del dispositivo nel bus. Come parametri opzionali vengono passati il Clock e il tempo di TimeOut con dei valori di default nel
caso che non vengano utilizzati. Dopo alcuni controlli la classe crea un nuovo device (se non era già istanziato), ma prima azzera la linea SDA dei dati. L’azzeramento della linea SDA è stata introdotto per risolvere un problema noto del firmare del Netduino. Le proprietà della classe sono tutte Read Only dato che non si possono cambiare “al volo” una volta creata la classe.
Passiamo ora ai tre metodi piú importanti della classe: Read(), Write() e WriteAndRead().

...

Protected Function Read(ByRef Buffer() As Byte) As Integer
		Dim res As Integer = -1
		SyncLock m_Device
				'Selects the specific device configuration ...
				m_Device.Config = m_Config
				Dim action As I2CDevice.I2CTransaction() = New I2CDevice.I2CTransaction() {I2CDevice.CreateReadTransaction(Buffer)}
				res = m_Device.Execute(action, m_TimeOut)
		End SyncLock
		Return res
End Function

Protected Function Write(ByRef Buffer() As Byte) As Integer
		Dim res As Integer = -1
		SyncLock m_Device
				'Selects the specific device configuration ...
				m_Device.Config = m_Config
				Dim action As I2CDevice.I2CTransaction() = New I2CDevice.I2CTransaction() {I2CDevice.CreateWriteTransaction(Buffer)}
				res = m_Device.Execute(action, m_TimeOut)
		End SyncLock
		Return res
End Function

Protected Function WriteAndRead(ByRef WriteBuffer() As Byte, ByRef ReadBuffer() As Byte) As Integer
		Dim res As Integer = -1
		SyncLock m_Device
				'Selects the specific device configuration ...
				m_Device.Config = m_Config
				Dim actions As I2CDevice.I2CTransaction() = New I2CDevice.I2CTransaction() {I2CDevice.CreateWriteTransaction(WriteBuffer), I2CDevice.CreateReadTransaction(ReadBuffer)}
				res = m_Device.Execute(actions, m_TimeOut)
		End SyncLock
		Return res
End Function

...

Le tre funzioni permettono di leggere o scrivere (o scrivere e successivamente leggere) un array di byte. L’array di byte deve essere precedentemente istanziato dato che viene passato per riferimento (ByRef). Quando viene creata una nuova transazione con la classe I2CDevice.I2CTransaction(), il numero di byte da leggere o scrivere viene ricavato direttamente dalla dimensione del buffer passato come parametro. Le funzioni sono definite Protected ed è stato previsto l’utilizzo di SyncLock
per serializzare l’accesso al bus e quindi permettere l’utilizzo delle funzioni da piú task contemporaneamente (MultiThreading). L’ultima funzione cioè WriteAndRead() è un metodo molto interessante ed è anche il piú utilizzato. Questo metodo scrive uno o piú byte nello slave e poi legge uno o piú byte dallo stesso. Come le precedenti chiamate, i due buffer devono essere già configurati con le dimensioni opportune prima di passarli alla funzione.

...

Protected Function ReadRegister(ByRef Register As Byte, ByRef Buffer() As Byte) As Integer
		Return WriteAndRead(New Byte() {Register}, Buffer)
End Function

Protected Function WriteRegister(ByRef Register As Byte, ByRef Buffer() As Byte) As Integer
		Dim tmp() As Byte = New Byte(Buffer.Length) {}
		tmp(0) = Register
		Buffer.CopyTo(tmp, 1)
		Return Write(tmp)
End Function

Protected Function ReadRegister(ByRef Register As Byte, ByRef Buffer As Byte) As Integer
		Return WriteAndRead(New Byte() {Register}, New Byte() {Buffer})
End Function

Protected Function WriteRegister(ByRef Register As Byte, ByRef Buffer As Byte) As Integer
		Dim tmp As Byte() = New Byte(1) {Register, Buffer}
		Return Write(tmp)
End Function

...

Per semplificare la scrittura e la lettura del codice, sono state aggiunte altre funzioni (Expanded Methods) per leggere e scrivere nei registri dello slave un singolo byte oppure un array di byte dato l’indirizzo. Questo risulta molto comodo quando si svilupperà un driver customizzato per un dispositivo specifico. Alla fine della classe sono state inserite alcune funzioni di pubblica utilità per la conversione da byte a decimale e viceversa, dato che in alcuni dispositivi i valori sono codificati in BCD.

...

Public Function BCDtoDEC(ByVal Value As Byte) As Byte
		Return CByte((Value \ 16) * 10) + CByte(Value And &HF)
End Function

Public Function DECtoBCD(ByVal Value As Byte) As Byte
		If (Value <= 99) Then
				Return CByte((Value \ 10) * 16) + CByte(Value Mod 10)
		Else
				Return CByte(0)
		End If
End Function

Public Function DECtoBCD(ByVal Value As Integer) As Byte
		Value = (Value And &HFF)
		Return CByte((Value \ 10) * 16) + CByte(Value Mod 10)
End Function

...

Esempio di utilizzo della Classe I2CPlug

Vediamo ora un esempio semplice di utilizzo della classe I2CPlug. Prima di tutto si dichiara una nuova classe derivata da I2CPlug ereditando metodi e proprietà dalla classe base. Successivamente vengono definite alcune costanti per indirizzare due registri del sensore e il bit 7 che corrisponde al valore decimale 128 oppure &H80 in esadecimale (nel byte i bit vengono indicizzati da 0 a 7).

Public Class Sensor

		Inherits I2CPlug

		Const BIT7 As Byte = &H80

		Const REGISTERBOOL as Byte = &H7
		Const REGISTERBYTE as Byte = &H8

		Public Sub New(ByVal Address As UShort, Optional ByVal ClockRateKHz As Integer = 300, Optional ByVal TimeOut As Integer = 100)
				'Device Creation
				MyBase.New(Address, ClockRateKHz, TimeOut)
		End Sub

		Public Property BooleanValue As Boolean
				Get
						'Read Boolean from Register
						Dim tmp(0) As Byte
						MyBase.ReadRegister(REGISTERBOOL, tmp)
						Return ((tmp(0) And BIT7) = BIT7)
				End Get
				Set(ByVal value As Boolean)
						'Read from Register
						Dim tmp(0) As Byte
						MyBase.ReadRegister(REGISTERBOOL, tmp)
						If value Then
								tmp(0) = tmp(0) Or BIT7
						Else
								tmp(0) = tmp(0) And Not (BIT7)
						End If
						MyBase.WriteRegister(REGISTERBOOL, tmp)
				End Set
		End Property

		Public Property ByteValue As Byte
				Get
						'Read from Register
						Dim tmp(0) As Byte
						MyBase.ReadRegister(REGISTERBYTE, tmp)
						Return tmp(0)
				End Get
				Set(ByVal value As Byte)
						'Write to Register
						Dim tmp(0) As Byte
						tmp(0) = value
						MyBase.WriteRegister(REGISTERBYTE, tmp)
				End Set
		End Property

		...

End Class

Viene ridefinito il metodo New() per la creazione della classe, ma solo per cambiare il valore di default del ClockRate per renderlo piú congeniale al dispositivo. Le due proprietà BooleanValue e ByteValue leggono e scrivono nei due registri rispettivamente, un solo bit (il settimo) oppure un byte.

Per utilizzare la nuova classe Sensor con indirizzo &H68, basta inserire nella chiamata Sub Main di netduino il codice sottostante.

Public Sub Main()

	Dim SEN As Sensor = New Sensor(&H68)
	Dim Value as Byte

	'Esegui per sempre ...
	While True

			...
			'Lettura del valore
			Value = SEN.ByteValue

			'Stampa del valore letto
			Debug.Print(Value.ToString)
			...

	End While

End Sub

Cosa resta da fare?

Restano ancora da implementare nella classe I2CPlug alcuni metodi utili per la gestione dei singoli bit di un byte e per la lettura di variabili Short/UShort e Single dato un offset di registro. Per quanto riguarda le variabili che occupano piú di un byte bisognerebbe prevedere anche il byte reversing (Little endian/Big endian) … per invertire i byte prima di scriverli oppure dopo averli letti dal dispositivo, … non si sa mai!

Prossimamente ci sarà un aggiornamento della classe con alcune nuove funzioni e ricordo che gli script qui presentati fanno riferimento alla versione v1.3 sviluppata in VB.NET Micro.

Conclusioni

Il codice sopra presentato è un pò prolisso (vedere i vari if/then/else) proprio perchè l’articolo ha uno scopo didattico/divulgativo ed è orientato a persone che sono alle prime armi con Microsoft VB.NET Micro.

Nel prossimi articoli vedremo come utilizzare la classe I2CPlug per gestire il dispositivo RTC DS1307 (Real Time Clock) e il sensore di Temperatura e Pressione BMP085. Per il momento buon lavoro a tutti.

Informazioni e Risorse

This work is licensed under a
Creative Commons Attribution 4.0 International License
Download the I2CPlug Sources v1.3 (VB.NET)

Tags: , , , ,

This entry was posted on Friday, January 3rd, 2014 at 20:22 and is filed under Elettronica, Italiano, Programmazione. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

 
 
  • Chuck Norris Joke


  • a man, a myth.