aboutsummaryrefslogtreecommitdiff
path: root/hsm-pwm/Hsm/PWM.hs
blob: aa16d5c4b19d2ffe1410c0f30176aad3766e44c1 (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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
{-# 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_, void)
import Control.Monad.Loops (untilM_)
import Effectful (Dispatch (Static), DispatchOf, Eff, IOE, liftIO, (:>))
import Effectful.Dispatch.Static
  ( SideEffects (WithSideEffects)
  , StaticRep
  , evalStaticRep
  , unsafeEff_
  )
import Effectful.Resource (Resource, allocateEff_)
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, Resource :> es)
  => Eff (PWM : es) a
  -> Eff es a
runPWM action =
  evalStaticRep (PWM ()) $ do
    void $ allocateEff_ pwmAlloc pwmDealloc
    action
  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)