diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a98046 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +src/waveform_*.wav +src/pydemod +src/test +*.o + diff --git a/doc/galaxy_s2.jpg b/doc/galaxy_s2.jpg new file mode 100644 index 0000000..5a23d96 Binary files /dev/null and b/doc/galaxy_s2.jpg differ diff --git a/doc/vfd_display.jpg b/doc/vfd_display.jpg new file mode 100644 index 0000000..3148d25 Binary files /dev/null and b/doc/vfd_display.jpg differ diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..c3dd5a1 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,53 @@ +CC = gcc +STD_CFLAGS = -Wall -std=gnu99 -c -g -O3 + +# Enable ARM-specific options only on ARM, and compilation of the app only on ARM +UNAME := $(shell uname -m) + +# Determine the hardware platform. Below, pi1 stands for the RaspberryPi 1 (the original one), +# and pi2 stands for both the RaspberryPi 2 and 3. +ifeq ($(UNAME), armv6l) + CFLAGS = $(STD_CFLAGS) -march=armv6 -mtune=arm1176jzf-s -mfloat-abi=hard -mfpu=vfp -ffast-math -DRASPI=1 + TARGET = pi1 +else ifeq ($(UNAME), armv7l) + CFLAGS = $(STD_CFLAGS) -march=armv7-a -mtune=arm1176jzf-s -mfloat-abi=hard -mfpu=vfp -ffast-math -DRASPI=2 + TARGET = pi2 +else + CFLAGS = $(STD_CFLAGS) + TARGET = other +endif + +ifneq ($(TARGET), other) + +app: rds.o waveforms.o pi_fm_adv.o fm_mpx.o control_pipe.o mailbox.o + $(CC) -o pi_fm_adv rds.o waveforms.o mailbox.o pi_fm_adv.o fm_mpx.o control_pipe.o -lm -lsndfile + +endif + + +rds_wav: rds.o waveforms.o rds_wav.o fm_mpx.o + $(CC) -o rds_wav rds_wav.o rds.o waveforms.o fm_mpx.o -lm -lsndfile + +rds.o: rds.c waveforms.h + $(CC) $(CFLAGS) rds.c + +control_pipe.o: control_pipe.c control_pipe.h rds.h + $(CC) $(CFLAGS) control_pipe.c + +waveforms.o: waveforms.c waveforms.h + $(CC) $(CFLAGS) waveforms.c + +mailbox.o: mailbox.c mailbox.h + $(CC) $(CFLAGS) mailbox.c + +pi_fm_rds.o: pi_fm_adv.c control_pipe.h fm_mpx.h rds.h mailbox.h + $(CC) $(CFLAGS) pi_fm_adv.c + +rds_wav.o: rds_wav.c + $(CC) $(CFLAGS) rds_wav.c + +fm_mpx.o: fm_mpx.c fm_mpx.h + $(CC) $(CFLAGS) fm_mpx.c + +clean: + rm -f *.o diff --git a/src/control_pipe.c b/src/control_pipe.c new file mode 100644 index 0000000..0045610 --- /dev/null +++ b/src/control_pipe.c @@ -0,0 +1,120 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include + +#include "rds.h" +#include "control_pipe.h" + +#define CTL_BUFFER_SIZE 100 + +FILE *f_ctl; + +/* + * Opens a file (pipe) to be used to control the RDS coder, in non-blocking mode. + */ +int open_control_pipe(char *filename) { + int fd = open(filename, O_RDONLY | O_NONBLOCK); + if(fd == -1) return -1; + + int flags; + flags = fcntl(fd, F_GETFL, 0); + flags |= O_NONBLOCK; + if( fcntl(fd, F_SETFL, flags) == -1 ) return -1; + + f_ctl = fdopen(fd, "r"); + if(f_ctl == NULL) return -1; + + return 0; +} + + +/* + * Polls the control file (pipe), non-blockingly, and if a command is received, + * processes it and updates the RDS data. + */ +int poll_control_pipe() { + static char buf[CTL_BUFFER_SIZE]; + + char *res = fgets(buf, CTL_BUFFER_SIZE, f_ctl); + if(res == NULL) return -1; + if(strlen(res) > 3 && res[2] == ' ') { + char *arg = res+3; + if(arg[strlen(arg)-1] == '\n') arg[strlen(arg)-1] = 0; + if(res[0] == 'P' && res[1] == 'S') { + arg[8] = 0; + set_rds_ps(arg); + printf("PS set to: \"%s\"\n", arg); + return CONTROL_PIPE_PS_SET; + } + if(res[0] == 'R' && res[1] == 'T') { + arg[64] = 0; + set_rds_rt(arg); + printf("RT set to: \"%s\"\n", arg); + return CONTROL_PIPE_RT_SET; + } + if(res[0] == 'T' && res[1] == 'A') { + int ta = ( strcmp(arg, "ON") == 0 ); + set_rds_ta(ta); + printf("Set TA to "); + if(ta) printf("ON\n"); else printf("OFF\n"); + return CONTROL_PIPE_TA_SET; + } + } + + if(strlen(res) > 4 && res[3] == ' ') { + char *arg = res+4; + if(arg[strlen(arg)-1] == '\n') arg[strlen(arg)-1] = 0; + if(res[0] == 'P' && res[1] == 'T' && res[2] == 'Y') { + int pty = atoi(arg); + if (pty >= 0 && pty <= 31) { + set_rds_pty(pty); + if (pty == 0) { + printf("PTY disabled\n"); + } + else { + printf("PTY set to: %i\n", pty); + } + } + else { + printf("Wrong PTY code! The PTY code range is 0 - 31.\n"); + } + return CONTROL_PIPE_PTY_SET; + } + } + + return -1; +} + +/* + * Closes the control pipe. + */ +int close_control_pipe() { + if(f_ctl) return fclose(f_ctl); + else return 0; +} diff --git a/src/control_pipe.h b/src/control_pipe.h new file mode 100644 index 0000000..873d0cf --- /dev/null +++ b/src/control_pipe.h @@ -0,0 +1,32 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#define CONTROL_PIPE_PS_SET 1 +#define CONTROL_PIPE_RT_SET 2 +#define CONTROL_PIPE_TA_SET 3 +#define CONTROL_PIPE_PTY_SET 4 + +extern int open_control_pipe(char *filename); +extern int close_control_pipe(); +extern int poll_control_pipe(); diff --git a/src/fm_mpx.c b/src/fm_mpx.c new file mode 100644 index 0000000..59a5d6c --- /dev/null +++ b/src/fm_mpx.c @@ -0,0 +1,324 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include + +#include "rds.h" + + +#define PI 3.141592654 + + +#define FIR_HALF_SIZE 30 +#define FIR_SIZE (2*FIR_HALF_SIZE-1) + + +size_t length; + +// coefficients of the low-pass FIR filter +float low_pass_fir[FIR_HALF_SIZE]; + + +float carrier_38[] = {0.0, 0.8660254037844386, 0.8660254037844388, 1.2246467991473532e-16, -0.8660254037844384, -0.8660254037844386}; + +float carrier_19[] = {0.0, 0.5, 0.8660254037844386, 1.0, 0.8660254037844388, 0.5, 1.2246467991473532e-16, -0.5, -0.8660254037844384, -1.0, -0.8660254037844386, -0.5}; + +int phase_38 = 0; +int phase_19 = 0; + + +float downsample_factor; + + +float *audio_buffer; +int audio_index = 0; +int audio_len = 0; +float audio_pos; + +float fir_buffer_mono[FIR_SIZE] = {0}; +float fir_buffer_stereo[FIR_SIZE] = {0}; +int fir_index = 0; +int channels; + +float *last_buffer_val; +float preemphasis_prewarp; +float preemphasis_coefficient; + +int transmit_only_stereo; +int no_rds; + +SNDFILE *inf; + + + +float *alloc_empty_buffer(size_t length) { + float *p = malloc(length * sizeof(float)); + if(p == NULL) return NULL; + + bzero(p, length * sizeof(float)); + + return p; +} + + +int fm_mpx_open(char *filename, size_t len, float cutoff_freq, float preemphasis_corner_freq, int raw, int onlystereo, int nords) { + length = len; + + if(filename != NULL) { + // Open the input file + SF_INFO sfinfo; + + if(raw) { + sfinfo.format = SF_FORMAT_RAW | SF_FORMAT_PCM_16; + sfinfo.samplerate = 44100; + sfinfo.channels = 2; + } + + // stdin or file on the filesystem? + if(filename[0] == '-') { + if(! (inf = sf_open_fd(fileno(stdin), SFM_READ, &sfinfo, 0))) { + fprintf(stderr, "Error: could not open stdin for audio input.\n") ; + return -1; + } else { + printf("Using stdin for audio input.\n"); + } + } else { + if(! (inf = sf_open(filename, SFM_READ, &sfinfo))) { + fprintf(stderr, "Error: could not open input file %s.\n", filename) ; + return -1; + } else { + printf("Using audio file: %s\n", filename); + } + } + + int in_samplerate = sfinfo.samplerate; + downsample_factor = 228000. / in_samplerate; + + printf("Input: %d Hz, upsampling factor: %.2f\n", in_samplerate, downsample_factor); + + channels = sfinfo.channels; + if(channels > 1) { + printf("%d channels, generating stereo multiplex.\n", channels); + } else { + printf("1 channel, monophonic operation.\n"); + } + + // Create the preemphasis + last_buffer_val = (float*) malloc(sizeof(float)*channels); + for(int i=0;i= downsample_factor) { + audio_pos -= downsample_factor; + + if(audio_len == 0) { + for(int j=0; j<2; j++) { // one retry + audio_len = sf_read_float(inf, audio_buffer, length); + if (audio_len < 0) { + fprintf(stderr, "Error reading audio\n"); + return -1; + } + if(audio_len == 0) { + if( sf_seek(inf, 0, SEEK_SET) < 0 ) { + //fprintf(stderr, "Could not rewind in audio file, terminating\n"); + //return -1; + return 0; + } + } else { + //apply preemphasis + int k; + int l; + float tmp; + for(k=0;k= FIR_SIZE) fir_index = 0; + + // Now apply the FIR low-pass filter + + /* As the FIR filter is symmetric, we do not multiply all + the coefficients independently, but two-by-two, thus reducing + the total number of multiplications by a factor of two + */ + float out_mono = 0; + float out_stereo = 0; + int ifbi = fir_index; // ifbi = increasing FIR Buffer Index + int dfbi = fir_index; // dfbi = decreasing FIR Buffer Index + for(int fi=0; fi 1) { + out_stereo += + low_pass_fir[fi] * + (fir_buffer_stereo[ifbi] + fir_buffer_stereo[dfbi]); + } + ifbi++; + if(ifbi >= FIR_SIZE) ifbi = 0; + } + // End of FIR filter + + if (transmit_only_stereo) { // If set, PiFmAdv will transmit audio only on stereo subcarrier (38Khz). This is useful when testing FM receivers. + if (no_rds) { + mpx_buffer[i] = + 4.05 * carrier_38[phase_38] * out_mono; // Unmodulated monophonic (or stereo-sum) signal on 38Khz (stereo) subcarrier + } else { + mpx_buffer[i] = + rds_buffer[i] + // RDS data samples are currently in mpx_buffer + 4.05 * carrier_38[phase_38] * out_mono; // Unmodulated monophonic (or stereo-sum) signal on 38Khz (stereo) subcarrier + } + + if(channels>1) { + mpx_buffer[i] += + 1.0 * carrier_19[phase_19]; // Stereo pilot tone + + phase_19++; + phase_38++; + if(phase_19 >= 12) phase_19 = 0; + if(phase_38 >= 6) phase_38 = 0; + } + } + else { + if (no_rds) { + mpx_buffer[i] = + 4.05 * out_mono; // Unmodulated monophonic (or stereo-sum) signal + } else { + mpx_buffer[i] = + rds_buffer[i] + // RDS data samples are currently in mpx_buffer + 4.05 * out_mono; // Unmodulated monophonic (or stereo-sum) signal + } + + if(channels>1) { + mpx_buffer[i] += + 4.05 * carrier_38[phase_38] * out_stereo + // Stereo difference signal + 1.0 * carrier_19[phase_19]; // Stereo pilot tone + + phase_19++; + phase_38++; + if(phase_19 >= 12) phase_19 = 0; + if(phase_38 >= 6) phase_38 = 0; + } + } + audio_pos++; + } + + return 0; +} + + +int fm_mpx_close() { + if(sf_close(inf) ) { + fprintf(stderr, "Error closing audio file"); + } + + if(audio_buffer != NULL) free(audio_buffer); + + return 0; +} diff --git a/src/fm_mpx.h b/src/fm_mpx.h new file mode 100644 index 0000000..8c3020a --- /dev/null +++ b/src/fm_mpx.h @@ -0,0 +1,26 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +extern int fm_mpx_open(char *filename, size_t len, float cutoff_freq, float preemphasis_corner_freq, int raw, int onlystereo, int nords); +extern int fm_mpx_get_samples(float *mpx_buffer, float *rds_buffer); +extern int fm_mpx_close(); diff --git a/src/generate_pulses.py b/src/generate_pulses.py new file mode 100644 index 0000000..23f75bb --- /dev/null +++ b/src/generate_pulses.py @@ -0,0 +1,36 @@ +#!/usr/bin/python + + +# PiFmAdv - FM/RDS transmitter for the Raspberry Pi +# Copyright (C) 2017 Miegl +# +# See https://github.com/Miegl/PiFmAdv +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# This program generates a WAV file with a 1-second sine wave at 440 Hz, +# followed by a 1-second silence. + + +import scipy.io.wavfile as wavfile +import numpy + +sample_rate = 228000 +samples = numpy.zeros(2 * sample_rate, dtype=numpy.dtype('>i2')) + +# 1-second tune +samples[:sample_rate] = (numpy.sin(2*numpy.pi*440*numpy.arange(sample_rate)/sample_rate) + * 20000).astype(numpy.dtype('>i2')) + +wavfile.write("pulses.wav", sample_rate, samples) diff --git a/src/generate_waveforms.py b/src/generate_waveforms.py new file mode 100644 index 0000000..f396719 --- /dev/null +++ b/src/generate_waveforms.py @@ -0,0 +1,84 @@ +#!/usr/bin/python + + +# PiFmAdv - FM/RDS transmitter for the Raspberry Pi +# Copyright (C) 2017 Miegl +# +# See https://github.com/Miegl/PiFmAdv +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +# This program generates the waveform of a single biphase symbol +# +# This program uses Pydemod, see https://github.com/ChristopheJacquet/Pydemod + +import pydemod.app.rds as rds +import numpy +import scipy.io.wavfile as wavfile +import io +import matplotlib.pyplot as plt + +sample_rate = 228000 + +outc = io.open("waveforms.c", mode="w", encoding="utf8") +outh = io.open("waveforms.h", mode="w", encoding="utf8") + +header = u""" +/* This file was automatically generated by "generate_waveforms.py". + (C) 2014 Christophe Jacquet. + Released under the GNU GPL v3 license. +*/ + +""" + +outc.write(header) +outh.write(header) + +def generate_bit(name): + offset = 240 + l = 96 + count = 2 + + + sample = numpy.zeros(3*l) + sample[l] = 1 + sample[2*l] = -1 + + # Apply the data-shaping filter + sf = rds.pulse_shaping_filter(96*8, 228000) + shapedSamples = numpy.convolve(sample, sf) + + + out = shapedSamples[528-288:528+288] #[offset:offset+l*count] + #plt.plot(sf) + #plt.plot(out) + #plt.show() + + iout = (out * 20000./max(abs(out)) ).astype(numpy.dtype('>i2')) + wavfile.write(u"waveform_{}.wav".format(name), sample_rate, iout) + + outc.write(u"float waveform_{name}[] = {{{values}}};\n\n".format( + name = name, + values = u", ".join(map(unicode, out/2.5)))) + # note: need to limit the amplitude so as not to saturate when the biphase + # waveforms are summed + + outh.write(u"extern float waveform_{name}[{size}];\n".format(name=name, size=len(out))) + + +generate_bit("biphase") + +outc.close() +outh.close() diff --git a/src/mailbox.c b/src/mailbox.c new file mode 100644 index 0000000..902f6bb --- /dev/null +++ b/src/mailbox.c @@ -0,0 +1,276 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mailbox.h" + +#define PAGE_SIZE (4*1024) + +void *mapmem(unsigned base, unsigned size) +{ + int mem_fd; + unsigned offset = base % PAGE_SIZE; + base = base - offset; + /* open /dev/mem */ + if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) { + printf("can't open /dev/mem\nThis program should be run as root. Try prefixing command with: sudo\n"); + exit (-1); + } + void *mem = mmap( + 0, + size, + PROT_READ|PROT_WRITE, + MAP_SHARED/*|MAP_FIXED*/, + mem_fd, + base); +#ifdef DEBUG + printf("base=0x%x, mem=%p\n", base, mem); +#endif + if (mem == MAP_FAILED) { + printf("mmap error %d\n", (int)mem); + exit (-1); + } + close(mem_fd); + return (char *)mem + offset; +} + +void *unmapmem(void *addr, unsigned size) +{ + int s = munmap(addr, size); + if (s != 0) { + printf("munmap error %d\n", s); + exit (-1); + } + + return NULL; +} + +/* + * use ioctl to send mbox property message + */ + +static int mbox_property(int file_desc, void *buf) +{ + int ret_val = ioctl(file_desc, IOCTL_MBOX_PROPERTY, buf); + + if (ret_val < 0) { + printf("ioctl_set_msg failed:%d\n", ret_val); + } + +#ifdef DEBUG + unsigned *p = buf; int i; unsigned size = *(unsigned *)buf; + for (i=0; i= 0) { + printf("Using mbox device " DEVICE_FILE_NAME ".\n"); + return file_desc; + } + + // Try to create one + unlink(LOCAL_DEVICE_FILE_NAME); + if(mknod(LOCAL_DEVICE_FILE_NAME, S_IFCHR|0600, makedev(MAJOR_NUM_A, 0)) >= 0 && + (file_desc = open(LOCAL_DEVICE_FILE_NAME, 0)) >= 0) { + printf("Using local mbox device file with major %d.\n", MAJOR_NUM_A); + return file_desc; + } + + unlink(LOCAL_DEVICE_FILE_NAME); + if(mknod(LOCAL_DEVICE_FILE_NAME, S_IFCHR|0600, makedev(MAJOR_NUM_B, 0)) >= 0 && + (file_desc = open(LOCAL_DEVICE_FILE_NAME, 0)) >= 0) { + printf("Using local mbox device file with major %d.\n", MAJOR_NUM_B); + return file_desc; + } + + return -1; +} + +void mbox_close(int file_desc) { + close(file_desc); +} diff --git a/src/mailbox.h b/src/mailbox.h new file mode 100644 index 0000000..5315591 --- /dev/null +++ b/src/mailbox.h @@ -0,0 +1,50 @@ +/* +Copyright (c) 2012, Broadcom Europe Ltd. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include + +// Newer kernels (>= 4.1) use major 249, older ones major 100. +#define MAJOR_NUM_A 249 +#define MAJOR_NUM_B 100 +#define IOCTL_MBOX_PROPERTY _IOWR(MAJOR_NUM_B, 0, char *) +#define DEVICE_FILE_NAME "/dev/vcio" +#define LOCAL_DEVICE_FILE_NAME "mbox" + +int mbox_open(); +void mbox_close(int file_desc); + +unsigned get_version(int file_desc); +unsigned mem_alloc(int file_desc, unsigned size, unsigned align, unsigned flags); +unsigned mem_free(int file_desc, unsigned handle); +unsigned mem_lock(int file_desc, unsigned handle); +unsigned mem_unlock(int file_desc, unsigned handle); +void *mapmem(unsigned base, unsigned size); +void *unmapmem(void *addr, unsigned size); + +unsigned execute_code(int file_desc, unsigned code, unsigned r0, unsigned r1, unsigned r2, unsigned r3, unsigned r4, unsigned r5); +unsigned execute_qpu(int file_desc, unsigned num_qpus, unsigned control, unsigned noflush, unsigned timeout); +unsigned qpu_enable(int file_desc, unsigned enable); diff --git a/src/noise_22050.wav b/src/noise_22050.wav new file mode 100644 index 0000000..5fb079a Binary files /dev/null and b/src/noise_22050.wav differ diff --git a/src/pi_fm_adv.c b/src/pi_fm_adv.c new file mode 100644 index 0000000..0bc5c15 --- /dev/null +++ b/src/pi_fm_adv.c @@ -0,0 +1,651 @@ +/* + * PiFmAdv - FM/RDS transmitter for the Raspberry Pi + * Copyright (C) 2017 Miegl + * Copyright (C) 2014, 2015 Christophe Jacquet, F8FTK + * Copyright (C) 2012, 2015 Richard Hirst + * Copyright (C) 2012 Oliver Mattos and Oskar Weigl + * + * See https://github.com/Miegl/PiFmAdv + * + * PI-FM-ADV: RaspberryPi FM transmitter, with RDS. + * + * This file contains the VHF FM modulator. All credit goes to the original + * authors, Oliver Mattos and Oskar Weigl for the original idea, and to + * Richard Hirst for using the Pi's DMA engine, which reduced CPU usage + * dramatically. + * + * I (Christophe Jacquet) have adapted their idea to transmitting samples + * at 228 kHz, allowing to build the 57 kHz subcarrier for RDS BPSK data. + * + * To make it work on the Raspberry Pi 2, I used a fix by Richard Hirst + * (again) to request memory using Broadcom's mailbox interface. This fix + * was published for ServoBlaster here: + * https://www.raspberrypi.org/forums/viewtopic.php?p=699651#p699651 + * + * Never use this to transmit VHF-FM data through an antenna, as it is + * illegal in most countries. This code is for testing purposes only. + * Always connect a shielded transmission line from the RaspberryPi directly + * to a radio receiver, so as *not* to emit radio waves. + * + * --------------------------------------------------------------------------- + * These are the comments from Richard Hirst's version: + * + * RaspberryPi based FM transmitter. For the original idea, see: + * + * http://www.icrobotics.co.uk/wiki/index.php/Turning_the_Raspberry_Pi_Into_an_FM_Transmitter + * + * All credit to Oliver Mattos and Oskar Weigl for creating the original code. + * + * I have taken their idea and reworked it to use the Pi DMA engine, so + * reducing the CPU overhead for playing a .wav file from 100% to about 1.6%. + * + * I have implemented this in user space, using an idea I picked up from Joan + * on the Raspberry Pi forums - credit to Joan for the DMA from user space + * idea. + * + * The idea of feeding the PWM FIFO in order to pace DMA control blocks comes + * from ServoBlaster, and I take credit for that :-) + * + * This code uses DMA channel 0 and the PWM hardware, with no regard for + * whether something else might be trying to use it at the same time (such as + * the 3.5mm jack audio driver). + * + * I know nothing much about sound, subsampling, or FM broadcasting, so it is + * quite likely the sound quality produced by this code can be improved by + * someone who knows what they are doing. There may be issues realting to + * caching, as the user space process just writes to its virtual address space, + * and expects the DMA controller to see the data; it seems to work for me + * though. + * + * NOTE: THIS CODE MAY WELL CRASH YOUR PI, TRASH YOUR FILE SYSTEMS, AND + * POTENTIALLY EVEN DAMAGE YOUR HARDWARE. THIS IS BECAUSE IT STARTS UP THE DMA + * CONTROLLER USING MEMORY OWNED BY A USER PROCESS. IF THAT USER PROCESS EXITS + * WITHOUT STOPPING THE DMA CONTROLLER, ALL HELL COULD BREAK LOOSE AS THE + * MEMORY GETS REALLOCATED TO OTHER PROCESSES WHILE THE DMA CONTROLLER IS STILL + * USING IT. I HAVE ATTEMPTED TO MINIMISE ANY RISK BY CATCHING SIGNALS AND + * RESETTING THE DMA CONTROLLER BEFORE EXITING, BUT YOU HAVE BEEN WARNED. I + * ACCEPT NO LIABILITY OR RESPONSIBILITY FOR ANYTHING THAT HAPPENS AS A RESULT + * OF YOU RUNNING THIS CODE. IF IT BREAKS, YOU GET TO KEEP ALL THE PIECES. + * + * NOTE ALSO: THIS MAY BE ILLEGAL IN YOUR COUNTRY. HERE ARE SOME COMMENTS + * FROM MORE KNOWLEDGEABLE PEOPLE ON THE FORUM: + * + * "Just be aware that in some countries FM broadcast and especially long + * distance FM broadcast could get yourself into trouble with the law, stray FM + * broadcasts over Airband aviation is also strictly forbidden." + * + * "A low pass filter is really really required for this as it has strong + * harmonics at the 3rd, 5th 7th and 9th which sit in licensed and rather + * essential bands, ie GSM, HAM, emergency services and others. Polluting these + * frequencies is immoral and dangerous, whereas "breaking in" on FM bands is + * just plain illegal." + * + * "Don't get caught, this GPIO use has the potential to exceed the legal + * limits by about 2000% with a proper aerial." + * + * + * As for the original code, this code is released under the GPL. + * + * Richard Hirst December 2012 + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rds.h" +#include "fm_mpx.h" +#include "control_pipe.h" + +#include "mailbox.h" +#define MBFILE DEVICE_FILE_NAME /* From mailbox.h */ + +#if (RASPI)==1 +#define PERIPH_VIRT_BASE 0x20000000 +#define PERIPH_PHYS_BASE 0x7e000000 +#define DRAM_PHYS_BASE 0x40000000 +#define MEM_FLAG 0x0c +#elif (RASPI)==2 +#define PERIPH_VIRT_BASE 0x3f000000 +#define PERIPH_PHYS_BASE 0x7e000000 +#define DRAM_PHYS_BASE 0xc0000000 +#define MEM_FLAG 0x04 +#else +#error Unknown Raspberry Pi version (variable RASPI) +#endif + +#define NUM_SAMPLES 50000 +#define NUM_CBS (NUM_SAMPLES * 2) + +#define BCM2708_DMA_NO_WIDE_BURSTS (1<<26) +#define BCM2708_DMA_WAIT_RESP (1<<3) +#define BCM2708_DMA_D_DREQ (1<<6) +#define BCM2708_DMA_PER_MAP(x) ((x)<<16) +#define BCM2708_DMA_END (1<<1) +#define BCM2708_DMA_RESET (1<<31) +#define BCM2708_DMA_INT (1<<2) + +#define DMA_CS (0x00/4) +#define DMA_CONBLK_AD (0x04/4) +#define DMA_DEBUG (0x20/4) + +#define DMA_BASE_OFFSET 0x00007000 +#define DMA_LEN 0x24 +#define PWM_BASE_OFFSET 0x0020C000 +#define PWM_LEN 0x28 +#define CLK_BASE_OFFSET 0x00101000 +#define CLK_LEN 0xA8 +#define GPIO_BASE_OFFSET 0x00200000 +#define GPIO_LEN 0x100 + +#define DMA_VIRT_BASE (PERIPH_VIRT_BASE + DMA_BASE_OFFSET) +#define PWM_VIRT_BASE (PERIPH_VIRT_BASE + PWM_BASE_OFFSET) +#define CLK_VIRT_BASE (PERIPH_VIRT_BASE + CLK_BASE_OFFSET) +#define GPIO_VIRT_BASE (PERIPH_VIRT_BASE + GPIO_BASE_OFFSET) +#define PCM_VIRT_BASE (PERIPH_VIRT_BASE + PCM_BASE_OFFSET) + +#define PWM_PHYS_BASE (PERIPH_PHYS_BASE + PWM_BASE_OFFSET) +#define PCM_PHYS_BASE (PERIPH_PHYS_BASE + PCM_BASE_OFFSET) +#define GPIO_PHYS_BASE (PERIPH_PHYS_BASE + GPIO_BASE_OFFSET) + + +#define PWM_CTL (0x00/4) +#define PWM_DMAC (0x08/4) +#define PWM_RNG1 (0x10/4) +#define PWM_FIFO (0x18/4) + +#define PWMCLK_CNTL 40 +#define PWMCLK_DIV 41 + +#define CM_GP0DIV (0x7e101074) + +#define GPCLK_CNTL (0x70/4) +#define GPCLK_DIV (0x74/4) + +#define PWMCTL_MODE1 (1<<1) +#define PWMCTL_PWEN1 (1<<0) +#define PWMCTL_CLRF (1<<6) +#define PWMCTL_USEF1 (1<<5) + +#define PWMDMAC_ENAB (1<<31) +// I think this means it requests as soon as there is one free slot in the FIFO +// which is what we want as burst DMA would mess up our timing. +#define PWMDMAC_THRSHLD ((15<<8)|(15<<0)) + +#define GPFSEL0 (0x00/4) + +#define PLLFREQ 500000000. // PLLD is running at 500MHz + +typedef struct { + uint32_t info, src, dst, length, + stride, next, pad[2]; +} dma_cb_t; + +#define BUS_TO_PHYS(x) ((x)&~0xC0000000) + + +static struct { + int handle; /* From mbox_open() */ + unsigned mem_ref; /* From mem_alloc() */ + unsigned bus_addr; /* From mem_lock() */ + uint8_t *virt_addr; /* From mapmem() */ +} mbox; + + + +static volatile uint32_t *pwm_reg; +static volatile uint32_t *clk_reg; +static volatile uint32_t *dma_reg; +static volatile uint32_t *gpio_reg; + +struct control_data_s { + dma_cb_t cb[NUM_CBS]; + uint32_t sample[NUM_SAMPLES]; +}; + +#define PAGE_SIZE 4096 +#define PAGE_SHIFT 12 +#define NUM_PAGES ((sizeof(struct control_data_s) + PAGE_SIZE - 1) >> PAGE_SHIFT) + +static struct control_data_s *ctl; + +static void +udelay(int us) +{ + struct timespec ts = { 0, us * 1000 }; + + nanosleep(&ts, NULL); +} + +static void +terminate(int num) +{ + // Stop outputting and generating the clock. + if (clk_reg && gpio_reg && mbox.virt_addr) { + // Set GPIO4 to be an output (instead of ALT FUNC 0, which is the clock). + gpio_reg[GPFSEL0] = (gpio_reg[GPFSEL0] & ~(7 << 12)) | (1 << 12); + + // Disable the clock generator. + clk_reg[GPCLK_CNTL] = 0x5A; + } + + if (dma_reg && mbox.virt_addr) { + dma_reg[DMA_CS] = BCM2708_DMA_RESET; + udelay(10); + } + + fm_mpx_close(); + close_control_pipe(); + + if (mbox.virt_addr != NULL) { + unmapmem(mbox.virt_addr, NUM_PAGES * 4096); + mem_unlock(mbox.handle, mbox.mem_ref); + mem_free(mbox.handle, mbox.mem_ref); + } + + printf("Terminating: cleanly deactivated the DMA engine and killed the carrier.\n"); + + exit(num); +} + +static void +fatal(char *fmt, ...) +{ + va_list ap; + fprintf(stderr,"ERROR: "); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + terminate(0); +} + +static void +warn(char *fmt, ...) +{ + va_list ap; + fprintf(stderr,"WARNING: "); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} + + +static uint32_t +mem_virt_to_phys(void *virt) +{ + uint32_t offset = (uint8_t *)virt - mbox.virt_addr; + + return mbox.bus_addr + offset; +} + +static uint32_t +mem_phys_to_virt(uint32_t phys) +{ + return phys - (uint32_t)mbox.bus_addr + (uint32_t)mbox.virt_addr; +} + +static void * +map_peripheral(uint32_t base, uint32_t len) +{ + int fd = open("/dev/mem", O_RDWR | O_SYNC); + void * vaddr; + + if (fd < 0) + fatal("Failed to open /dev/mem: %m.\n"); + vaddr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, base); + if (vaddr == MAP_FAILED) + fatal("Failed to map peripheral at 0x%08x: %m.\n", base); + close(fd); + + return vaddr; +} + + + +#define SUBSIZE 1 +#define DATA_SIZE 5000 + + +int tx(uint32_t carrier_freq, char *audio_file, uint16_t pi, char *ps, char *rt, float ppm, float deviation, float cutoff, float preemphasis_cutoff, char *control_pipe, int raw, int pty, int onlystereo, int nords) { + // Catch all signals possible - it is vital we kill the DMA engine + // on process exit! + for (int i = 0; i < 64; i++) { + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = terminate; + sigaction(i, &sa, NULL); + } + + dma_reg = map_peripheral(DMA_VIRT_BASE, DMA_LEN); + pwm_reg = map_peripheral(PWM_VIRT_BASE, PWM_LEN); + clk_reg = map_peripheral(CLK_VIRT_BASE, CLK_LEN); + gpio_reg = map_peripheral(GPIO_VIRT_BASE, GPIO_LEN); + + // Use the mailbox interface to the VC to ask for physical memory. + mbox.handle = mbox_open(); + if (mbox.handle < 0) + fatal("Failed to open mailbox. Check kernel support for vcio / BCM2708 mailbox.\n"); + printf("Allocating physical memory: size = %d ", NUM_PAGES * 4096); + if(! (mbox.mem_ref = mem_alloc(mbox.handle, NUM_PAGES * 4096, 4096, MEM_FLAG))) { + fatal("Could not allocate memory.\n"); + } + // TODO: How do we know that succeeded? + printf("mem_ref = %u ", mbox.mem_ref); + if(! (mbox.bus_addr = mem_lock(mbox.handle, mbox.mem_ref))) { + fatal("Could not lock memory.\n"); + } + printf("bus_addr = %x ", mbox.bus_addr); + if(! (mbox.virt_addr = mapmem(BUS_TO_PHYS(mbox.bus_addr), NUM_PAGES * 4096))) { + fatal("Could not map memory.\n"); + } + printf("virt_addr = %p\n", mbox.virt_addr); + + + // GPIO4 needs to be ALT FUNC 0 to output the clock + gpio_reg[GPFSEL0] = (gpio_reg[GPFSEL0] & ~(7 << 12)) | (4 << 12); + + // Program GPCLK to use MASH setting 1, so fractional dividers work + clk_reg[GPCLK_CNTL] = 0x5A << 24 | 6; + udelay(100); + clk_reg[GPCLK_CNTL] = 0x5A << 24 | 1 << 9 | 1 << 4 | 6; + + ctl = (struct control_data_s *) mbox.virt_addr; + dma_cb_t *cbp = ctl->cb; + uint32_t phys_sample_dst = CM_GP0DIV; + uint32_t phys_pwm_fifo_addr = PWM_PHYS_BASE + 0x18; + + + // Calculate the frequency control word + // The fractional part is stored in the lower 12 bits + uint32_t freq_ctl = ((float)(PLLFREQ / carrier_freq)) * ( 1 << 12 ); + + + for (int i = 0; i < NUM_SAMPLES; i++) { + ctl->sample[i] = 0x5a << 24 | freq_ctl; // Silence + // Write a frequency sample + cbp->info = BCM2708_DMA_NO_WIDE_BURSTS | BCM2708_DMA_WAIT_RESP; + cbp->src = mem_virt_to_phys(ctl->sample + i); + cbp->dst = phys_sample_dst; + cbp->length = 4; + cbp->stride = 0; + cbp->next = mem_virt_to_phys(cbp + 1); + cbp++; + // Delay + cbp->info = BCM2708_DMA_NO_WIDE_BURSTS | BCM2708_DMA_WAIT_RESP | BCM2708_DMA_D_DREQ | BCM2708_DMA_PER_MAP(5); + cbp->src = mem_virt_to_phys(mbox.virt_addr); + cbp->dst = phys_pwm_fifo_addr; + cbp->length = 4; + cbp->stride = 0; + cbp->next = mem_virt_to_phys(cbp + 1); + cbp++; + } + cbp--; + cbp->next = mem_virt_to_phys(mbox.virt_addr); + + // Here we define the rate at which we want to update the GPCLK control + // register. + // + // Set the range to 2 bits. PLLD is at 500 MHz, therefore to get 228 kHz + // we need a divisor of 500000 / 2 / 228 = 1096.491228 + // + // This is 1096 + 2012*2^-12 theoretically + // + // However the fractional part may have to be adjusted to take the actual + // frequency of your Pi's oscillator into account. For example on my Pi, + // the fractional part should be 1916 instead of 2012 to get exactly + // 228 kHz. However RDS decoding is still okay even at 2012. + // + // So we use the 'ppm' parameter to compensate for the oscillator error + + float divider = (500000./(2*228*(1.+ppm/1.e6))); + uint32_t idivider = (uint32_t) divider; + uint32_t fdivider = (uint32_t) ((divider - idivider)*pow(2, 12)); + + printf("ppm corr is %.4f, divider is %.4f (%d + %d*2^-12) [nominal 1096.4912].\n", + ppm, divider, idivider, fdivider); + + pwm_reg[PWM_CTL] = 0; + udelay(10); + clk_reg[PWMCLK_CNTL] = 0x5A000006; // Source=PLLD and disable + udelay(100); + // theorically : 1096 + 2012*2^-12 + clk_reg[PWMCLK_DIV] = 0x5A000000 | (idivider<<12) | fdivider; + udelay(100); + clk_reg[PWMCLK_CNTL] = 0x5A000216; // Source=PLLD and enable + MASH filter 1 + udelay(100); + pwm_reg[PWM_RNG1] = 2; + udelay(10); + pwm_reg[PWM_DMAC] = PWMDMAC_ENAB | PWMDMAC_THRSHLD; + udelay(10); + pwm_reg[PWM_CTL] = PWMCTL_CLRF; + udelay(10); + pwm_reg[PWM_CTL] = PWMCTL_USEF1 | PWMCTL_PWEN1; + udelay(10); + + + // Initialise the DMA + dma_reg[DMA_CS] = BCM2708_DMA_RESET; + udelay(10); + dma_reg[DMA_CS] = BCM2708_DMA_INT | BCM2708_DMA_END; + dma_reg[DMA_CONBLK_AD] = mem_virt_to_phys(ctl->cb); + dma_reg[DMA_DEBUG] = 7; // clear debug error flags + dma_reg[DMA_CS] = 0x10880001; // go, mid priority, wait for outstanding writes + + + uint32_t last_cb = (uint32_t)ctl->cb; + + // Data structures for baseband data + float data[DATA_SIZE]; + float rdsdata[DATA_SIZE]; + int data_len = 0; + int data_index = 0; + + // Initialize the baseband generator + if(fm_mpx_open(audio_file, DATA_SIZE, cutoff, preemphasis_cutoff, raw, onlystereo, nords) < 0) return 1; + + // Initialize the RDS modulator + char myps[9] = {0}; + set_rds_pi(pi); + set_rds_rt(rt); + set_rds_pty(pty); + uint16_t count = 0; + uint16_t count2 = 0; + int varying_ps = 0; + + if(ps) { + set_rds_ps(ps); + printf("PI: %04X, PS: \"%s\".\n", pi, ps); + } else { + printf("PI: %04X, PS: .\n", pi); + varying_ps = 1; + } + printf("RT: \"%s\"\n", rt); + + // Initialize the control pipe reader + if(control_pipe) { + if(open_control_pipe(control_pipe) == 0) { + printf("Reading control commands on %s.\n", control_pipe); + } else { + printf("Failed to open control pipe: %s.\n", control_pipe); + control_pipe = NULL; + } + } + + + printf("Starting to transmit on %3.1f MHz.\n", carrier_freq/1e6); + + for (;;) { + // Default (varying) PS + if(varying_ps) { + if(count == 512) { + snprintf(myps, 9, "%08d", count2); + set_rds_ps(myps); + count2++; + } + if(count == 1024) { + set_rds_ps("RPi-Live"); + count = 0; + } + count++; + } + + if(control_pipe && poll_control_pipe() == CONTROL_PIPE_PS_SET) { + varying_ps = 0; + } + + usleep(5000); + + uint32_t cur_cb = mem_phys_to_virt(dma_reg[DMA_CONBLK_AD]); + int last_sample = (last_cb - (uint32_t)mbox.virt_addr) / (sizeof(dma_cb_t) * 2); + int this_sample = (cur_cb - (uint32_t)mbox.virt_addr) / (sizeof(dma_cb_t) * 2); + int free_slots = this_sample - last_sample; + + if (free_slots < 0) + free_slots += NUM_SAMPLES; + + while (free_slots >= SUBSIZE) { + // get more baseband samples if necessary + if(data_len == 0) { + if( fm_mpx_get_samples(data, rdsdata) < 0 ) { + //terminate(0); + return 0; + } + data_len = DATA_SIZE; + data_index = 0; + } + + float dval = data[data_index] * (deviation / 10.); + data_index++; + data_len--; + + int intval = (int)((floor)(dval)); + //int frac = (int)((dval - (float)intval) * SUBSIZE); + + + ctl->sample[last_sample++] = (0x5A << 24 | freq_ctl) + intval; //(frac > j ? intval + 1 : intval); + if (last_sample == NUM_SAMPLES) + last_sample = 0; + + free_slots -= SUBSIZE; + } + last_cb = (uint32_t)mbox.virt_addr + last_sample * sizeof(dma_cb_t) * 2; + } + + return 0; +} + +#define PREEMPHASIS_EU 3185 +#define PREEMPHASIS_US 2120 + +#define CUTOFF_COMPLIANT 15000 +#define CUTOFF_QUALITY 22050 + + +int main(int argc, char **argv) { + char *audio_file = NULL; + char *control_pipe = NULL; + uint32_t carrier_freq = 107900000; + char *ps = NULL; + char *rt = "PiFmAdv: live FM-RDS transmission from the RaspberryPi"; + uint16_t pi = 0x1234; + float ppm = 0; + float deviation = 75.0; + float cutoff = CUTOFF_COMPLIANT; + float preemphasis_cutoff = PREEMPHASIS_EU; + int raw = 0; + int pty = 15; + int onlystereo = 0; + int nords = 0; + + + + // Parse command-line arguments + for(int i=1; i 108e6) + warn("Frequency should be in megahertz between 76.0 and 108.0, but is %f MHz\n", atof(param)); + } else if(strcmp("-pi", arg)==0 && param != NULL) { + i++; + pi = (uint16_t) strtol(param, NULL, 16); + } else if(strcmp("-ps", arg)==0 && param != NULL) { + i++; + ps = param; + } else if(strcmp("-rt", arg)==0 && param != NULL) { + i++; + rt = param; + } else if(strcmp("-dev", arg)==0 && param != NULL) { + i++; + deviation = atof(param); + } else if(strcmp("-ppm", arg)==0 && param != NULL) { + i++; + ppm = atof(param); + } else if(strcmp("-preemph", arg)==0 && param != NULL) { + i++; + if(strcmp("eu", param)==0) { + preemphasis_cutoff = PREEMPHASIS_EU; + } else if(strcmp("us", param)==0) { + preemphasis_cutoff = PREEMPHASIS_US; + } + else { + preemphasis_cutoff = atof(param); + } + } else if(strcmp("-cutoff", arg)==0 && param != NULL) { + i++; + if(strcmp("compliant", param)==0) { + cutoff = CUTOFF_COMPLIANT; + } else if(strcmp("quality", param)==0) { + cutoff = CUTOFF_QUALITY; + } + else { + cutoff = atof(param); + } + } else if(strcmp("-ctl", arg)==0 && param != NULL) { + i++; + control_pipe = param; + } else if(strcmp("-raw", arg)==0) { //expect raw input of 44.1khz, 2 channels, 16 bit pcm. + i++; + raw = 1; + } else if(strcmp("-onlystereo", arg)==0) { + i++; + onlystereo = 1; + } else if(strcmp("-nords", arg)==0) { + i++; + nords = 1; + } else if(strcmp("-pty", arg)==0 && param != NULL) { + i++; + pty = atof(param); + } else { + fatal("Unrecognised argument: %s.\n" + "Syntax: pi_fm_adv [-freq freq] [-audio file] [-ppm ppm_error] [-pi pi_code]\n" + " [-ps ps_text] [-rt rt_text] [-pty program type] [-dev deviation] [-raw]\n" + " [-cutoff cutoff_freq] [-preemph preemphasis_mode] [-ctl control_pipe] [-onlystereo]\n", arg); + } + } + + int errcode = tx(carrier_freq, audio_file, pi, ps, rt, ppm, deviation, cutoff, preemphasis_cutoff, control_pipe, raw, pty, onlystereo, nords); + + terminate(errcode); +} diff --git a/src/pulses.wav b/src/pulses.wav new file mode 100644 index 0000000..c910e6c Binary files /dev/null and b/src/pulses.wav differ diff --git a/src/rds.c b/src/rds.c new file mode 100644 index 0000000..c584fed --- /dev/null +++ b/src/rds.c @@ -0,0 +1,297 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include +#include +#include +#include "waveforms.h" + +#define RT_LENGTH 64 +#define PS_LENGTH 8 +#define GROUP_LENGTH 4 + +struct { + uint16_t pi; + int ta; + char ps[PS_LENGTH]; + char rt[RT_LENGTH]; + int pty; +} rds_params = { 0 }; +/* Here, the first member of the struct must be a scalar to avoid a + warning on -Wmissing-braces with GCC < 4.8.3 + (bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=53119) +*/ + +/* The RDS error-detection code generator polynomial is + x^10 + x^8 + x^7 + x^5 + x^4 + x^3 + x^0 +*/ +#define POLY 0x1B9 +#define POLY_DEG 10 +#define MSB_BIT 0x8000 +#define BLOCK_SIZE 16 + +#define BITS_PER_GROUP (GROUP_LENGTH * (BLOCK_SIZE+POLY_DEG)) +#define SAMPLES_PER_BIT 192 +#define FILTER_SIZE (sizeof(waveform_biphase)/sizeof(float)) +#define SAMPLE_BUFFER_SIZE (SAMPLES_PER_BIT + FILTER_SIZE) + +uint16_t offset_words[] = {0x0FC, 0x198, 0x168, 0x1B4}; +// We don't handle offset word C' here for the sake of simplicity + +/* Classical CRC computation */ +uint16_t crc(uint16_t block) { + uint16_t crc = 0; + + for(int j=0; j> (POLY_DEG-1)) & 1; + crc <<= 1; + if((msb ^ bit) != 0) { + crc = crc ^ POLY; + } + } + + return crc; +} + +/* Possibly generates a CT (clock time) group if the minute has just changed + Returns 1 if the CT group was generated, 0 otherwise +*/ +int get_rds_ct_group(uint16_t *blocks) { + static int latest_minutes = -1; + + // Check time + time_t now; + struct tm *utc; + + now = time (NULL); + utc = gmtime (&now); + + if(utc->tm_min != latest_minutes) { + // Generate CT group + latest_minutes = utc->tm_min; + + int l = utc->tm_mon <= 1 ? 1 : 0; + int mjd = 14956 + utc->tm_mday + + (int)((utc->tm_year - l) * 365.25) + + (int)((utc->tm_mon + 2 + l*12) * 30.6001); + + blocks[1] = 0x4400 | (mjd>>15); + blocks[2] = (mjd<<1) | (utc->tm_hour>>4); + blocks[3] = (utc->tm_hour & 0xF)<<12 | utc->tm_min<<6; + + utc = localtime(&now); + + int offset = utc->tm_gmtoff / (30 * 60); + blocks[3] |= abs(offset); + if(offset < 0) blocks[3] |= 0x20; + + //printf("Generated CT: %04X %04X %04X\n", blocks[1], blocks[2], blocks[3]); + return 1; + } else return 0; +} + +/* Creates an RDS group. This generates sequences of the form 0A, 0A, 0A, 0A, 2A, etc. + The pattern is of length 5, the variable 'state' keeps track of where we are in the + pattern. 'ps_state' and 'rt_state' keep track of where we are in the PS (0A) sequence + or RT (2A) sequence, respectively. +*/ +void get_rds_group(int *buffer) { + static int state = 0; + static int ps_state = 0; + static int rt_state = 0; + uint16_t blocks[GROUP_LENGTH] = {rds_params.pi, 0, 0, 0}; + + // Generate block content + if(! get_rds_ct_group(blocks)) { // CT (clock time) has priority on other group types + if(state < 4) { + blocks[1] = 0x0400 | ps_state; + if(rds_params.ta) blocks[1] |= 0x0010; + blocks[2] = 0xCDCD; // no AF + blocks[3] = rds_params.ps[ps_state*2]<<8 | rds_params.ps[ps_state*2+1]; + ps_state++; + if(ps_state >= 4) ps_state = 0; + } else { // state == 5 + blocks[1] = 0x2400 | rt_state; + blocks[2] = rds_params.rt[rt_state*4+0]<<8 | rds_params.rt[rt_state*4+1]; + blocks[3] = rds_params.rt[rt_state*4+2]<<8 | rds_params.rt[rt_state*4+3]; + rt_state++; + if(rt_state >= 16) rt_state = 0; + } + + state++; + if(state >= 5) state = 0; + } + + switch(rds_params.pty) { + case 0: break; + case 1: blocks[1] = (blocks[1] & 0xFC1F) | 0x20; break; + case 2: blocks[1] = (blocks[1] & 0xFC1F) | 0x40; break; + case 3: blocks[1] = (blocks[1] & 0xFC1F) | 0x60; break; + case 4: blocks[1] = (blocks[1] & 0xFC1F) | 0x80; break; + case 5: blocks[1] = (blocks[1] & 0xFC1F) | 0xA0; break; + case 6: blocks[1] = (blocks[1] & 0xFC1F) | 0xC0; break; + case 7: blocks[1] = (blocks[1] & 0xFC1F) | 0xE0; break; + case 8: blocks[1] = (blocks[1] & 0xFC1F) | 0x100; break; + case 9: blocks[1] = (blocks[1] & 0xFC1F) | 0x120; break; + case 10: blocks[1] = (blocks[1] & 0xFC1F) | 0x140; break; + case 11: blocks[1] = (blocks[1] & 0xFC1F) | 0x160; break; + case 12: blocks[1] = (blocks[1] & 0xFC1F) | 0x180; break; + case 13: blocks[1] = (blocks[1] & 0xFC1F) | 0x1A0; break; + case 14: blocks[1] = (blocks[1] & 0xFC1F) | 0x1C0; break; + case 15: blocks[1] = (blocks[1] & 0xFC1F) | 0x1E0; break; + case 16: blocks[1] = (blocks[1] & 0xFC1F) | 0x200; break; + case 17: blocks[1] = (blocks[1] & 0xFC1F) | 0x220; break; + case 18: blocks[1] = (blocks[1] & 0xFC1F) | 0x240; break; + case 19: blocks[1] = (blocks[1] & 0xFC1F) | 0x260; break; + case 20: blocks[1] = (blocks[1] & 0xFC1F) | 0x280; break; + case 21: blocks[1] = (blocks[1] & 0xFC1F) | 0x2A0; break; + case 22: blocks[1] = (blocks[1] & 0xFC1F) | 0x2C0; break; + case 23: blocks[1] = (blocks[1] & 0xFC1F) | 0x2E0; break; + case 24: blocks[1] = (blocks[1] & 0xFC1F) | 0x300; break; + case 25: blocks[1] = (blocks[1] & 0xFC1F) | 0x320; break; + case 26: blocks[1] = (blocks[1] & 0xFC1F) | 0x340; break; + case 27: blocks[1] = (blocks[1] & 0xFC1F) | 0x360; break; + case 28: blocks[1] = (blocks[1] & 0xFC1F) | 0x380; break; + case 29: blocks[1] = (blocks[1] & 0xFC1F) | 0x3A0; break; + case 30: blocks[1] = (blocks[1] & 0xFC1F) | 0x3C0; break; + case 31: blocks[1] = (blocks[1] & 0xFC1F) | 0x3E0; break; + } + + // Calculate the checkword for each block and emit the bits + for(int i=0; i= SAMPLES_PER_BIT) { + if(bit_pos >= BITS_PER_GROUP) { + get_rds_group(bit_buffer); + bit_pos = 0; + } + + // do differential encoding + cur_bit = bit_buffer[bit_pos]; + prev_output = cur_output; + cur_output = prev_output ^ cur_bit; + + inverting = (cur_output == 1); + + float *src = waveform_biphase; + int idx = in_sample_index; + + for(int j=0; j= SAMPLE_BUFFER_SIZE) idx = 0; + } + + in_sample_index += SAMPLES_PER_BIT; + if(in_sample_index >= SAMPLE_BUFFER_SIZE) in_sample_index -= SAMPLE_BUFFER_SIZE; + + bit_pos++; + sample_count = 0; + } + + float sample = sample_buffer[out_sample_index]; + sample_buffer[out_sample_index] = 0; + out_sample_index++; + if(out_sample_index >= SAMPLE_BUFFER_SIZE) out_sample_index = 0; + + + // modulate at 57 kHz + // use phase for this + switch(phase) { + case 0: + case 2: sample = 0; break; + case 1: break; + case 3: sample = -sample; break; + } + phase++; + if(phase >= 4) phase = 0; + + *buffer++ = sample; + sample_count++; + } +} + +void set_rds_pi(uint16_t pi_code) { + rds_params.pi = pi_code; +} + +void set_rds_rt(char *rt) { + strncpy(rds_params.rt, rt, 64); + for(int i=0; i<64; i++) { + if(rds_params.rt[i] == 0) rds_params.rt[i] = 32; + } +} + +void set_rds_ps(char *ps) { + strncpy(rds_params.ps, ps, 8); + for(int i=0; i<8; i++) { + if(rds_params.ps[i] == 0) rds_params.ps[i] = 32; + } +} + +void set_rds_pty(int pty) { + rds_params.pty = pty; +} + +void set_rds_ta(int ta) { + rds_params.ta = ta; +} diff --git a/src/rds.h b/src/rds.h new file mode 100644 index 0000000..3943d9e --- /dev/null +++ b/src/rds.h @@ -0,0 +1,38 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef RDS_H +#define RDS_H + + +#include + +extern void get_rds_samples(float *buffer, int count); +extern void set_rds_pi(uint16_t pi_code); +extern void set_rds_rt(char *rt); +extern void set_rds_ps(char *ps); +extern void set_rds_ta(int ta); +void set_rds_pty(int pty); + + +#endif /* RDS_H */ diff --git a/src/rds_wav.c b/src/rds_wav.c new file mode 100644 index 0000000..d8649f4 --- /dev/null +++ b/src/rds_wav.c @@ -0,0 +1,101 @@ +/* + PiFmAdv - FM/RDS transmitter for the Raspberry Pi + Copyright (C) 2017 Miegl + + See https://github.com/Miegl/PiFmAdv + + rds_wav.c is a test program that writes a RDS baseband signal to a WAV + file. It requires libsndfile. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + + +#include +#include +#include +#include +#include + +#include "rds.h" +#include "fm_mpx.h" + + +#define LENGTH 114000 + + +/* Simple test program */ +int main(int argc, char **argv) { + if(argc < 4) { + fprintf(stderr, "Error: missing argument.\n"); + fprintf(stderr, "Syntax: rds_wav \n"); + return EXIT_FAILURE; + } + + set_rds_pi(0x1234); + set_rds_ps(argv[3]); + set_rds_rt(argv[3]); + + char *in_file = argv[1]; + if(strcmp("NONE", argv[1]) == 0) in_file = NULL; + + if(fm_mpx_open(in_file, LENGTH) != 0) { + printf("Could not setup FM mulitplex generator.\n"); + return EXIT_FAILURE; + } + + + + // Set the format of the output file + SNDFILE *outf; + SF_INFO sfinfo; + + sfinfo.frames = LENGTH; + sfinfo.samplerate = 228000; + sfinfo.channels = 1; + sfinfo.format = SF_FORMAT_WAV | SF_FORMAT_PCM_16; + sfinfo.sections = 1; + sfinfo.seekable = 0; + + // Open the output file + char *out_file = argv[2]; + if (! (outf = sf_open(out_file, SFM_WRITE, &sfinfo))) { + fprintf(stderr, "Error: could not open output file %s.\n", out_file); + return EXIT_FAILURE; + } + + float mpx_buffer[LENGTH]; + + for(int j=0; j<40; j++) { + if( fm_mpx_get_samples(mpx_buffer) < 0 ) break; + + // scale samples + for(int i=0; i