Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

524 lines
13KB

  1. //go:build pluto && windows
  2. // Package plutosdr provides a direct libiio-based TX driver for ADALM-Pluto.
  3. // Pure Go on Windows — loads libiio.dll via syscall.LoadLibrary at runtime.
  4. // No CGO, no C compiler required.
  5. //
  6. // Build: go build -tags pluto ./cmd/fmrtx
  7. // Requires: libiio installed (https://github.com/analogdevicesinc/libiio/releases)
  8. package plutosdr
  9. import (
  10. "context"
  11. "fmt"
  12. "sync"
  13. "sync/atomic"
  14. "syscall"
  15. "time"
  16. "unsafe"
  17. "github.com/jan/fm-rds-tx/internal/output"
  18. "github.com/jan/fm-rds-tx/internal/platform"
  19. )
  20. // iioLib holds function pointers loaded from libiio.dll
  21. type iioLib struct {
  22. dll *syscall.DLL
  23. // Context
  24. pCreateCtxFromURI *syscall.Proc
  25. pDestroyCtx *syscall.Proc
  26. // Device
  27. pCtxFindDevice *syscall.Proc
  28. pDeviceFindChannel *syscall.Proc
  29. pChannelEnable *syscall.Proc
  30. pChannelDisable *syscall.Proc
  31. pChannelIsEnabled *syscall.Proc
  32. // Attributes
  33. pChannelAttrWriteLongLong *syscall.Proc
  34. pChannelAttrWriteBool *syscall.Proc
  35. pDeviceAttrWriteLongLong *syscall.Proc
  36. // Buffer
  37. pCreateBuffer *syscall.Proc
  38. pDestroyBuffer *syscall.Proc
  39. pBufferPush *syscall.Proc
  40. pBufferStep *syscall.Proc
  41. pBufferStart *syscall.Proc
  42. pBufferEnd *syscall.Proc
  43. pBufferFirst *syscall.Proc
  44. }
  45. var dllSearchPaths = []string{
  46. "libiio",
  47. "iio",
  48. `C:\Program Files\libiio\libiio.dll`,
  49. `C:\Program Files (x86)\libiio\libiio.dll`,
  50. }
  51. func loadIIOLib() (*iioLib, error) {
  52. var dll *syscall.DLL
  53. var lastErr error
  54. for _, path := range dllSearchPaths {
  55. dll, lastErr = syscall.LoadDLL(path)
  56. if dll != nil {
  57. break
  58. }
  59. }
  60. if dll == nil {
  61. return nil, fmt.Errorf("cannot load libiio.dll: %v", lastErr)
  62. }
  63. p := func(name string) *syscall.Proc {
  64. proc, _ := dll.FindProc(name)
  65. return proc
  66. }
  67. return &iioLib{
  68. dll: dll,
  69. pCreateCtxFromURI: p("iio_create_context_from_uri"),
  70. pDestroyCtx: p("iio_context_destroy"),
  71. pCtxFindDevice: p("iio_context_find_device"),
  72. pDeviceFindChannel: p("iio_device_find_channel"),
  73. pChannelEnable: p("iio_channel_enable"),
  74. pChannelDisable: p("iio_channel_disable"),
  75. pChannelIsEnabled: p("iio_channel_is_enabled"),
  76. pChannelAttrWriteLongLong: p("iio_channel_attr_write_longlong"),
  77. pChannelAttrWriteBool: p("iio_channel_attr_write_bool"),
  78. pDeviceAttrWriteLongLong: p("iio_device_attr_write_longlong"),
  79. pCreateBuffer: p("iio_device_create_buffer"),
  80. pDestroyBuffer: p("iio_buffer_destroy"),
  81. pBufferPush: p("iio_buffer_push"),
  82. pBufferStep: p("iio_buffer_step"),
  83. pBufferStart: p("iio_buffer_start"),
  84. pBufferEnd: p("iio_buffer_end"),
  85. pBufferFirst: p("iio_buffer_first"),
  86. }, nil
  87. }
  88. // --- Driver ---
  89. type PlutoDriver struct {
  90. mu sync.Mutex
  91. lib *iioLib
  92. cfg platform.SoapyConfig
  93. ctx uintptr // iio_context*
  94. txDev uintptr // iio_device* (cf-ad9361-dds-core-lpc)
  95. phyDev uintptr // iio_device* (ad9361-phy)
  96. chanI uintptr // iio_channel* TX I
  97. chanQ uintptr // iio_channel* TX Q
  98. buf uintptr // iio_buffer*
  99. bufSize int // samples per buffer push
  100. started bool
  101. configured bool
  102. framesWritten atomic.Uint64
  103. samplesWritten atomic.Uint64
  104. underruns atomic.Uint64
  105. lastError string
  106. lastErrorAt string
  107. initError string
  108. }
  109. func NewPlutoDriver() platform.SoapyDriver {
  110. lib, err := loadIIOLib()
  111. if err != nil {
  112. return &PlutoDriver{initError: err.Error()}
  113. }
  114. return &PlutoDriver{lib: lib}
  115. }
  116. func (d *PlutoDriver) Name() string { return "pluto-iio" }
  117. func (d *PlutoDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
  118. d.mu.Lock()
  119. defer d.mu.Unlock()
  120. if d.lib == nil {
  121. return fmt.Errorf("libiio not loaded: %s", d.initError)
  122. }
  123. // Cleanup existing
  124. d.cleanup()
  125. d.cfg = cfg
  126. // Create IIO context via USB
  127. uri := "usb:"
  128. if cfg.Device != "" && cfg.Device != "plutosdr" {
  129. uri = cfg.Device // allow "ip:192.168.2.1" or specific USB
  130. }
  131. ctx, err := d.createContext(uri)
  132. if err != nil {
  133. return err
  134. }
  135. d.ctx = ctx
  136. // Find TX streaming device
  137. txDev := d.findDevice("cf-ad9361-dds-core-lpc")
  138. if txDev == 0 {
  139. return fmt.Errorf("pluto: TX device 'cf-ad9361-dds-core-lpc' not found")
  140. }
  141. d.txDev = txDev
  142. // Find PHY device for configuration
  143. phyDev := d.findDevice("ad9361-phy")
  144. if phyDev == 0 {
  145. return fmt.Errorf("pluto: PHY device 'ad9361-phy' not found")
  146. }
  147. d.phyDev = phyDev
  148. // --- AD9361 PHY configuration ---
  149. // The AD9364 (PlutoSDR) has TX on voltage3 (output), not voltage0
  150. // voltage0 = RX input, voltage3 = TX output on single-channel AD9364
  151. // Find TX PHY output channel
  152. phyChanTX := d.findChannel(phyDev, "voltage3", true) // output=true
  153. if phyChanTX == 0 {
  154. // Fallback for dual-channel AD9361: try voltage0 output
  155. phyChanTX = d.findChannel(phyDev, "voltage0", true)
  156. }
  157. if phyChanTX == 0 {
  158. return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
  159. }
  160. // Sample rate — AD9361 minimum is ~2.084 MHz.
  161. // We set the hardware to this minimum and resample from composite rate.
  162. rate := int64(cfg.SampleRateHz)
  163. if rate < 2084000 {
  164. rate = 2084000 // AD9361 minimum
  165. }
  166. // Update effective rate so the engine knows the real device rate
  167. d.cfg.SampleRateHz = float64(rate)
  168. d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate)
  169. // RF bandwidth — set to match our signal bandwidth (wider than composite)
  170. bw := rate
  171. if bw > 2000000 {
  172. bw = 2000000 // 2 MHz BW is enough for FM broadcast
  173. }
  174. d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw)
  175. // TX LO frequency
  176. phyChanLO := d.findChannel(phyDev, "altvoltage1", true) // TX LO
  177. if phyChanLO != 0 {
  178. freqHz := int64(cfg.CenterFreqHz)
  179. if freqHz <= 0 {
  180. freqHz = 100000000 // 100 MHz default
  181. }
  182. d.writeChanAttrLL(phyChanLO, "frequency", freqHz)
  183. }
  184. // TX gain/attenuation
  185. // PlutoSDR TX hardwaregain: 0 dB = max power, -89.75 dB = min
  186. // Value is in dB (not millidB) as a negative number
  187. // For max power: set to 0. For safety: set to -20 or so.
  188. // cfg.GainDB from our config is 0..89 positive, we negate it and subtract from 0
  189. attenDB := int64(0) // default = max power
  190. if cfg.GainDB > 0 {
  191. // GainDB=89 means full attenuation, GainDB=0 means max power
  192. attenDB = -int64(89 - cfg.GainDB)
  193. if attenDB > 0 {
  194. attenDB = 0
  195. }
  196. if attenDB < -89 {
  197. attenDB = -89
  198. }
  199. }
  200. d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000) // millidB
  201. // --- TX streaming channels on cf-ad9361-dds-core-lpc ---
  202. // voltage0 (I) and voltage1 (Q) are output channels
  203. chanI := d.findChannel(txDev, "voltage0", true)
  204. chanQ := d.findChannel(txDev, "voltage1", true)
  205. if chanI == 0 || chanQ == 0 {
  206. return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
  207. }
  208. d.enableChannel(chanI)
  209. d.enableChannel(chanQ)
  210. d.chanI = chanI
  211. d.chanQ = chanQ
  212. // Create buffer — samples per push (per channel)
  213. // At 2.084 MHz with 50ms chunks = 104200 samples. Buffer must fit this.
  214. d.bufSize = int(rate) / 20 // 50ms worth
  215. if d.bufSize < 4096 {
  216. d.bufSize = 4096
  217. }
  218. // libiio can handle large buffers; no artificial cap needed
  219. buf := d.createBuffer(txDev, d.bufSize, false)
  220. if buf == 0 {
  221. return fmt.Errorf("pluto: failed to create TX buffer (size=%d)", d.bufSize)
  222. }
  223. d.buf = buf
  224. d.configured = true
  225. return nil
  226. }
  227. func (d *PlutoDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
  228. return platform.DeviceCaps{
  229. MinSampleRate: 521e3,
  230. MaxSampleRate: 61.44e6,
  231. HasGain: true,
  232. GainMinDB: -89,
  233. GainMaxDB: 0,
  234. Channels: []int{0},
  235. }, nil
  236. }
  237. func (d *PlutoDriver) Start(_ context.Context) error {
  238. d.mu.Lock()
  239. defer d.mu.Unlock()
  240. if !d.configured {
  241. return fmt.Errorf("pluto: not configured")
  242. }
  243. if d.started {
  244. return fmt.Errorf("pluto: already started")
  245. }
  246. d.started = true
  247. return nil
  248. }
  249. func (d *PlutoDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
  250. d.mu.Lock()
  251. lib := d.lib
  252. buf := d.buf
  253. started := d.started
  254. bufSize := d.bufSize
  255. d.mu.Unlock()
  256. if !started || buf == 0 || lib == nil {
  257. return 0, fmt.Errorf("pluto: not active")
  258. }
  259. if frame == nil || len(frame.Samples) == 0 {
  260. return 0, nil
  261. }
  262. written := 0
  263. total := len(frame.Samples)
  264. for written < total {
  265. chunk := total - written
  266. if chunk > bufSize {
  267. chunk = bufSize
  268. }
  269. // Get buffer pointers
  270. start := d.bufferStart(buf)
  271. end := d.bufferEnd(buf)
  272. step := d.bufferStep(buf)
  273. if start == 0 || end == 0 || step == 0 {
  274. return written, fmt.Errorf("pluto: invalid buffer pointers")
  275. }
  276. // Fill buffer with interleaved I/Q as int16 (PlutoSDR native format)
  277. // IQSample is {I float32, Q float32} normalized to [-1,+1]
  278. // PlutoSDR expects int16 interleaved: I0 Q0 I1 Q1 ...
  279. bufLen := (end - start) / uintptr(step)
  280. if int(bufLen) < chunk {
  281. chunk = int(bufLen)
  282. }
  283. ptr := start
  284. for i := 0; i < chunk; i++ {
  285. s := frame.Samples[written+i]
  286. // Scale float32 [-1,+1] to int16 [-32767,+32767]
  287. iVal := int16(float32(s.I) * 32767)
  288. qVal := int16(float32(s.Q) * 32767)
  289. *(*int16)(unsafe.Pointer(ptr)) = iVal
  290. *(*int16)(unsafe.Pointer(ptr + 2)) = qVal
  291. ptr += uintptr(step)
  292. }
  293. // Push buffer to hardware
  294. pushed := d.bufferPush(buf)
  295. if pushed < 0 {
  296. d.mu.Lock()
  297. d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
  298. d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
  299. d.underruns.Add(1)
  300. d.mu.Unlock()
  301. return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
  302. }
  303. written += chunk
  304. }
  305. d.framesWritten.Add(1)
  306. d.samplesWritten.Add(uint64(written))
  307. return written, nil
  308. }
  309. func (d *PlutoDriver) Stop(_ context.Context) error {
  310. d.mu.Lock()
  311. defer d.mu.Unlock()
  312. d.started = false
  313. return nil
  314. }
  315. func (d *PlutoDriver) Flush(_ context.Context) error { return nil }
  316. func (d *PlutoDriver) Close(_ context.Context) error {
  317. d.mu.Lock()
  318. defer d.mu.Unlock()
  319. d.started = false
  320. d.cleanup()
  321. return nil
  322. }
  323. func (d *PlutoDriver) Stats() platform.RuntimeStats {
  324. d.mu.Lock()
  325. defer d.mu.Unlock()
  326. return platform.RuntimeStats{
  327. TXEnabled: d.started,
  328. StreamActive: d.started && d.buf != 0,
  329. FramesWritten: d.framesWritten.Load(),
  330. SamplesWritten: d.samplesWritten.Load(),
  331. Underruns: d.underruns.Load(),
  332. LastError: d.lastError,
  333. LastErrorAt: d.lastErrorAt,
  334. EffectiveRate: d.cfg.SampleRateHz,
  335. }
  336. }
  337. // --- internal helpers ---
  338. func (d *PlutoDriver) cleanup() {
  339. if d.buf != 0 && d.lib.pDestroyBuffer != nil {
  340. d.lib.pDestroyBuffer.Call(d.buf)
  341. d.buf = 0
  342. }
  343. if d.chanI != 0 {
  344. d.disableChannel(d.chanI)
  345. d.chanI = 0
  346. }
  347. if d.chanQ != 0 {
  348. d.disableChannel(d.chanQ)
  349. d.chanQ = 0
  350. }
  351. if d.ctx != 0 && d.lib.pDestroyCtx != nil {
  352. d.lib.pDestroyCtx.Call(d.ctx)
  353. d.ctx = 0
  354. }
  355. d.configured = false
  356. }
  357. func (d *PlutoDriver) createContext(uri string) (uintptr, error) {
  358. if d.lib.pCreateCtxFromURI == nil {
  359. return 0, fmt.Errorf("iio_create_context_from_uri not found")
  360. }
  361. cURI, _ := syscall.BytePtrFromString(uri)
  362. ret, _, _ := d.lib.pCreateCtxFromURI.Call(uintptr(unsafe.Pointer(cURI)))
  363. if ret == 0 {
  364. return 0, fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
  365. }
  366. return ret, nil
  367. }
  368. func (d *PlutoDriver) findDevice(name string) uintptr {
  369. if d.lib.pCtxFindDevice == nil || d.ctx == 0 {
  370. return 0
  371. }
  372. cName, _ := syscall.BytePtrFromString(name)
  373. ret, _, _ := d.lib.pCtxFindDevice.Call(d.ctx, uintptr(unsafe.Pointer(cName)))
  374. return ret
  375. }
  376. func (d *PlutoDriver) findChannel(dev uintptr, name string, isOutput bool) uintptr {
  377. if d.lib.pDeviceFindChannel == nil || dev == 0 {
  378. return 0
  379. }
  380. cName, _ := syscall.BytePtrFromString(name)
  381. out := uintptr(0)
  382. if isOutput {
  383. out = 1
  384. }
  385. ret, _, _ := d.lib.pDeviceFindChannel.Call(dev, uintptr(unsafe.Pointer(cName)), out)
  386. return ret
  387. }
  388. func (d *PlutoDriver) enableChannel(ch uintptr) {
  389. if d.lib.pChannelEnable != nil && ch != 0 {
  390. d.lib.pChannelEnable.Call(ch)
  391. }
  392. }
  393. func (d *PlutoDriver) disableChannel(ch uintptr) {
  394. if d.lib.pChannelDisable != nil && ch != 0 {
  395. d.lib.pChannelDisable.Call(ch)
  396. }
  397. }
  398. func (d *PlutoDriver) writeChanAttrLL(ch uintptr, attr string, val int64) {
  399. if d.lib.pChannelAttrWriteLongLong == nil || ch == 0 {
  400. return
  401. }
  402. cAttr, _ := syscall.BytePtrFromString(attr)
  403. d.lib.pChannelAttrWriteLongLong.Call(ch, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  404. }
  405. func (d *PlutoDriver) writeDevAttrLL(dev uintptr, attr string, val int64) {
  406. if d.lib.pDeviceAttrWriteLongLong == nil || dev == 0 {
  407. return
  408. }
  409. cAttr, _ := syscall.BytePtrFromString(attr)
  410. d.lib.pDeviceAttrWriteLongLong.Call(dev, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  411. }
  412. func (d *PlutoDriver) createBuffer(dev uintptr, sampleCount int, cyclic bool) uintptr {
  413. if d.lib.pCreateBuffer == nil || dev == 0 {
  414. return 0
  415. }
  416. c := uintptr(0)
  417. if cyclic {
  418. c = 1
  419. }
  420. ret, _, _ := d.lib.pCreateBuffer.Call(dev, uintptr(sampleCount), c)
  421. return ret
  422. }
  423. func (d *PlutoDriver) bufferPush(buf uintptr) int {
  424. if d.lib.pBufferPush == nil || buf == 0 {
  425. return -1
  426. }
  427. ret, _, _ := d.lib.pBufferPush.Call(buf)
  428. return int(int32(ret))
  429. }
  430. func (d *PlutoDriver) bufferStart(buf uintptr) uintptr {
  431. if d.lib.pBufferStart == nil {
  432. return 0
  433. }
  434. ret, _, _ := d.lib.pBufferStart.Call(buf)
  435. return ret
  436. }
  437. func (d *PlutoDriver) bufferEnd(buf uintptr) uintptr {
  438. if d.lib.pBufferEnd == nil {
  439. return 0
  440. }
  441. ret, _, _ := d.lib.pBufferEnd.Call(buf)
  442. return ret
  443. }
  444. func (d *PlutoDriver) bufferStep(buf uintptr) uintptr {
  445. if d.lib.pBufferStep == nil {
  446. return 0
  447. }
  448. ret, _, _ := d.lib.pBufferStep.Call(buf)
  449. return ret
  450. }
  451. func (d *PlutoDriver) bufferFirst(buf uintptr, ch uintptr) uintptr {
  452. if d.lib.pBufferFirst == nil {
  453. return 0
  454. }
  455. ret, _, _ := d.lib.pBufferFirst.Call(buf, ch)
  456. return ret
  457. }