Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

553 linhas
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. chanLO uintptr // iio_channel* TX LO (altvoltage1), cached for Tune()
  99. buf uintptr // iio_buffer*
  100. bufSize int // samples per buffer push
  101. started bool
  102. configured bool
  103. framesWritten atomic.Uint64
  104. samplesWritten atomic.Uint64
  105. underruns atomic.Uint64
  106. lastError string
  107. lastErrorAt string
  108. initError string
  109. }
  110. func NewPlutoDriver() platform.SoapyDriver {
  111. lib, err := loadIIOLib()
  112. if err != nil {
  113. return &PlutoDriver{initError: err.Error()}
  114. }
  115. return &PlutoDriver{lib: lib}
  116. }
  117. func (d *PlutoDriver) Name() string { return "pluto-iio" }
  118. func (d *PlutoDriver) Configure(_ context.Context, cfg platform.SoapyConfig) error {
  119. d.mu.Lock()
  120. defer d.mu.Unlock()
  121. if d.lib == nil {
  122. return fmt.Errorf("libiio not loaded: %s", d.initError)
  123. }
  124. // Cleanup existing
  125. d.cleanup()
  126. d.cfg = cfg
  127. // Create IIO context via USB
  128. uri := "usb:"
  129. if cfg.Device != "" && cfg.Device != "plutosdr" {
  130. uri = cfg.Device // allow "ip:192.168.2.1" or specific USB
  131. }
  132. ctx, err := d.createContext(uri)
  133. if err != nil {
  134. return err
  135. }
  136. d.ctx = ctx
  137. // Find TX streaming device
  138. txDev := d.findDevice("cf-ad9361-dds-core-lpc")
  139. if txDev == 0 {
  140. return fmt.Errorf("pluto: TX device 'cf-ad9361-dds-core-lpc' not found")
  141. }
  142. d.txDev = txDev
  143. // Find PHY device for configuration
  144. phyDev := d.findDevice("ad9361-phy")
  145. if phyDev == 0 {
  146. return fmt.Errorf("pluto: PHY device 'ad9361-phy' not found")
  147. }
  148. d.phyDev = phyDev
  149. // --- AD9361 PHY configuration ---
  150. // The AD9364 (PlutoSDR) has TX on voltage3 (output), not voltage0
  151. // voltage0 = RX input, voltage3 = TX output on single-channel AD9364
  152. // Find TX PHY output channel
  153. phyChanTX := d.findChannel(phyDev, "voltage3", true) // output=true
  154. if phyChanTX == 0 {
  155. // Fallback for dual-channel AD9361: try voltage0 output
  156. phyChanTX = d.findChannel(phyDev, "voltage0", true)
  157. }
  158. if phyChanTX == 0 {
  159. return fmt.Errorf("pluto: PHY TX channel not found (tried voltage3, voltage0)")
  160. }
  161. // Sample rate — AD9361 minimum is ~2.084 MHz.
  162. // We set the hardware to this minimum and resample from composite rate.
  163. rate := int64(cfg.SampleRateHz)
  164. if rate < 2084000 {
  165. rate = 2084000 // AD9361 minimum
  166. }
  167. // Update effective rate so the engine knows the real device rate
  168. d.cfg.SampleRateHz = float64(rate)
  169. d.writeChanAttrLL(phyChanTX, "sampling_frequency", rate)
  170. // RF bandwidth — set to match our signal bandwidth (wider than composite)
  171. bw := rate
  172. if bw > 2000000 {
  173. bw = 2000000 // 2 MHz BW is enough for FM broadcast
  174. }
  175. d.writeChanAttrLL(phyChanTX, "rf_bandwidth", bw)
  176. // TX LO frequency
  177. phyChanLO := d.findChannel(phyDev, "altvoltage1", true) // TX LO
  178. d.chanLO = phyChanLO // cache for Tune()
  179. if phyChanLO != 0 {
  180. freqHz := int64(cfg.CenterFreqHz)
  181. if freqHz <= 0 {
  182. freqHz = 100000000 // 100 MHz default
  183. }
  184. d.writeChanAttrLL(phyChanLO, "frequency", freqHz)
  185. }
  186. // TX gain/attenuation
  187. // PlutoSDR TX hardwaregain: 0 dB = max power, -89.75 dB = min
  188. // Value is in dB (not millidB) as a negative number
  189. // For max power: set to 0. For safety: set to -20 or so.
  190. // cfg.GainDB from our config is 0..89 positive, we negate it and subtract from 0
  191. attenDB := int64(0) // default = max power
  192. if cfg.GainDB > 0 {
  193. // GainDB=89 means full attenuation, GainDB=0 means max power
  194. attenDB = -int64(89 - cfg.GainDB)
  195. if attenDB > 0 {
  196. attenDB = 0
  197. }
  198. if attenDB < -89 {
  199. attenDB = -89
  200. }
  201. }
  202. d.writeChanAttrLL(phyChanTX, "hardwaregain", attenDB*1000) // millidB
  203. // --- TX streaming channels on cf-ad9361-dds-core-lpc ---
  204. // voltage0 (I) and voltage1 (Q) are output channels
  205. chanI := d.findChannel(txDev, "voltage0", true)
  206. chanQ := d.findChannel(txDev, "voltage1", true)
  207. if chanI == 0 || chanQ == 0 {
  208. return fmt.Errorf("pluto: TX I/Q channels not found on streaming device")
  209. }
  210. d.enableChannel(chanI)
  211. d.enableChannel(chanQ)
  212. d.chanI = chanI
  213. d.chanQ = chanQ
  214. // Create buffer — samples per push (per channel)
  215. // At 2.084 MHz with 50ms chunks = 104200 samples. Buffer must fit this.
  216. d.bufSize = int(rate) / 20 // 50ms worth
  217. if d.bufSize < 4096 {
  218. d.bufSize = 4096
  219. }
  220. // libiio can handle large buffers; no artificial cap needed
  221. buf := d.createBuffer(txDev, d.bufSize, false)
  222. if buf == 0 {
  223. return fmt.Errorf("pluto: failed to create TX buffer (size=%d)", d.bufSize)
  224. }
  225. d.buf = buf
  226. d.configured = true
  227. return nil
  228. }
  229. func (d *PlutoDriver) Capabilities(_ context.Context) (platform.DeviceCaps, error) {
  230. return platform.DeviceCaps{
  231. MinSampleRate: 521e3,
  232. MaxSampleRate: 61.44e6,
  233. HasGain: true,
  234. GainMinDB: -89,
  235. GainMaxDB: 0,
  236. Channels: []int{0},
  237. }, nil
  238. }
  239. func (d *PlutoDriver) Start(_ context.Context) error {
  240. d.mu.Lock()
  241. defer d.mu.Unlock()
  242. if !d.configured {
  243. return fmt.Errorf("pluto: not configured")
  244. }
  245. if d.started {
  246. return fmt.Errorf("pluto: already started")
  247. }
  248. d.started = true
  249. return nil
  250. }
  251. func (d *PlutoDriver) Write(_ context.Context, frame *output.CompositeFrame) (int, error) {
  252. d.mu.Lock()
  253. lib := d.lib
  254. buf := d.buf
  255. chanI := d.chanI
  256. chanQ := d.chanQ
  257. started := d.started
  258. bufSize := d.bufSize
  259. d.mu.Unlock()
  260. if !started || buf == 0 || lib == nil {
  261. return 0, fmt.Errorf("pluto: not active")
  262. }
  263. if frame == nil || len(frame.Samples) == 0 {
  264. return 0, nil
  265. }
  266. written := 0
  267. total := len(frame.Samples)
  268. for written < total {
  269. chunk := total - written
  270. if chunk > bufSize {
  271. chunk = bufSize
  272. }
  273. step := d.bufferStep(buf)
  274. if step == 0 {
  275. return written, fmt.Errorf("pluto: buffer step is 0")
  276. }
  277. // iio_buffer_first gives the pointer to the first sample for each channel.
  278. // For interleaved buffers (step=4 with 2x int16), ptrI and ptrQ may
  279. // differ by 2 bytes (I at offset 0, Q at offset 2 within each step).
  280. // For non-interleaved, they point to separate memory regions.
  281. ptrI := d.bufferFirst(buf, chanI)
  282. ptrQ := d.bufferFirst(buf, chanQ)
  283. if ptrI == 0 || ptrQ == 0 {
  284. return written, fmt.Errorf("pluto: buffer_first returned null (I=%d Q=%d)", ptrI, ptrQ)
  285. }
  286. end := d.bufferEnd(buf)
  287. if end > 0 {
  288. bufSamples := int((end - ptrI) / uintptr(step))
  289. if bufSamples > 0 && chunk > bufSamples {
  290. chunk = bufSamples
  291. }
  292. }
  293. for i := 0; i < chunk; i++ {
  294. s := frame.Samples[written+i]
  295. // Scale float32 [-1,+1] to int16 [-32767,+32767]
  296. *(*int16)(unsafe.Pointer(ptrI)) = int16(s.I * 32767)
  297. *(*int16)(unsafe.Pointer(ptrQ)) = int16(s.Q * 32767)
  298. ptrI += uintptr(step)
  299. ptrQ += uintptr(step)
  300. }
  301. pushed := d.bufferPush(buf)
  302. if pushed < 0 {
  303. d.mu.Lock()
  304. d.lastError = fmt.Sprintf("buffer_push: %d", pushed)
  305. d.lastErrorAt = time.Now().UTC().Format(time.RFC3339)
  306. d.underruns.Add(1)
  307. d.mu.Unlock()
  308. return written, fmt.Errorf("pluto: buffer_push returned %d", pushed)
  309. }
  310. written += chunk
  311. }
  312. d.framesWritten.Add(1)
  313. d.samplesWritten.Add(uint64(written))
  314. return written, nil
  315. }
  316. func (d *PlutoDriver) Stop(_ context.Context) error {
  317. d.mu.Lock()
  318. defer d.mu.Unlock()
  319. d.started = false
  320. return nil
  321. }
  322. func (d *PlutoDriver) Flush(_ context.Context) error { return nil }
  323. func (d *PlutoDriver) Tune(_ context.Context, freqHz float64) error {
  324. d.mu.Lock()
  325. defer d.mu.Unlock()
  326. if !d.configured || d.chanLO == 0 {
  327. return fmt.Errorf("pluto: not configured or LO channel not available")
  328. }
  329. if d.lib.pChannelAttrWriteLongLong == nil {
  330. return fmt.Errorf("pluto: iio_channel_attr_write_longlong not loaded")
  331. }
  332. cAttr, _ := syscall.BytePtrFromString("frequency")
  333. ret, _, _ := d.lib.pChannelAttrWriteLongLong.Call(
  334. d.chanLO,
  335. uintptr(unsafe.Pointer(cAttr)),
  336. uintptr(int64(freqHz)),
  337. )
  338. if int32(ret) < 0 {
  339. return fmt.Errorf("pluto: LO tune to %.0f Hz failed (iio rc=%d)", freqHz, int32(ret))
  340. }
  341. return nil
  342. }
  343. func (d *PlutoDriver) Close(_ context.Context) error {
  344. d.mu.Lock()
  345. defer d.mu.Unlock()
  346. d.started = false
  347. d.cleanup()
  348. return nil
  349. }
  350. func (d *PlutoDriver) Stats() platform.RuntimeStats {
  351. d.mu.Lock()
  352. defer d.mu.Unlock()
  353. return platform.RuntimeStats{
  354. TXEnabled: d.started,
  355. StreamActive: d.started && d.buf != 0,
  356. FramesWritten: d.framesWritten.Load(),
  357. SamplesWritten: d.samplesWritten.Load(),
  358. Underruns: d.underruns.Load(),
  359. LastError: d.lastError,
  360. LastErrorAt: d.lastErrorAt,
  361. EffectiveRate: d.cfg.SampleRateHz,
  362. }
  363. }
  364. // --- internal helpers ---
  365. func (d *PlutoDriver) cleanup() {
  366. if d.buf != 0 && d.lib.pDestroyBuffer != nil {
  367. d.lib.pDestroyBuffer.Call(d.buf)
  368. d.buf = 0
  369. }
  370. if d.chanI != 0 {
  371. d.disableChannel(d.chanI)
  372. d.chanI = 0
  373. }
  374. if d.chanQ != 0 {
  375. d.disableChannel(d.chanQ)
  376. d.chanQ = 0
  377. }
  378. d.chanLO = 0 // config-only channel, no disable needed
  379. if d.ctx != 0 && d.lib.pDestroyCtx != nil {
  380. d.lib.pDestroyCtx.Call(d.ctx)
  381. d.ctx = 0
  382. }
  383. d.configured = false
  384. }
  385. func (d *PlutoDriver) createContext(uri string) (uintptr, error) {
  386. if d.lib.pCreateCtxFromURI == nil {
  387. return 0, fmt.Errorf("iio_create_context_from_uri not found")
  388. }
  389. cURI, _ := syscall.BytePtrFromString(uri)
  390. ret, _, _ := d.lib.pCreateCtxFromURI.Call(uintptr(unsafe.Pointer(cURI)))
  391. if ret == 0 {
  392. return 0, fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
  393. }
  394. return ret, nil
  395. }
  396. func (d *PlutoDriver) findDevice(name string) uintptr {
  397. if d.lib.pCtxFindDevice == nil || d.ctx == 0 {
  398. return 0
  399. }
  400. cName, _ := syscall.BytePtrFromString(name)
  401. ret, _, _ := d.lib.pCtxFindDevice.Call(d.ctx, uintptr(unsafe.Pointer(cName)))
  402. return ret
  403. }
  404. func (d *PlutoDriver) findChannel(dev uintptr, name string, isOutput bool) uintptr {
  405. if d.lib.pDeviceFindChannel == nil || dev == 0 {
  406. return 0
  407. }
  408. cName, _ := syscall.BytePtrFromString(name)
  409. out := uintptr(0)
  410. if isOutput {
  411. out = 1
  412. }
  413. ret, _, _ := d.lib.pDeviceFindChannel.Call(dev, uintptr(unsafe.Pointer(cName)), out)
  414. return ret
  415. }
  416. func (d *PlutoDriver) enableChannel(ch uintptr) {
  417. if d.lib.pChannelEnable != nil && ch != 0 {
  418. d.lib.pChannelEnable.Call(ch)
  419. }
  420. }
  421. func (d *PlutoDriver) disableChannel(ch uintptr) {
  422. if d.lib.pChannelDisable != nil && ch != 0 {
  423. d.lib.pChannelDisable.Call(ch)
  424. }
  425. }
  426. func (d *PlutoDriver) writeChanAttrLL(ch uintptr, attr string, val int64) {
  427. if d.lib.pChannelAttrWriteLongLong == nil || ch == 0 {
  428. return
  429. }
  430. cAttr, _ := syscall.BytePtrFromString(attr)
  431. d.lib.pChannelAttrWriteLongLong.Call(ch, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  432. }
  433. func (d *PlutoDriver) writeDevAttrLL(dev uintptr, attr string, val int64) {
  434. if d.lib.pDeviceAttrWriteLongLong == nil || dev == 0 {
  435. return
  436. }
  437. cAttr, _ := syscall.BytePtrFromString(attr)
  438. d.lib.pDeviceAttrWriteLongLong.Call(dev, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  439. }
  440. func (d *PlutoDriver) createBuffer(dev uintptr, sampleCount int, cyclic bool) uintptr {
  441. if d.lib.pCreateBuffer == nil || dev == 0 {
  442. return 0
  443. }
  444. c := uintptr(0)
  445. if cyclic {
  446. c = 1
  447. }
  448. ret, _, _ := d.lib.pCreateBuffer.Call(dev, uintptr(sampleCount), c)
  449. return ret
  450. }
  451. func (d *PlutoDriver) bufferPush(buf uintptr) int {
  452. if d.lib.pBufferPush == nil || buf == 0 {
  453. return -1
  454. }
  455. ret, _, _ := d.lib.pBufferPush.Call(buf)
  456. return int(int32(ret))
  457. }
  458. func (d *PlutoDriver) bufferStart(buf uintptr) uintptr {
  459. if d.lib.pBufferStart == nil {
  460. return 0
  461. }
  462. ret, _, _ := d.lib.pBufferStart.Call(buf)
  463. return ret
  464. }
  465. func (d *PlutoDriver) bufferEnd(buf uintptr) uintptr {
  466. if d.lib.pBufferEnd == nil {
  467. return 0
  468. }
  469. ret, _, _ := d.lib.pBufferEnd.Call(buf)
  470. return ret
  471. }
  472. func (d *PlutoDriver) bufferStep(buf uintptr) uintptr {
  473. if d.lib.pBufferStep == nil {
  474. return 0
  475. }
  476. ret, _, _ := d.lib.pBufferStep.Call(buf)
  477. return ret
  478. }
  479. func (d *PlutoDriver) bufferFirst(buf uintptr, ch uintptr) uintptr {
  480. if d.lib.pBufferFirst == nil {
  481. return 0
  482. }
  483. ret, _, _ := d.lib.pBufferFirst.Call(buf, ch)
  484. return ret
  485. }