ftdi-0.3.0.2: System/FTDI/MPSSE.hs
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE BangPatterns #-}
module System.FTDI.MPSSE
( Command
, run
, Failure(..)
-- * Clock divisor
, setClockDivisor
-- ** FT232H divide-by-5
, enableClkDivBy5
, disableClkDivBy5
, enable3PhaseClocking
, disable3PhaseClocking
-- * Loopback
, enableLoopback
, disableLoopback
-- * Data transfer
, BitOrder(..)
, ClockEdge(..)
, flush
-- ** Pausing
, waitOnHigh
, waitOnLow
-- ** Byte-wise
, readBytes
, writeBytes
, readWriteBytes
-- * GPIO
, Gpios(..)
, allInputs
, Direction(..)
, GpioBank(..)
, setGpioDirValue
, getGpioValue
) where
import Data.Bits
import Data.Word
import Numeric (showHex)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Builder as BSB
import Control.Concurrent.Async
import Control.Monad (void)
import qualified System.FTDI as FTDI
import System.FTDI (InterfaceHandle)
import System.IO
debug :: Bool
debug = False
debugLog :: String -> IO ()
debugLog
| debug = hPutStrLn stderr
| otherwise = const $ return ()
-- Useful for debugging
showBS :: BS.ByteString -> String
showBS = foldr (\n rest -> showHex n . showChar ' ' $ rest) "" . BS.unpack
data Command a = Command { command :: BSB.Builder
, expectedBytes :: !Int
, parseBytes :: BS.ByteString -> a
}
instance Functor Command where
fmap f (Command a b c) = Command a b (f . c)
{-# INLINE fmap #-}
instance Applicative Command where
pure x = Command mempty 0 (const x)
{-# INLINE pure #-}
Command a b c <*> Command a' b' c' =
Command (a <> a') (b + b') parse
where
parse bs =
let (bs1, bs2) = BS.splitAt b bs
in c bs1 (c' bs2)
{-# INLINE (<*>) #-}
opCode :: Word8 -> Command ()
opCode = byte
{-# INLINE opCode #-}
byte :: Word8 -> Command ()
byte o = void $ transfer (BSB.word8 o) 0
{-# INLINE byte #-}
word16 :: Word16 -> Command ()
word16 o = void $ transfer (BSB.word16LE o) 0
{-# INLINE word16 #-}
transfer :: BSB.Builder -> Int -> Command BS.ByteString
transfer b n = Command { command = b
, expectedBytes = n
, parseBytes = id }
{-# INLINE transfer #-}
writeByteString :: BS.ByteString -> Command ()
writeByteString bs = void $ transfer (BSB.byteString bs) 0
{-# INLINE writeByteString #-}
readN :: Int -> Command BS.ByteString
readN = transfer mempty
{-# INLINE readN #-}
-------------------------------------------------------------------------------
-- Interpreter
-------------------------------------------------------------------------------
data Failure = WriteTimedOut BS.ByteString Int
-- ^ content to be written and number of bytes actually written.
| ReadTimedOut BS.ByteString Int BS.ByteString
-- ^ data written, expected returned bytes, and data actually read.
| ReadTooLong Int BS.ByteString
-- ^ bytes expected and content actually read.
| BadStatus BS.ByteString
instance Show Failure where
show (WriteTimedOut write written) =
unlines [ "Write timed out:"
, " Wrote " <> show written <> " of " <> show (BS.length write) <> ": " <> showBS write
]
show (ReadTimedOut written expected readBS) =
unlines [ "Read timed out:"
, " Wrote " <> show (BS.length written) <> ": " <> showBS written
, " Expected to read " <> show expected
, " Actually read " <> show (BS.length readBS) <> ": " <> showBS readBS
]
show (ReadTooLong expected readBS) =
unlines [ "Read too long:"
, " Expected to read " <> show expected
, " Actually read " <> show (BS.length readBS) <> ": " <> showBS readBS
]
show (BadStatus status) =
unlines [ "Bad status"
, " Status: " <> showBS status
]
-- | Assumes that the interface has already been placed in 'BitMode_MPSSE'
-- using 'setBitMode'.
run :: forall a. InterfaceHandle -> Command a -> IO (Either Failure a)
run ifHnd (Command cmd n parse) = do
let cmd' = BSL.toStrict $ BSB.toLazyByteString cmd
debugLog $ "W ("++show n++"): " ++ showBS cmd'
writer <- async $ FTDI.writeBulk ifHnd cmd'
link writer
let readLoop :: Int -> BS.ByteString -> IO (Either Failure a)
readLoop iters acc
| remain < 0 = return $ Left $ ReadTooLong n acc
| remain == 0 = return $ Right $ parse acc
| otherwise = do
(resp, _readStatus) <- FTDI.readBulk ifHnd (remain+2)
debugLog $ "R " ++ show (BS.length acc) ++ "/" ++ show n ++ ": " ++ showBS resp
let acc' = acc <> BS.drop 2 resp
statusOnly = BS.length resp == 2
iters' = if statusOnly then iters + 1 else iters
if | BS.take 2 resp == "\xfa" -> return $ Left $ BadStatus resp
| iters == 10 -> return $ Left $ ReadTimedOut cmd' n acc
| otherwise -> readLoop iters' acc'
where remain = n - BS.length acc
resp <- readLoop 0 mempty
(written, _writeStatus) <- wait writer
return $ if written /= BS.length cmd'
then Left $ WriteTimedOut cmd' written
else resp
{-# INLINE run #-}
-------------------------------------------------------------------------------
-- Clocking
-------------------------------------------------------------------------------
setClockDivisor :: Word16 -> Command ()
setClockDivisor n = opCode 0x86 *> word16 n
{-# INLINE setClockDivisor #-}
-- | The FT232H, FT2232H, and FT4232H can achieve higher data rates if the
-- clock divider is disabled.
disableClkDivBy5 :: Command ()
disableClkDivBy5 = opCode 0x8a
-- | Enable clock divide by 5 to allow for backward compatibility with FT2232D.
enableClkDivBy5 :: Command ()
enableClkDivBy5 = opCode 0x8b
-- | Enables 3 phase data clocking.
-- Used by I2C interfaces to allow data on both clock edges.
enable3PhaseClocking :: Command ()
enable3PhaseClocking = opCode 0x8c
-- | Disables 3 phase data clocking.
disable3PhaseClocking :: Command ()
disable3PhaseClocking = opCode 0x8d
-------------------------------------------------------------------------------
-- Loopback
-------------------------------------------------------------------------------
enableLoopback :: Command ()
enableLoopback = opCode 0x84
{-# INLINE enableLoopback #-}
disableLoopback :: Command ()
disableLoopback = opCode 0x85
{-# INLINE disableLoopback #-}
-------------------------------------------------------------------------------
-- GPIO
-------------------------------------------------------------------------------
data Gpios a = Gpios { gpio0 :: a -- ^ BankL: TXD, clock
, gpio1 :: a -- ^ BankL: RXD, TDI, MOSI
, gpio2 :: a -- ^ BankL: RTS#, TDO, MISO
, gpio3 :: a -- ^ BankL: CTS#, TMS, CS
, gpio4 :: a
, gpio5 :: a
, gpio6 :: a
, gpio7 :: a
}
deriving (Functor, Foldable, Traversable)
data Direction i o = Input i | Output o
data GpioBank = BankL | BankH
allInputs :: Gpios (Direction () Bool)
allInputs = Gpios i i i i i i i i
where i = Input ()
gpioBits :: Gpios Bool -> Word8
gpioBits Gpios{..} =
b 0 gpio0 .|.
b 1 gpio1 .|.
b 2 gpio2 .|.
b 3 gpio3 .|.
b 4 gpio4 .|.
b 5 gpio5 .|.
b 6 gpio6 .|.
b 7 gpio7
where b n True = bit n
b _ False = 0
-- | Set the direction and logic state of the pins
setGpioDirValue :: GpioBank -> Gpios (Direction () Bool) -> Command ()
setGpioDirValue bank vals = opCode o *> byte valueByte *> byte dirByte
where o = case bank of
BankL -> 0x80
BankH -> 0x82
!dirByte = gpioBits $ fmap f vals
where f (Output _) = True
f _ = False
!valueByte = gpioBits $ fmap f vals
where f (Output True) = True
f _ = False
-- | Read the current state of the pins in the bank and send back 1 byte
getGpioValue :: GpioBank -> Command BS.ByteString
getGpioValue BankL = opCode 0x81 *> readN 1
getGpioValue BankH = opCode 0x83 *> readN 1
-------------------------------------------------------------------------------
-- Transfers
-------------------------------------------------------------------------------
-- | This will make the chip flush its buffer back to the PC.
flush :: Command ()
flush = opCode 0x87
waitOnHigh :: Command ()
waitOnHigh = opCode 0x88
waitOnLow :: Command ()
waitOnLow = opCode 0x89
data BitOrder = MsbFirst | LsbFirst
data ClockEdge = Rising | Falling
otherEdge :: ClockEdge -> ClockEdge
otherEdge Rising = Falling
otherEdge Falling = Rising
bitOrderBit :: BitOrder -> Word8
bitOrderBit MsbFirst = 0x0
bitOrderBit LsbFirst = 0x8
outEdgeBit :: ClockEdge -> Word8
outEdgeBit Rising = 0x0
outEdgeBit Falling = 0x1
inEdgeBit :: ClockEdge -> Word8
inEdgeBit Rising = 0x0
inEdgeBit Falling = 0x4
writeBytes :: ClockEdge -> BitOrder -> BS.ByteString -> Command ()
writeBytes edge order bs
| BS.null bs = error "writeBytes: too short"
| BS.length bs > 0x10000 = error "writeBytes: too long"
| otherwise =
opCode o *> word16 (fromIntegral $ BS.length bs - 1) *> writeByteString bs
where
o = 0x10 .|. bitOrderBit order .|. outEdgeBit edge
{-# INLINE writeBytes #-}
readBytes :: ClockEdge -> BitOrder -> Int -> Command BS.ByteString
readBytes edge order n
| n == 0 = error "readBytes: too short"
| n > 0x10000 = error "readBytes: too long"
| otherwise =
opCode o
*> word16 (fromIntegral $ n - 1)
*> readN (fromIntegral n)
where
o = 0x20 .|. bitOrderBit order .|. inEdgeBit edge
{-# INLINE readBytes #-}
readWriteBytes :: ClockEdge -- ^ which edge to clock *out* data on
-> BitOrder -> BS.ByteString -> Command BS.ByteString
readWriteBytes outEdge order bs
| BS.null bs = error "readWriteBytes: too short"
| BS.length bs > 0x10000 = error "readWriteBytes: too long"
| otherwise =
opCode o
*> word16 (fromIntegral $ BS.length bs - 1)
*> transfer (BSB.byteString bs) (BS.length bs)
where
o = 0x30 .|. bitOrderBit order .|. inEdgeBit (otherEdge outEdge) .|. outEdgeBit outEdge
{-# INLINE readWriteBytes #-}