Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

365 lines
8.2KB

  1. //go:build pluto && linux
  2. package plutosdr
  3. /*
  4. #cgo pkg-config: libiio
  5. #include <iio.h>
  6. #include <stdlib.h>
  7. #include <stdint.h>
  8. */
  9. import "C"
  10. import (
  11. "context"
  12. "fmt"
  13. "log"
  14. "sync"
  15. "sync/atomic"
  16. "time"
  17. "unsafe"
  18. "github.com/jan/fm-rds-tx/internal/output"
  19. "github.com/jan/fm-rds-tx/internal/platform"
  20. )
  21. type PlutoDriver struct {
  22. mu sync.Mutex
  23. cfg platform.SoapyConfig
  24. ctx *C.struct_iio_context
  25. txDev *C.struct_iio_device
  26. phyDev *C.struct_iio_device
  27. chanI *C.struct_iio_channel
  28. chanQ *C.struct_iio_channel
  29. chanLO *C.struct_iio_channel
  30. buf *C.struct_iio_buffer
  31. bufSize int
  32. started bool
  33. configured bool
  34. framesWritten atomic.Uint64
  35. samplesWritten atomic.Uint64
  36. underruns atomic.Uint64
  37. lastError string
  38. lastErrorAt string
  39. layoutLogged bool
  40. }
  41. func NewPlutoDriver() platform.SoapyDriver {
  42. return &PlutoDriver{}
  43. }
  44. func (d *PlutoDriver) Name() string { return "pluto-iio" }
  45. func (d *PlutoDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
  46. d.mu.Lock()
  47. defer d.mu.Unlock()
  48. d.cleanup()
  49. d.cfg = cfg
  50. uri := "usb:"
  51. if cfg.Device != "" && cfg.Device != "plutosdr" {
  52. uri = cfg.Device
  53. }
  54. if v, ok := cfg.DeviceArgs["uri"]; ok && v != "" {
  55. uri = v
  56. }
  57. cURI := C.CString(uri)
  58. defer C.free(unsafe.Pointer(cURI))
  59. ctx := C.iio_create_context_from_uri(cURI)
  60. if ctx == nil {
  61. return fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
  62. }
  63. d.ctx = ctx
  64. txDev := d.findDevice("cf-ad9361-dds-core-lpc")
  65. if txDev == nil {
  66. return fmt.Errorf("pluto: TX device 'cf-ad9361-dds-core-lpc' not found")
  67. }
  68. d.txDev = txDev
  69. phyDev := d.findDevice("ad9361-phy")
  70. if phyDev == nil {
  71. return fmt.Errorf("pluto: PHY device 'ad9361-phy' not found")
  72. }
  73. d.phyDev = phyDev
  74. phyChanTX := d.findChannel(phyDev, "voltage3", true)
  75. if phyChanTX == nil {
  76. phyChanTX = d.findChannel(phyDev, "voltage0", true)
  77. }
  78. if phyChanTX == nil {
  79. return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
  80. }
  81. rate := int64(cfg.SampleRateHz)
  82. if rate < 2084000 {
  83. rate = 2084000
  84. }
  85. d.cfg.SampleRateHz = float64(rate)
  86. if err := d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate); err != nil {
  87. return err
  88. }
  89. bw := rate
  90. if bw > 2000000 {
  91. bw = 2000000
  92. }
  93. if err := d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw); err != nil {
  94. return err
  95. }
  96. phyChanLO := d.findChannel(phyDev, "altvoltage1", true)
  97. d.chanLO = phyChanLO
  98. if phyChanLO != nil {
  99. freqHz := int64(cfg.CenterFreqHz)
  100. if freqHz <= 0 {
  101. freqHz = 100000000
  102. }
  103. if err := d.writeChanAttrLL(phyChanLO, "frequency", freqHz); err != nil {
  104. return err
  105. }
  106. }
  107. attenDB := int64(0)
  108. if cfg.GainDB > 0 {
  109. attenDB = -int64(89 - cfg.GainDB)
  110. if attenDB > 0 {
  111. attenDB = 0
  112. }
  113. if attenDB < -89 {
  114. attenDB = -89
  115. }
  116. }
  117. _ = d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000)
  118. chanI := d.findChannel(txDev, "voltage0", true)
  119. chanQ := d.findChannel(txDev, "voltage1", true)
  120. if chanI == nil || chanQ == nil {
  121. return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
  122. }
  123. C.iio_channel_enable(chanI)
  124. C.iio_channel_enable(chanQ)
  125. d.chanI = chanI
  126. d.chanQ = chanQ
  127. d.bufSize = int(rate) / 20
  128. if d.bufSize < 4096 {
  129. d.bufSize = 4096
  130. }
  131. buf := C.iio_device_create_buffer(txDev, C.size_t(d.bufSize), C.bool(false))
  132. if buf == nil {
  133. return fmt.Errorf("pluto: failed to create TX buffer (size=%d)", d.bufSize)
  134. }
  135. d.buf = buf
  136. d.configured = true
  137. return nil
  138. }
  139. func (d *PlutoDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
  140. return platform.DeviceCaps{
  141. MinSampleRate: 521e3,
  142. MaxSampleRate: 61.44e6,
  143. HasGain: true,
  144. GainMinDB: -89,
  145. GainMaxDB: 0,
  146. Channels: []int{0},
  147. }, nil
  148. }
  149. func (d *PlutoDriver) Start(_ context.Context) error {
  150. d.mu.Lock()
  151. defer d.mu.Unlock()
  152. if !d.configured {
  153. return fmt.Errorf("pluto: not configured")
  154. }
  155. if d.started {
  156. return fmt.Errorf("pluto: already started")
  157. }
  158. d.started = true
  159. return nil
  160. }
  161. func (d *PlutoDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
  162. d.mu.Lock()
  163. buf := d.buf
  164. chanI := d.chanI
  165. chanQ := d.chanQ
  166. started := d.started
  167. bufSize := d.bufSize
  168. d.mu.Unlock()
  169. if !started || buf == nil {
  170. return 0, fmt.Errorf("pluto: not active")
  171. }
  172. if frame == nil || len(frame.Samples) == 0 {
  173. return 0, nil
  174. }
  175. written := 0
  176. total := len(frame.Samples)
  177. for written < total {
  178. chunk := total - written
  179. if chunk > bufSize {
  180. chunk = bufSize
  181. }
  182. step := uintptr(C.iio_buffer_step(buf))
  183. if step == 0 {
  184. return written, fmt.Errorf("pluto: buffer step is 0")
  185. }
  186. ptrI := uintptr(C.iio_buffer_first(buf, chanI))
  187. ptrQ := uintptr(C.iio_buffer_first(buf, chanQ))
  188. if ptrI == 0 || ptrQ == 0 {
  189. return written, fmt.Errorf("pluto: buffer_first returned null")
  190. }
  191. end := uintptr(C.iio_buffer_end(buf))
  192. d.mu.Lock()
  193. if !d.layoutLogged {
  194. delta := int64(ptrQ) - int64(ptrI)
  195. span := int64(0)
  196. if end > ptrI {
  197. span = int64(end - ptrI)
  198. }
  199. log.Printf("pluto-linux: buffer layout step=%d ptrI=%#x ptrQ=%#x delta=%d end=%#x span=%d bufSize=%d", step, ptrI, ptrQ, delta, end, span, bufSize)
  200. d.layoutLogged = true
  201. }
  202. d.mu.Unlock()
  203. if end > 0 {
  204. bufSamples := int((end - ptrI) / step)
  205. if bufSamples > 0 && chunk > bufSamples {
  206. chunk = bufSamples
  207. }
  208. }
  209. for i := 0; i < chunk; i++ {
  210. s := frame.Samples[written+i]
  211. *(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
  212. *(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
  213. ptrI += step
  214. ptrQ += step
  215. }
  216. pushed := int(C.iio_buffer_push(buf))
  217. if pushed < 0 {
  218. d.mu.Lock()
  219. d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
  220. d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
  221. d.underruns.Add(1)
  222. d.mu.Unlock()
  223. return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
  224. }
  225. written += chunk
  226. }
  227. d.framesWritten.Add(1)
  228. d.samplesWritten.Add(uint64(written))
  229. return written, nil
  230. }
  231. func (d *PlutoDriver) Stop(_ context.Context) error {
  232. d.mu.Lock()
  233. defer d.mu.Unlock()
  234. d.started = false
  235. return nil
  236. }
  237. func (d *PlutoDriver) Flush(_ context.Context) error { return nil }
  238. func (d *PlutoDriver) Tune(_ context.Context, freqHz float64) error {
  239. d.mu.Lock()
  240. defer d.mu.Unlock()
  241. if !d.configured || d.chanLO == nil {
  242. return fmt.Errorf("pluto: not configured or LO channel not available")
  243. }
  244. return d.writeChanAttrLL(d.chanLO, "frequency", int64(freqHz))
  245. }
  246. func (d *PlutoDriver) Close(_ context.Context) error {
  247. d.mu.Lock()
  248. defer d.mu.Unlock()
  249. d.started = false
  250. d.cleanup()
  251. return nil
  252. }
  253. func (d *PlutoDriver) Stats() platform.RuntimeStats {
  254. d.mu.Lock()
  255. defer d.mu.Unlock()
  256. return platform.RuntimeStats{
  257. TXEnabled: d.started,
  258. StreamActive: d.started && d.buf != nil,
  259. FramesWritten: d.framesWritten.Load(),
  260. SamplesWritten: d.samplesWritten.Load(),
  261. Underruns: d.underruns.Load(),
  262. LastError: d.lastError,
  263. LastErrorAt: d.lastErrorAt,
  264. EffectiveRate: d.cfg.SampleRateHz,
  265. }
  266. }
  267. func (d *PlutoDriver) cleanup() {
  268. if d.buf != nil {
  269. C.iio_buffer_destroy(d.buf)
  270. d.buf = nil
  271. }
  272. if d.chanI != nil {
  273. C.iio_channel_disable(d.chanI)
  274. d.chanI = nil
  275. }
  276. if d.chanQ != nil {
  277. C.iio_channel_disable(d.chanQ)
  278. d.chanQ = nil
  279. }
  280. d.chanLO = nil
  281. if d.ctx != nil {
  282. C.iio_context_destroy(d.ctx)
  283. d.ctx = nil
  284. }
  285. d.txDev = nil
  286. d.phyDev = nil
  287. d.configured = false
  288. d.layoutLogged = false
  289. }
  290. func (d *PlutoDriver) findDevice(name string) *C.struct_iio_device {
  291. if d.ctx == nil {
  292. return nil
  293. }
  294. cName := C.CString(name)
  295. defer C.free(unsafe.Pointer(cName))
  296. return C.iio_context_find_device(d.ctx, cName)
  297. }
  298. func (d *PlutoDriver) findChannel(dev *C.struct_iio_device, name string, isOutput bool) *C.struct_iio_channel {
  299. if dev == nil {
  300. return nil
  301. }
  302. cName := C.CString(name)
  303. defer C.free(unsafe.Pointer(cName))
  304. if isOutput {
  305. return C.iio_device_find_channel(dev, cName, C.bool(true))
  306. }
  307. return C.iio_device_find_channel(dev, cName, C.bool(false))
  308. }
  309. func (d *PlutoDriver) writeChanAttrLL(ch *C.struct_iio_channel, attr string, val int64) error {
  310. if ch == nil {
  311. return fmt.Errorf("pluto: channel missing for attr %s", attr)
  312. }
  313. cAttr := C.CString(attr)
  314. defer C.free(unsafe.Pointer(cAttr))
  315. ret := C.iio_channel_attr_write_longlong(ch, cAttr, C.longlong(val))
  316. if ret < 0 {
  317. return fmt.Errorf("pluto: write attr %s failed (rc=%d)", attr, int(ret))
  318. }
  319. return nil
  320. }