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.

529 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) Close(_ context.Context) error {
  322. d.mu.Lock()
  323. defer d.mu.Unlock()
  324. d.started = false
  325. d.cleanup()
  326. return nil
  327. }
  328. func (d *PlutoDriver) Stats() platform.RuntimeStats {
  329. d.mu.Lock()
  330. defer d.mu.Unlock()
  331. return platform.RuntimeStats{
  332. TXEnabled: d.started,
  333. StreamActive: d.started && d.buf != 0,
  334. FramesWritten: d.framesWritten.Load(),
  335. SamplesWritten: d.samplesWritten.Load(),
  336. Underruns: d.underruns.Load(),
  337. LastError: d.lastError,
  338. LastErrorAt: d.lastErrorAt,
  339. EffectiveRate: d.cfg.SampleRateHz,
  340. }
  341. }
  342. // --- internal helpers ---
  343. func (d *PlutoDriver) cleanup() {
  344. if d.buf != 0 && d.lib.pDestroyBuffer != nil {
  345. d.lib.pDestroyBuffer.Call(d.buf)
  346. d.buf = 0
  347. }
  348. if d.chanI != 0 {
  349. d.disableChannel(d.chanI)
  350. d.chanI = 0
  351. }
  352. if d.chanQ != 0 {
  353. d.disableChannel(d.chanQ)
  354. d.chanQ = 0
  355. }
  356. if d.ctx != 0 && d.lib.pDestroyCtx != nil {
  357. d.lib.pDestroyCtx.Call(d.ctx)
  358. d.ctx = 0
  359. }
  360. d.configured = false
  361. }
  362. func (d *PlutoDriver) createContext(uri string) (uintptr, error) {
  363. if d.lib.pCreateCtxFromURI == nil {
  364. return 0, fmt.Errorf("iio_create_context_from_uri not found")
  365. }
  366. cURI, _ := syscall.BytePtrFromString(uri)
  367. ret, _, _ := d.lib.pCreateCtxFromURI.Call(uintptr(unsafe.Pointer(cURI)))
  368. if ret == 0 {
  369. return 0, fmt.Errorf("pluto: failed to create IIO context (uri=%s)", uri)
  370. }
  371. return ret, nil
  372. }
  373. func (d *PlutoDriver) findDevice(name string) uintptr {
  374. if d.lib.pCtxFindDevice == nil || d.ctx == 0 {
  375. return 0
  376. }
  377. cName, _ := syscall.BytePtrFromString(name)
  378. ret, _, _ := d.lib.pCtxFindDevice.Call(d.ctx, uintptr(unsafe.Pointer(cName)))
  379. return ret
  380. }
  381. func (d *PlutoDriver) findChannel(dev uintptr, name string, isOutput bool) uintptr {
  382. if d.lib.pDeviceFindChannel == nil || dev == 0 {
  383. return 0
  384. }
  385. cName, _ := syscall.BytePtrFromString(name)
  386. out := uintptr(0)
  387. if isOutput {
  388. out = 1
  389. }
  390. ret, _, _ := d.lib.pDeviceFindChannel.Call(dev, uintptr(unsafe.Pointer(cName)), out)
  391. return ret
  392. }
  393. func (d *PlutoDriver) enableChannel(ch uintptr) {
  394. if d.lib.pChannelEnable != nil && ch != 0 {
  395. d.lib.pChannelEnable.Call(ch)
  396. }
  397. }
  398. func (d *PlutoDriver) disableChannel(ch uintptr) {
  399. if d.lib.pChannelDisable != nil && ch != 0 {
  400. d.lib.pChannelDisable.Call(ch)
  401. }
  402. }
  403. func (d *PlutoDriver) writeChanAttrLL(ch uintptr, attr string, val int64) {
  404. if d.lib.pChannelAttrWriteLongLong == nil || ch == 0 {
  405. return
  406. }
  407. cAttr, _ := syscall.BytePtrFromString(attr)
  408. d.lib.pChannelAttrWriteLongLong.Call(ch, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  409. }
  410. func (d *PlutoDriver) writeDevAttrLL(dev uintptr, attr string, val int64) {
  411. if d.lib.pDeviceAttrWriteLongLong == nil || dev == 0 {
  412. return
  413. }
  414. cAttr, _ := syscall.BytePtrFromString(attr)
  415. d.lib.pDeviceAttrWriteLongLong.Call(dev, uintptr(unsafe.Pointer(cAttr)), uintptr(val))
  416. }
  417. func (d *PlutoDriver) createBuffer(dev uintptr, sampleCount int, cyclic bool) uintptr {
  418. if d.lib.pCreateBuffer == nil || dev == 0 {
  419. return 0
  420. }
  421. c := uintptr(0)
  422. if cyclic {
  423. c = 1
  424. }
  425. ret, _, _ := d.lib.pCreateBuffer.Call(dev, uintptr(sampleCount), c)
  426. return ret
  427. }
  428. func (d *PlutoDriver) bufferPush(buf uintptr) int {
  429. if d.lib.pBufferPush == nil || buf == 0 {
  430. return -1
  431. }
  432. ret, _, _ := d.lib.pBufferPush.Call(buf)
  433. return int(int32(ret))
  434. }
  435. func (d *PlutoDriver) bufferStart(buf uintptr) uintptr {
  436. if d.lib.pBufferStart == nil {
  437. return 0
  438. }
  439. ret, _, _ := d.lib.pBufferStart.Call(buf)
  440. return ret
  441. }
  442. func (d *PlutoDriver) bufferEnd(buf uintptr) uintptr {
  443. if d.lib.pBufferEnd == nil {
  444. return 0
  445. }
  446. ret, _, _ := d.lib.pBufferEnd.Call(buf)
  447. return ret
  448. }
  449. func (d *PlutoDriver) bufferStep(buf uintptr) uintptr {
  450. if d.lib.pBufferStep == nil {
  451. return 0
  452. }
  453. ret, _, _ := d.lib.pBufferStep.Call(buf)
  454. return ret
  455. }
  456. func (d *PlutoDriver) bufferFirst(buf uintptr, ch uintptr) uintptr {
  457. if d.lib.pBufferFirst == nil {
  458. return 0
  459. }
  460. ret, _, _ := d.lib.pBufferFirst.Call(buf, ch)
  461. return ret
  462. }