-- -- Author: Brent Seidel -- Date: 9-Aug-2024 -- -- This file is part of bbs_embed. -- Bbs_embed is free software: you can redistribute it and/or modify it -- under the terms of the GNU General Public License as published by the -- Free Software Foundation, either version 3 of the License, or (at your -- option) any later version. -- -- bbs_embed is distributed in the hope that it will be useful, but -- WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -- Public License for more details. -- -- You should have received a copy of the GNU General Public License along -- with bbs_embed. If not, see .-- -- with SAM3x8e; use type SAM3x8e.Bit; with SAM3x8e.PMC; -- Needed to enable I2C clocking with SAM3x8e.PIO; -- Needed to configure I2C pins with SAM3x8e.TWI; -- Needed for I2C interface with BBS; use type BBS.uint32; with BBS.embed; with BBS.embed.due.serial.int; with BBS.embed.log; -- -- Package for the I2C interface -- -- The Arduino Due has two I2C interfaces. -- Interface SCL SDA TWI -- I2C-0 PB13 PB12 TWI0 -- I2C-1 PA18 PA17 TWI1 -- package body bbs.embed.i2c.due is -- -- Function to return access to a device record. -- function get_interface(d : port_id) return due_i2c_interface is begin return i2c_port(d); end; -- -- Initialize an interface -- -- -- The I2C-0 port is connected to pins PB13 and PB12. The UART -- initialization is used as a basis for this. -- procedure init(chan : port_id; speed : speed_type) is pins : uint32; begin -- -- Initialize internal data structures for both devices. -- i2c_port(0).dev_id := BBS.embed.due.dev.TWI1_ID; i2c_port(0).port := TWI1'Access; i2c_port(0).pioc := BBS.embed.GPIO.Due.PIOB'Access; i2c_port(0).sda_pin := 12; i2c_port(0).scl_pin := 13; i2c_port(0).int_id := Ada.Interrupts.Names.TWI1_Interrupt; i2c_port(0).handle := buff(0); buff0.set_interface(i2c_0'Access); -- i2c_port(1).dev_id := BBS.embed.due.dev.TWI0_ID; i2c_port(1).port := TWI0'Access; i2c_port(1).pioc := BBS.embed.GPIO.Due.PIOA'Access; i2c_port(1).sda_pin := 17; i2c_port(1).scl_pin := 18; i2c_port(1).int_id := Ada.Interrupts.Names.TWI0_Interrupt; i2c_port(1).handle := buff(1); buff1.set_interface(i2c_1'Access); -- pins := 2**Natural(i2c_port(chan).sda_pin) or 2**Natural(i2c_port(chan).scl_pin); -- -- Initialize hardware for selected device. -- -- Enable clock for I2C-x -- SAM3x8e.PMC.PMC_Periph.PMC_PCER0.PID.Arr(Integer(i2c_port(chan).dev_id)) := 1; -- -- Configure pins PB12 and PB13 to be I2C pins. -- -- PER -- PDR i2c_port(chan).pioc.PDR.Val := SAM3x8e.UInt32(pins); -- OER -- ODR i2c_port(chan).pioc.OER.Val := SAM3x8e.UInt32(pins); -- IFER -- IFDR i2c_port(chan).pioc.IFDR.Val := SAM3x8e.UInt32(pins); -- SODR -- CODR -- IER -- IDR i2c_port(chan).pioc.IDR.Val := SAM3x8e.UInt32(pins); -- MDER -- MDDR i2c_port(chan).pioc.MDDR.Val := SAM3x8e.UInt32(pins); -- PUDR -- PUER i2c_port(chan).pioc.PUER.Val := SAM3x8e.UInt32(pins); -- ABSR - really needed since both TWI are function A i2c_port(chan).pioc.ABSR.Arr(Integer(i2c_port(chan).sda_pin)) := 0; i2c_port(chan).pioc.ABSR.Arr(Integer(i2c_port(chan).scl_pin)) := 0; -- OWER -- OWDR i2c_port(chan).pioc.OWDR.Val := SAM3x8e.UInt32(pins); -- -- Do whatever configuration is needed to configure the I2C controller. -- -- -- Set TWI clock for 100kHz -- The system clock is 84_000_000Hz -- The I2C clock is 100_000Hz -- This gives 840 system clocks per I2C clock. -- -- The clock rate is set by three parameters: CLDIV, CHDIV, and CKDIV. -- Instead of specifying a clock rate, these specify the high time and -- the low time for the I2C clock. The times are given by (note that -- the wave is symmetrical so Tlow = Thigh, meaning that CLDIV = CHDIV): -- Tlow = ((CLDIV * 2^CKDIV) + 4) * Tmck -- Where: -- Tmch is the master clock period -- This should reduce to: -- 420 = (CLDIV * 2^CKDIV) + 4 -- or -- 416 = CLDIV * 2^CKDIV -- or -- CLDIV = 416/(2^CKDIV) -- -- CLDIV and CHDIV occupy 8 bits and this are limited to 0 - 255 -- CKDIV is 3 bits and is limited to 0 - 7. -- So, we can get: -- CLDIV = CHDIV = 104 -- CKDIV = 2 -- if speed = high400 then i2c_port(chan).port.CWGR.CKDIV := 0; else i2c_port(chan).port.CWGR.CKDIV := 2; end if; i2c_port(chan).port.CWGR.CLDIV := 104; i2c_port(chan).port.CWGR.CHDIV := 104; -- -- Enable master mode -- i2c_port(chan).port.CR.MSEN := 1; -- Enable master mode i2c_port(chan).port.CR.SVDIS := 1; -- Disable slave mode -- -- Set channel not busy -- Ada.Synchronous_Task_Control.Set_True(i2c_port(chan).not_busy); end init; -- -- Non-object oriented interface. -- -- Routines to read and write data on the i2c bus. These are based on the -- flowcharts in the datasheet. The read routines are interrupt driven. -- The write routine is still partially polled. It will eventually be -- converted to interrupt driven and a block write added. -- procedure write(chan : port_id; addr : addr7; reg : uint8; data : uint8; error : out err_code) is status : SAM3x8e.TWI.TWI0_SR_Register; begin if (addr < 16#0E#) or (addr > 16#77#) then error := invalid_addr; return; end if; Ada.Synchronous_Task_Control.Suspend_Until_True(i2c_port(chan).not_busy); i2c_port(chan).port.CR.MSEN := 1; -- Enable master mode i2c_port(chan).port.CR.SVDIS := 1; -- Disable slave mode i2c_port(chan).port.MMR.MREAD := 0; -- Master write i2c_port(chan).port.MMR.IADRSZ := SAM3x8e.TWI.Val_1_Byte; -- Register addresses are 1 byte; i2c_port(chan).port.MMR.DADR := SAM3x8e.UInt7(addr); i2c_port(chan).port.IADR.IADR := SAM3x8e.UInt24(reg); i2c_port(chan).port.THR.TXDATA := SAM3x8e.Byte(data); i2c_port(chan).port.CR.STOP := 1; loop status := i2c_port(chan).port.SR; exit when status.TXRDY = 1; exit when status.NACK = 1; exit when status.OVRE = 1; end loop; if status.NACK = 1 then error := nack; elsif status.OVRE = 1 then error := ovre; else error := none; end if; delay until Ada.Real_Time.Clock + i2c_delay; Ada.Synchronous_Task_Control.Set_True(i2c_port(chan).not_busy); end; -- function read(chan : port_id; addr : addr7; reg : uint8; error : out err_code) return uint8 is begin read(chan, addr, reg, 1, error); return uint8(i2c_port(chan).b(0)); end; -- -- When reading or writing two bytes, is the MSB first or second? There is -- no standard even within a single device. -- -- Read a word with MSB first -- function readm1(chan : port_id; addr : addr7; reg : uint8; error : out err_code) return UInt16 is begin read(chan, addr, reg, 2, error); return UInt16(i2c_port(chan).b(0))*256 + UInt16(i2c_port(chan).b(1)); end; -- -- Read a word with MSB second (LSB first) -- function readm2(chan : port_id; addr : addr7; reg : uint8; error : out err_code) return UInt16 is begin read(chan, addr, reg, 2, error); return UInt16(i2c_port(chan).b(1))*256 + UInt16(i2c_port(chan).b(0)); end; -- -- Read the specified number of bytes into the device buffer -- procedure read(chan : port_id; addr : addr7; reg : uint8; size : buff_index; error : out err_code) is count : buff_index := 0; begin if (addr < 16#0E#) or (addr > 16#77#) then error := invalid_addr; return; end if; Ada.Synchronous_Task_Control.Suspend_Until_True(i2c_port(chan).not_busy); buff(chan).rx_read(addr, reg, size); Ada.Synchronous_Task_Control.Suspend_Until_True(i2c_port(chan).not_busy); error := buff(chan).get_error; delay until Ada.Real_Time.Clock + i2c_delay; Ada.Synchronous_Task_Control.Set_True(i2c_port(chan).not_busy); end; -- -- -- Object oriented interface -- -- -- Write a byte to a specified register on an I2C device. -- procedure write(self : in out due_i2c_interface_record; addr : addr7; reg : uint8; data : uint8; error : out err_code) is status : SAM3x8e.TWI.TWI0_SR_Register; begin if (addr < 16#0E#) or (addr > 16#77#) then error := invalid_addr; return; end if; self.activity := self.activity + 1; Ada.Synchronous_Task_Control.Suspend_Until_True(self.not_busy); self.port.CR.MSEN := 1; -- Enable master mode self.port.CR.SVDIS := 1; -- Disable slave mode self.port.MMR.MREAD := 0; -- Master write self.port.MMR.IADRSZ := SAM3x8e.TWI.Val_1_Byte; -- Register addresses are 1 byte; self.port.MMR.DADR := SAM3x8e.UInt7(addr); self.port.IADR.IADR := SAM3x8e.UInt24(reg); self.port.THR.TXDATA := SAM3x8e.Byte(data); self.port.CR.STOP := 1; loop status := self.port.SR; exit when status.TXRDY = 1; exit when status.NACK = 1; exit when status.OVRE = 1; end loop; if status.NACK = 1 then error := nack; elsif status.OVRE = 1 then error := ovre; else error := none; end if; delay until Ada.Real_Time.Clock + i2c_delay; Ada.Synchronous_Task_Control.Set_True(self.not_busy); end write; -- -- Write an arbitrary number of bytes to a device on the i2c bus. -- procedure write(self : in out due_i2c_interface_record; addr : addr7; reg : uint8; size : buff_index; error : out err_code) is status : SAM3x8e.TWI.TWI0_SR_Register; index : buff_index := 0; begin if (addr < 16#0E#) or (addr > 16#77#) then error := invalid_addr; return; end if; Ada.Synchronous_Task_Control.Suspend_Until_True(self.not_busy); while index < size loop self.activity := self.activity + 1; self.port.CR.MSEN := 1; -- Enable master mode self.port.CR.SVDIS := 1; -- Disable slave mode self.port.MMR.MREAD := 0; -- Master write self.port.MMR.IADRSZ := SAM3x8e.TWI.Val_1_Byte; -- Register addresses are 1 byte; self.port.MMR.DADR := SAM3x8e.UInt7(addr); self.port.IADR.IADR := SAM3x8e.UInt24(reg); self.port.THR.TXDATA := SAM3x8e.Byte(self.b(index)); index := index + 1; if index = size then self.port.CR.STOP := 1; end if; loop status := self.port.SR; exit when status.TXRDY = 1; exit when status.NACK = 1; exit when status.OVRE = 1; end loop; if status.NACK = 1 then error := nack; elsif status.OVRE = 1 then error := ovre; else error := none; end if; end loop; delay until Ada.Real_Time.Clock + i2c_delay; Ada.Synchronous_Task_Control.Set_True(self.not_busy); end; -- -- All the read functions use the block read procedure and return the -- specified data. -- function read(self : in out due_i2c_interface_record; addr : addr7; reg : uint8; error : out err_code) return uint8 is begin self.read(addr, reg, 1, error); return self.b(0); end read; -- -- When reading two bytes, is the MSB first or second? There is no standard -- even within a single device. -- -- Read a word with MSB first -- function readm1(self : in out due_i2c_interface_record; addr : addr7; reg : uint8; error : out err_code) return UInt16 is begin self.read(addr, reg, 2, error); return UInt16(self.b(0))*256 + UInt16(self.b(1)); end; -- -- Read a word with MSB second (LSB first) -- function readm2(self : in out due_i2c_interface_record; addr : addr7; reg : uint8; error : out err_code) return UInt16 is begin self.read(addr, reg, 2, error); return UInt16(self.b(1))*256 + UInt16(self.b(0)); end; -- -- Write a word with MSB first. -- procedure writem1(self : in out i2c_interface_record; addr : addr7; reg : uint8; data : uint16; error : out err_code) is begin self.b(0) := uint8((data/256) and 16#FF#); self.b(1) := uint8(data and 16#FF#); self.write(addr, reg, buff_index(2), error); end; -- -- Write a word with MSB second (LSB first). -- procedure writem2(self : in out i2c_interface_record; addr : addr7; reg : uint8; data : uint16; error : out err_code) is begin self.b(0) := uint8(data and 16#FF#); self.b(1) := uint8((data/256) and 16#FF#); self.write(addr, reg, buff_index(2), error); end; -- -- Read the specified number of bytes into a buffer -- procedure read(self : in out due_i2c_interface_record; addr : addr7; reg : uint8; size : buff_index; error : out err_code) is status : SAM3x8e.TWI.TWI0_SR_Register; -- s : constant BBS.embed.due.serial.int.serial_port := BBS.embed.due.serial.int.get_port(0); begin if (addr < 16#0E#) or (addr > 16#77#) then error := invalid_addr; return; end if; -- s.put_line("read: Waiting for driver to be ready."); Ada.Synchronous_Task_Control.Suspend_Until_True(self.not_busy); self.handle.rx_read(addr, reg, size); status := self.handle.get_status; -- s.put_line("read: Waiting for transaction to finish."); Ada.Synchronous_Task_Control.Suspend_Until_True(self.not_busy); status := self.handle.get_saved_status; error := self.handle.get_error; -- s.put_line("read: Exiting."); delay until Ada.Real_Time.Clock + i2c_delay; Ada.Synchronous_Task_Control.Set_True(self.not_busy); end read; -- -- Get the activity counter -- function get_activity(self : in out due_i2c_interface_record) return uint32 is begin return self.activity; end; -- -- Get busy status -- function is_busy(self : in out due_i2c_interface_record) return Boolean is begin return not Ada.Synchronous_Task_Control.Current_State(self.not_busy); end; -- -- ------------------------------------------------------------------------- -- A protected type defining the transmit and receive buffers as well as an -- interface to the buffers. This is based on the serial port handler, but -- is a bit simpler since (a) tx and rx is not simultaneous, so only one -- buffer is needed, and (b) communications are more transaction/block -- oriented so the user only needs to be notified when the exchange is -- completed. -- protected body handler is -- -- Set the address to the device record. This only needs to be called -- once during initialization/configuration. -- procedure set_interface(d : due_i2c_interface) is begin device := d; end; -- -- Functions to return statuses -- function is_busy return Boolean is begin return not not_busy; end; -- function get_status return SAM3x8e.TWI.TWI0_SR_Register is begin return device.port.SR; end; -- -- Entry point to transmit a character. Per Ravenscar, there can be -- only one entry. -- entry send(addr : addr7; reg : uint8; size : buff_index) when not_busy is begin not_busy := False; bytes := size; index := 0; end; -- -- Procedure to read a specified number of characters into a buffer. -- Calls to this procedure need to be synchronized using -- susp_not_busy. -- procedure rx_read(addr : addr7; reg : uint8; size : buff_index) is begin not_busy := False; err := none; bytes := size; complete := False; index := 0; -- -- Disable interrupts -- device.port.IDR.TXRDY := 1; device.port.IDR.SVACC := 1; device.port.IDR.GACC := 1; device.port.IDR.OVRE := 1; device.port.IDR.NACK := 1; device.port.IDR.ARBLST := 1; device.port.IDR.SCL_WS := 1; device.port.IDR.ENDRX := 1; device.port.IDR.ENDTX := 1; device.port.IDR.RXBUFF := 1; device.port.IDR.TXBUFE := 1; -- -- Enable interrupts -- device.port.IER.RXRDY := 1; device.port.IER.TXCOMP := 1; -- device.port.CR.MSEN := 1; -- Enable master mode device.port.CR.SVDIS := 1; -- Disable slave mode device.port.MMR.MREAD := 1; -- Master read device.port.MMR.IADRSZ := SAM3x8e.TWI.Val_1_Byte; -- Register addresses are 1 byte; device.port.MMR.DADR := SAM3x8e.UInt7(addr); device.port.IADR.IADR := SAM3x8e.UInt24(reg); device.port.CR.START := 1; if size = 1 then device.port.CR.STOP := 1; end if; end; -- -- Return the error code, if any. -- function get_error return err_code is begin return err; end; function get_complete return Boolean is begin return complete; end; function get_saved_status return SAM3x8e.TWI.TWI0_SR_Register is begin return status; end; -- -- This is the interrupt handler. There are three different things that -- may cause an interrupt: -- Transmitter ready: Currently does nothing. -- -- Receiver ready: Add characters to the buffer. -- -- Transmit complete: -- procedure int_handler is begin status := device.port.SR; device.activity := device.activity + 1; if status.NACK = 1 then err := nack; elsif status.OVRE = 1 then err := ovre; end if; -- -- Currently transmit ready does nothing. -- if status.TXRDY = 1 then null; end if; -- -- Check for receiver ready and add new data to the buffer. -- if status.RXRDY = 1 then device.b(index) := uint8(device.port.RHR.RXDATA); index := index + 1; if index = bytes then device.port.CR.STOP := 1; Ada.Synchronous_Task_Control.Set_True(device.not_busy); end if; end if; -- -- Check for transmitter empty. From the flowcharts in the datasheet, -- this interrupt should also fire at the end of a read operation. -- if (status.TXCOMP = 1) then device.port.IDR.TXCOMP := 1; device.port.IDR.RXRDY := 1; device.port.IDR.OVRE := 1; device.port.IDR.NACK := 1; Ada.Synchronous_Task_Control.Set_True(device.not_busy); not_busy := True; complete := True; end if; end int_handler; end handler; -- end bbs.embed.i2c.due;