aboutsummaryrefslogtreecommitdiff
path: root/hsm-pwm/Hsm/PWM.hs
blob: 2fd5955761d7a2310b1889e5de3e8aaef5f49c08 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

-- Raspberry Pi 5 PWM control (sysfs interface)
--
-- Requires:
-- - `dtoverlay=pwm-2chan` in `/boot/config.txt`
-- - UDEV rules for non-root access
--
-- Supports 2 active PWM channels. For details see:
-- - PWM Configuration: https://www.pi4j.com/blog/2024/20240423_pwm_rpi5/#modify-configtxt
-- - SysFS Reference: https://forums.raspberrypi.com/viewtopic.php?t=359251
-- - UDEV Setup: https://forums.raspberrypi.com/viewtopic.php?t=316514
module Hsm.PWM
  ( PWMChannel(..)
  , PWM
  , setCycleDuration
  , runPWM
  ) where

import Control.Concurrent (threadDelay)
import Control.Monad (forM_)
import Control.Monad.Loops (untilM_)
import Effectful (Dispatch(Static), DispatchOf, Eff, IOE, (:>), liftIO)
import Effectful.Dispatch.Static (SideEffects(WithSideEffects), StaticRep, evalStaticRep, unsafeEff_)
import Effectful.Exception (bracket_)
import Hsm.Core.Serial (makeSerial)
import Hsm.Log (Log, Severity(Info, Trace), logMsg)
import System.FilePath ((</>))
import System.Posix.Files (fileAccess)

$(makeSerial "PWM" "Channel" "channelIndex" ''Int [1, 2])

data PWM (a :: * -> *) (b :: *)

type instance DispatchOf PWM = Static WithSideEffects

newtype instance StaticRep PWM =
  PWM ()

chipPath :: FilePath
chipPath = "/sys/class/pwm/pwmchip0"

channelPath :: PWMChannel -> FilePath
channelPath channel = chipPath </> "pwm" <> show (channelIndex channel)

enablePath :: PWMChannel -> FilePath
enablePath channel = channelPath channel </> "enable"

periodPath :: PWMChannel -> FilePath
periodPath channel = channelPath channel </> "period"

dutyCyclePath :: PWMChannel -> FilePath
dutyCyclePath channel = channelPath channel </> "duty_cycle"

setEnable :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Bool -> Eff es ()
setEnable channel enable = do
  logMsg Trace $ "Setting " <> enablePath channel <> " to " <> show enable
  unsafeEff_ . writeFile (enablePath channel) . show $ fromEnum enable

setPeriod :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Int -> Eff es ()
setPeriod channel period = do
  logMsg Trace $ "Setting " <> periodPath channel <> " to " <> show period
  unsafeEff_ . writeFile (periodPath channel) $ show period

setDutyCycle :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Int -> Eff es ()
setDutyCycle channel dutyCycle = do
  logMsg Trace $ "Setting " <> dutyCyclePath channel <> " to " <> show dutyCycle
  unsafeEff_ . writeFile (dutyCyclePath channel) $ show dutyCycle

-- Sets the PWM cycle duration (period) for a channel
--
-- - Special case: A duration of 0 halts PWM output
-- - Normal operation:
--   1. Zero the duty cycle first to avoid 'Invalid argument' errors
--      (period cannot be smaller than current duty cycle)
--   2. Update period
--   3. Set default 50% duty cycle
--   4. Re-enable output
setCycleDuration :: (Log "pwm" :> es, PWM :> es) => PWMChannel -> Int -> Eff es ()
setCycleDuration channel 0 = do
  logMsg Trace $ "Halting PWM signals on channel " <> show channel
  setEnable channel False
setCycleDuration channel cycleDuration = do
  logMsg Trace $ "Setting cycle duration on channel " <> show channel <> " to " <> show cycleDuration
  setEnable channel False
  setDutyCycle channel 0
  setPeriod channel cycleDuration
  setDutyCycle channel $ cycleDuration `div` 2
  setEnable channel True

runPWM :: (IOE :> es, Log "pwm" :> es) => Eff (PWM : es) a -> Eff es a
runPWM = evalStaticRep (PWM ()) . bracket_ pwmAlloc pwmDealloc
  where
    exportPath = chipPath </> "export"
    unexportPath = chipPath </> "unexport"
    -- Blocks until the PWM sysfs file becomes writable
    --
    -- Handles the race condition caused by:
    -- 1. Sysfs file creation
    -- 2. UDEV rules (async `chown` to `pwm` group)
    --
    -- Polls permissions with 1ms delay between checks.
    waitWritable path = do
      logMsg Info $ "Waiting for " <> path <> " to become writable"
      liftIO . untilM_ (threadDelay 1000) $ fileAccess path False True False
    allChannels = [minBound .. maxBound]
    pwmAlloc = do
      waitWritable exportPath
      waitWritable unexportPath
      forM_ allChannels $ \channel -> do
        logMsg Info $ "Exporting channel " <> show channel <> " on chip " <> chipPath
        liftIO . writeFile exportPath $ show (channelIndex channel)
        waitWritable $ enablePath channel
        waitWritable $ periodPath channel
        waitWritable $ dutyCyclePath channel
        setCycleDuration channel 0
    pwmDealloc =
      forM_ allChannels $ \channel -> do
        setEnable channel False
        logMsg Info $ "Unexporting channel " <> show channel <> " on chip " <> chipPath
        liftIO . writeFile unexportPath $ show (channelIndex channel)