packages feed

bus-pirate-0.7.0: System/Hardware/BusPirate/I2C.hs

{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving #-}

module System.Hardware.BusPirate.I2C
  ( -- * Types
    I2cM
  , i2cMode
    -- * Bus operations
  , startBit
  , stopBit
  , readByte
  , ackBit
  , nackBit
  , bulkWrite
    -- * Configuration
  , PeripheralConfig(..)
  , setConfig
  , I2cSpeed(..)
  , setSpeed
    -- * Device addresses
  , I2CAddress
  , from7Bit
  , from8Bit
  , readAddr
  , writeAddr
    -- * Register interface
  , writeReg
  , readReg
  , readReg'
  ) where

import Control.Applicative
import Control.Monad (replicateM, void)
import Control.Monad.IO.Class
import Control.Monad.Trans.Except
import Data.Bits
import Data.Word
import Data.List (intercalate)

import qualified Data.ByteString as BS
import Data.ByteString (ByteString)

import System.Hardware.BusPirate.Core

newtype I2cM a = I2cM (BusPirateM a)
               deriving (Functor, Applicative, Monad, MonadIO)

err :: String -> I2cM a
err = I2cM . BPM . throwE

-- | Enter I2C mode and run given action
i2cMode :: I2cM a -> BusPirateM a
i2cMode (I2cM m) = commandExpect 0x2 "I2C1" >> m

-- | Send a start bit
startBit :: I2cM ()
startBit = I2cM $ command 0x2

-- | Send a stop bit
stopBit :: I2cM ()
stopBit = I2cM $ command 0x3

-- | Read a byte
readByte :: I2cM Word8
readByte = I2cM $ putByte 0x4 >> getByte

-- | Send an ACK
ackBit :: I2cM ()
ackBit = I2cM $ command 0x6

-- | Send a NACK
nackBit :: I2cM ()
nackBit = I2cM $ command 0x7

data AckNack = Ack | Nack
             deriving (Show, Eq, Ord, Enum, Bounded)

-- | Write some bytes
bulkWrite :: ByteString -> I2cM ()
bulkWrite d
  | BS.null d = return ()
  | BS.length d > 16 = err "Too many bytes"
  | otherwise = I2cM $ do
    command $ fromIntegral $ 0x10 + BS.length d - 1
    put d
    acks <- replicateM (BS.length d) $ toEnum . fromIntegral <$> getByte
    case map fst $ filter (\(n,a)->a /= Ack) $ zip [0..] acks of
      []    -> return ()
      nacks -> let nacks' = intercalate ", " $ map show nacks
                   bytes = if length nacks > 1 then "bytes" else "byte"
               in fail $ "Nack after "++bytes++" "++nacks'++" during bulkWrite of "++show d


-- | Set Bus Pirate peripheral configuration bits
setConfig :: PeripheralConfig -> I2cM ()
setConfig config = I2cM $ setPeripherals config

-- | I2C bus speed
data I2cSpeed = I2c_5kHz
              | I2c_50kHz
              | I2c_100kHz
              | I2c_400kHz
              deriving (Show, Eq, Ord, Enum, Bounded)

-- | Set I2C bus speed
setSpeed :: I2cSpeed -> I2cM ()
setSpeed speed = I2cM $ command $ fromIntegral $ 0x60 + fromEnum speed

-- | Send Start bit, write some bytes, then read some bytes (ACKing
-- each until the last), then send a stop bit
--
-- This is very likely broken as the command structure itself appears
-- to be horribly broken, requiring a conditional read of a status byte.
writeRead :: ByteString -> Int -> I2cM ByteString
writeRead send recv
  | BS.length send > 0xffff = err "Too large send request"
  | recv > 0xffff           = err "Too large recieve request"
  | otherwise               = I2cM $ do
    putByte 0x8
    putWord16 $ fromIntegral $ BS.length send
    putWord16 $ fromIntegral $ recv
    put send
    status <- getByte
    case status of
      0x00 -> fail "writeRead: Failed"
      0x01 -> get recv

-- | An I2C address (shifted 7-bit)
newtype I2CAddress = I2cAddr Word8

-- | An I2C address from a unshifted 7-bit address
from7Bit :: Word8 -> I2CAddress
from7Bit = I2cAddr . (`shiftL` 1)

-- | An I2C address from a shifted 8-bit address (masking out the read/write bit)
from8Bit :: Word8 -> I2CAddress
from8Bit = I2cAddr . (`clearBit` 0)

readAddr :: I2CAddress -> Word8
readAddr (I2cAddr n) = n + 1

writeAddr :: I2CAddress -> Word8
writeAddr (I2cAddr n) = n

type Register = Word8

-- | Perform a read of the given length starting at the given register
readReg' :: I2CAddress -> Word8 -> Int -> I2cM BS.ByteString
readReg' addr reg length = do
    startBit
    bulkWrite $ BS.pack [writeAddr addr, reg]
    writeRead (BS.singleton $ readAddr addr) length

-- | Read the given register
readReg :: I2CAddress -> Word8 -> I2cM Word8
readReg addr reg = BS.head <$> readReg' addr reg 1

-- | Perform a write to the given register
writeReg :: I2CAddress -> Word8 -> Word8 -> I2cM ()
writeReg addr reg value = do
    void $ writeRead (BS.pack [writeAddr addr, reg, value]) 0