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à 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) |