Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

543 wiersze
14KB

  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. chanI := d.chanI
  254. chanQ := d.chanQ
  255. started := d.started
  256. bufSize := d.bufSize
  257. d.mu.Unlock()
  258. if !started || buf == 0 || lib == nil {
  259. return 0, fmt.Errorf("pluto: not active")
  260. }
  261. if frame == nil || len(frame.Samples) == 0 {
  262. return 0, nil
  263. }
  264. written := 0
  265. total := len(frame.Samples)
  266. for written < total {
  267. chunk := total - written
  268. if chunk > bufSize {
  269. chunk = bufSize
  270. }
  271. step := d.bufferStep(buf)
  272. if step == 0 {
  273. return written, fmt.Errorf("pluto: buffer step is 0")
  274. }
  275. // iio_buffer_first gives the pointer to the first sample for each channel.
  276. // For interleaved buffers (step=4 with 2x int16), ptrI and ptrQ may
  277. // differ by 2 bytes (I at offset 0, Q at offset 2 within each step).
  278. // For non-interleaved, they point to separate memory regions.
  279. ptrI := d.bufferFirst(buf, chanI)
  280. ptrQ := d.bufferFirst(buf, chanQ)
  281. if ptrI == 0 || ptrQ == 0 {
  282. return written, fmt.Errorf("pluto: buffer_first returned null (I=%d Q=%d)", ptrI, ptrQ)
  283. }
  284. end := d.bufferEnd(buf)
  285. if end > 0 {
  286. bufSamples := int((end - ptrI) / uintptr(step))
  287. if bufSamples > 0 && chunk > bufSamples {
  288. chunk = bufSamples
  289. }
  290. }
  291. for i := 0; i < chunk; i++ {
  292. s := frame.Samples[written+i]
  293. // Scale float32 [-1,+1] to int16 [-32767,+32767]
  294. *(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
  295. *(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
  296. ptrI += uintptr(step)
  297. ptrQ += uintptr(step)
  298. }
  299. pushed := d.bufferPush(buf)
  300. if pushed < 0 {
  301. d.mu.Lock()
  302. d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
  303. d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
  304. d.underruns.Add(1)
  305. d.mu.Unlock()
  306. return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
  307. }
  308. written += chunk
  309. }
  310. d.framesWritten.Add(1)
  311. d.samplesWritten.Add(uint64(written))
  312. return written, nil
  313. }
  314. func (d *PlutoDriver) Stop(_ context.Context) error {
  315. d.mu.Lock()
  316. defer d.mu.Unlock()
  317. d.started = false
  318. return nil
  319. }
  320. func (d *PlutoDriver) Flush(_ context.Context) error { return nil }
  321. func (d *PlutoDriver) Tune(_ context.Context, freqHz float64) error {
  322. d.mu.Lock()
  323. defer d.mu.Unlock()
  324. if d.phyDev == 0 {
  325. return fmt.Errorf("pluto: not configured")
  326. }
  327. phyChanLO := d.findChannel(d.phyDev, "altvoltage1", true)
  328. if phyChanLO == 0 {
  329. return fmt.Errorf("pluto: TX LO channel not found")
  330. }
  331. d.writeChanAttrLL(phyChanLO, "frequency", int64(freqHz))
  332. return nil
  333. }
  334. func (d *PlutoDriver) Close(_ context.Context) error {
  335. d.mu.Lock()
  336. defer d.mu.Unlock()
  337. d.started = false
  338. d.cleanup()
  339. return nil
  340. }
  341. func (d *PlutoDriver) Stats() platform.RuntimeStats {
  342. d.mu.Lock()
  343. defer d.mu.Unlock()
  344. return platform.RuntimeStats{
  345. TXEnabled: d.started,
  346. StreamActive: d.started && d.buf != 0,
  347. FramesWritten: d.framesWritten.Load(),
  348. SamplesWritten: d.samplesWritten.Load(),
  349. Underruns: d.underruns.Load(),
  350. LastError: d.lastError,
  351. LastErrorAt: d.lastErrorAt,
  352. EffectiveRate: d.cfg.SampleRateHz,
  353. }
  354. }
  355. // --- internal helpers ---
  356. func (d *PlutoDriver) cleanup() {
  357. if d.buf != 0 && d.lib.pDestroyBuffer != nil {
  358. d.lib.pDestroyBuffer.Call(d.buf)
  359. d.buf = 0
  360. }
  361. if d.chanI != 0 {
  362. d.disableChannel(d.chanI)
  363. d.chanI = 0
  364. }
  365. if d.chanQ != 0 {
  366. d.disableChannel(d.chanQ)
  367. d.chanQ = 0
  368. }
  369. if d.ctx != 0 && d.lib.pDestroyCtx != nil {
  370. d.lib.pDestroyCtx.Call(d.ctx)
  371. d.ctx = 0
  372. }
  373. d.configured = false
  374. }
  375. func (d *PlutoDriver) createContext(uri string) (uintptr, error) {
  376. if d.lib.pCreateCtxFromURI == nil {
  377. return 0, fmt.Errorf("iio_create_context_from_uri not found")
  378. }
  379. cURI, _ := syscall.BytePtrFromString(uri)
  380. ret, _, _ := d.lib.pCreateCtxFromURI.Call(uintptr(unsafe.Pointer(cURI)))
  381. if ret == 0 {
  382. return 0, fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
  383. }
  384. return ret, nil
  385. }
  386. func (d *PlutoDriver) findDevice(name string) uintptr {
  387. if d.lib.pCtxFindDevice == nil || d.ctx == 0 {
  388. return 0
  389. }
  390. cName, _ := syscall.BytePtrFromString(name)
  391. ret, _, _ := d.lib.pCtxFindDevice.Call(d.ctx, uintptr(unsafe.Pointer(cName)))
  392. return ret
  393. }
  394. func (d *PlutoDriver) findChannel(dev uintptr, name string, isOutput bool) uintptr {
  395. if d.lib.pDeviceFindChannel == nil || dev == 0 {
  396. return 0
  397. }
  398. cName, _ := syscall.BytePtrFromString(name)
  399. out := uintptr(0)
  400. if isOutput {
  401. out = 1
  402. }
  403. ret, _, _ := d.lib.pDeviceFindChannel.Call(dev, uintptr(unsafe.Pointer(cName)), out)
  404. return ret
  405. }
  406. func (d *PlutoDriver) enableChannel(ch uintptr) {
  407. if d.lib.pChannelEnable != nil && ch != 0 {
  408. d.lib.pChannelEnable.Call(ch)
  409. }
  410. }
  411. func (d *PlutoDriver) disableChannel(ch uintptr) {
  412. if d.lib.pChannelDisable != nil && ch != 0 {
  413. d.lib.pChannelDisable.Call(ch)
  414. }
  415. }
  416. func (d *PlutoDriver) writeChanAttrLL(ch uintptr, attr string, val int64) {
  417. if d.lib.pChannelAttrWriteLongLong == nil || ch == 0 {
  418. return
  419. }
  420. cAttr, _ := syscall.BytePtrFromString(attr)
  421. d.lib.pChannelAttrWriteLongLong.Call(ch, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  422. }
  423. func (d *PlutoDriver) writeDevAttrLL(dev uintptr, attr string, val int64) {
  424. if d.lib.pDeviceAttrWriteLongLong == nil || dev == 0 {
  425. return
  426. }
  427. cAttr, _ := syscall.BytePtrFromString(attr)
  428. d.lib.pDeviceAttrWriteLongLong.Call(dev, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  429. }
  430. func (d *PlutoDriver) createBuffer(dev uintptr, sampleCount int, cyclic bool) uintptr {
  431. if d.lib.pCreateBuffer == nil || dev == 0 {
  432. return 0
  433. }
  434. c := uintptr(0)
  435. if cyclic {
  436. c = 1
  437. }
  438. ret, _, _ := d.lib.pCreateBuffer.Call(dev, uintptr(sampleCount), c)
  439. return ret
  440. }
  441. func (d *PlutoDriver) bufferPush(buf uintptr) int {
  442. if d.lib.pBufferPush == nil || buf == 0 {
  443. return -1
  444. }
  445. ret, _, _ := d.lib.pBufferPush.Call(buf)
  446. return int(int32(ret))
  447. }
  448. func (d *PlutoDriver) bufferStart(buf uintptr) uintptr {
  449. if d.lib.pBufferStart == nil {
  450. return 0
  451. }
  452. ret, _, _ := d.lib.pBufferStart.Call(buf)
  453. return ret
  454. }
  455. func (d *PlutoDriver) bufferEnd(buf uintptr) uintptr {
  456. if d.lib.pBufferEnd == nil {
  457. return 0
  458. }
  459. ret, _, _ := d.lib.pBufferEnd.Call(buf)
  460. return ret
  461. }
  462. func (d *PlutoDriver) bufferStep(buf uintptr) uintptr {
  463. if d.lib.pBufferStep == nil {
  464. return 0
  465. }
  466. ret, _, _ := d.lib.pBufferStep.Call(buf)
  467. return ret
  468. }
  469. func (d *PlutoDriver) bufferFirst(buf uintptr, ch uintptr) uintptr {
  470. if d.lib.pBufferFirst == nil {
  471. return 0
  472. }
  473. ret, _, _ := d.lib.pBufferFirst.Call(buf, ch)
  474. return ret
  475. }