├── README.md ├── pru-ssb ├── Makefile ├── cape-bone-sdr.dts ├── sdr-helper.p ├── sdr.c └── sdr.p └── teensy_ssb └── teensy_ssb.ino /README.md: -------------------------------------------------------------------------------- 1 | ![Soft SSB being received with fldigi](http://farm4.staticflickr.com/3732/12551247315_49f8957bd6_z_d.jpg) 2 | 3 | Software SSB 4 | ============ 5 | 6 | *This is a work in progress* 7 | 8 | Teensy SSB 9 | ---------- 10 | On the teensy 3.1, this implements a software defined radio transmitter 11 | that encodes data as PSK31 audio in the 1 KHz range, modulates it using 12 | SSB and then uses the builtin DAC hardware to transmit it via a wire 13 | antenna. 14 | 15 | Due to limitations of the DAC and the current software design, the 16 | output frequency is quite low. Roughly 64 KHz, using 8 steps in the 17 | carrier wave. This has successfully transmitted a short distance to 18 | a SSB receiver and the PSK31 signal decoded by fldigi. 19 | 20 | 21 | PRU SSB 22 | ------------------------ 23 | The BeagleBone Black PRU is similar to the teensy design. 24 | The userspace SDR code performs the encoding and modulation, and 25 | writes the values to a shared buffer. The PRU just does parallel 26 | output to an external DAC. 27 | 28 | This could potentially use the LCD hardware to clock out parallel 29 | data at a high rate, although the extra capacitance from the HDMI 30 | framer makes it unsuitable for high-speed digital IO. 31 | -------------------------------------------------------------------------------- /pru-ssb/Makefile: -------------------------------------------------------------------------------- 1 | ######### 2 | # 3 | # The top level targets link in the two .o files for now. 4 | # 5 | TARGETS += sdr 6 | LEDSCAPE_DIR ?= $(HOME)/LEDscape 7 | 8 | eventmap.LDLIBS := \ 9 | -lX11 \ 10 | -lXtst \ 11 | 12 | all: $(TARGETS) sdr.bin sdr-helper.bin 13 | 14 | ifeq ($(shell uname -m),armv7l) 15 | # We are on the BeagleBone Black itself; 16 | # do not cross compile. 17 | export CROSS_COMPILE:= 18 | else 19 | # We are not on the BeagleBone and might be cross compiling. 20 | # If the environment does not set CROSS_COMPILE, set our 21 | # own. Install a cross compiler with something like: 22 | # 23 | # sudo apt-get install gcc-arm-linux-gnueabi 24 | # 25 | export CROSS_COMPILE?=arm-linux-gnueabi- 26 | endif 27 | 28 | CFLAGS += \ 29 | -std=c99 \ 30 | -g \ 31 | -W \ 32 | -Wall \ 33 | -D_BSD_SOURCE \ 34 | -Wp,-MMD,$(dir $@).$(notdir $@).d \ 35 | -Wp,-MT,$@ \ 36 | -I. \ 37 | -I$(LEDSCAPE_DIR) \ 38 | -O2 \ 39 | -mtune=cortex-a8 \ 40 | -march=armv7-a \ 41 | 42 | LDFLAGS += \ 43 | 44 | LDLIBS += \ 45 | -lm \ 46 | -lpthread \ 47 | 48 | 49 | COMPILE.o = $(CROSS_COMPILE)gcc $(CFLAGS) -c -o $@ $< 50 | COMPILE.a = $(CROSS_COMPILE)gcc -c -o $@ $< 51 | COMPILE.link = $(CROSS_COMPILE)gcc $(LDFLAGS) -o $@ $^ $(LDLIBS) $($@.LDLIBS) 52 | 53 | 54 | ##### 55 | # 56 | # The TI "app_loader" is the userspace library for talking to 57 | # the PRU and mapping memory between it and the ARM. 58 | # 59 | APP_LOADER_DIR ?= $(LEDSCAPE_DIR)/am335x/app_loader 60 | APP_LOADER_LIB := $(APP_LOADER_DIR)/lib/libprussdrv.a 61 | CFLAGS += -I$(APP_LOADER_DIR)/include 62 | LDLIBS += $(APP_LOADER_LIB) 63 | 64 | ##### 65 | # 66 | # The TI PRU assembler looks like it has macros and includes, 67 | # but it really doesn't. So instead we use cpp to pre-process the 68 | # file and then strip out all of the directives that it adds. 69 | # PASM also doesn't handle multiple statements per line, so we 70 | # insert hard newline characters for every ; in the file. 71 | # 72 | PASM_DIR ?= $(LEDSCAPE_DIR)/am335x/pasm 73 | PASM := $(PASM_DIR)/pasm 74 | 75 | %.bin: %.p $(PASM) 76 | $(CPP) \ 77 | -I$(LEDSCAPE_DIR)/ \ 78 | - \ 79 | < $< \ 80 | | perl -p -e 's/^#.*//; s/;/\n/g; s/BYTE\((\d+)\)/t\1/g' > $<.i 81 | $(PASM) -V3 -b $<.i $(basename $@) 82 | $(RM) $<.i 83 | 84 | %.o: %.c 85 | $(COMPILE.o) 86 | 87 | $(foreach O,$(TARGETS),$(eval $O: $O.o $(LEDSCAPE_DIR)/pru.o $(APP_LOADER_LIB))) 88 | 89 | $(TARGETS): 90 | $(COMPILE.link) 91 | 92 | 93 | .PHONY: clean 94 | 95 | clean: 96 | rm -rf \ 97 | *.o \ 98 | *.i \ 99 | .*.o.d \ 100 | *~ \ 101 | *.bin \ 102 | $(INCDIR_APP_LOADER)/*~ \ 103 | $(TARGETS) \ 104 | 105 | 106 | ########### 107 | # 108 | # PRU Libraries and PRU assembler are build from their own trees. 109 | # 110 | $(APP_LOADER_LIB): 111 | $(MAKE) -C $(APP_LOADER_DIR)/interface 112 | 113 | $(PASM): 114 | $(MAKE) -C $(PASM_DIR) 115 | 116 | # Compile and load the device tree file 117 | CAPE=cape-bone-sdr 118 | FIRMWARE=/lib/firmware/$(CAPE)-00A0.dtbo 119 | #$(FIRMWARE): $(CAPE).dts 120 | # dtc -I dts -O dtb $< > $@ 121 | $(FIRMWARE): FORCE 122 | dtc -@ -I dts -O dtb -o $@ $(CAPE).dts 123 | 124 | FORCE: 125 | 126 | firmware: $(FIRMWARE) 127 | echo $(CAPE) > /sys/devices/bone_capemgr.8/slots 128 | 129 | # Include all of the generated dependency files 130 | -include .*.o.d 131 | -------------------------------------------------------------------------------- /pru-ssb/cape-bone-sdr.dts: -------------------------------------------------------------------------------- 1 | /* 2 | * pru dts file BB-BONE-PRU-00A0.dts 3 | * 4 | * Available outputs on pru 0 and their r30 pins: 5 | * p9.31: 0 6 | * p9.29: 1 7 | * p9.30: 2 8 | * p9.28: 3 9 | * p9.27: 5 10 | * p9.25: 7 11 | * p8.12: 14 12 | * p8.11: 15 13 | */ 14 | /dts-v1/; 15 | /plugin/; 16 | 17 | / { 18 | compatible = "ti,beaglebone", "ti,beaglebone-black"; 19 | 20 | /* identification */ 21 | part-number = "BB-BONE-SDR"; 22 | version = "00A0"; 23 | 24 | exclusive-use = 25 | "P8.11", "P8.12", "P9.25", "P2.27", "P9.28", "P9.30", "P9.31"; 26 | 27 | fragment@0 { 28 | target = <&am33xx_pinmux>; 29 | __overlay__ { 30 | mygpio: pinmux_mygpio { 31 | pinctrl-single,pins = < 32 | 0x034 0x06 // p8.11, why are these two mode 6? 33 | 0x030 0x06 // p8.12 34 | 0x1ac 0x05 // p9.25 35 | 0x1a4 0x05 // p9.27 36 | 0x19c 0x05 // p9.28 37 | 0x194 0x05 // p9.29 38 | 0x198 0x05 // p9.30 39 | 0x190 0x05 // p9.31 40 | >; 41 | }; 42 | }; 43 | }; 44 | 45 | fragment@1 { 46 | target = <&ocp>; 47 | __overlay__ { 48 | test_helper: helper { 49 | compatible = "bone-pinmux-helper"; 50 | pinctrl-names = "default"; 51 | pinctrl-0 = <&mygpio>; 52 | status = "okay"; 53 | }; 54 | }; 55 | }; 56 | 57 | fragment@2{ 58 | target = <&pruss>; 59 | __overlay__ { 60 | status = "okay"; 61 | }; 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /pru-ssb/sdr-helper.p: -------------------------------------------------------------------------------- 1 | // \file 2 | /* PRU1 helps PRU0 by copying from DDR to SHAREDRAM. 3 | * This avoids latency issues with reading from DDR (100-200ns), 4 | * while allowing the user space application to take advantage of 5 | * this faster access to the DDR. 6 | * 7 | */ 8 | .origin 0 9 | .entrypoint START 10 | 11 | #include "ws281x.hp" 12 | 13 | /** Register map */ 14 | #define zero r0 15 | #define data_addr r1 16 | #define offset r2 17 | #define out_offset r3 18 | #define length r4 19 | #define shared_ram r5 20 | #define frame r6 21 | 22 | 23 | #define NOP ADD r0, r0, 0 24 | 25 | 26 | START: 27 | // Enable OCP master port 28 | // clear the STANDBY_INIT bit in the SYSCFG register, 29 | // otherwise the PRU will not be able to write outside the 30 | // PRU memory space and to the BeagleBon's pins. 31 | LBCO r0, C4, 4, 4 32 | CLR r0, r0, 4 33 | SBCO r0, C4, 4, 4 34 | 35 | // Configure the programmable pointer register for PRU0 by setting 36 | // c28_pointer[15:0] field to 0x0100. This will make C28 point to 37 | // 0x00010000 (PRU shared RAM). 38 | MOV r0, 0x00000120 39 | MOV r1, CTPPR_0 40 | ST32 r0, r1 41 | 42 | // Configure the programmable pointer register for PRU0 by setting 43 | // c31_pointer[15:0] field to 0x0010. This will make C31 point to 44 | // 0x80001000 (DDR memory). 45 | MOV r0, 0x00100000 46 | MOV r1, CTPPR_1 47 | ST32 r0, r1 48 | 49 | MOV zero, 0 50 | MOV frame, 0 51 | MOV length, 4096 52 | MOV shared_ram, 0x10000 53 | SBBO zero, shared_ram, 0, 4 54 | 55 | restart: 56 | LBCO data_addr, CONST_PRUDRAM, 0, 4 57 | QBEQ restart, data_addr, 0 58 | 59 | wait_loop: 60 | // Wait for the other PRU to indicate that it is halfway 61 | LBBO offset, shared_ram, 0, 4 62 | QBNE wait_loop, offset, 0 63 | 64 | // Let the user know we have read the command message 65 | // and are making progress on it 66 | SBCO zero, CONST_PRUDRAM, 0, 4 67 | 68 | // alternate between two different buffers 69 | MOV out_offset, 8192 70 | XOR frame, frame, 1 71 | QBEQ other_frame, frame, 0 72 | MOV out_offset, 1024 73 | other_frame: 74 | 75 | MOV offset, 0 76 | 77 | read_loop: 78 | // Read 16 registers, 64 bytes worth of data 79 | LBBO r10, data_addr, offset, 4*16 80 | SBBO r10, shared_ram, out_offset, 4*16 81 | ADD offset, offset, 4*16 82 | ADD out_offset, out_offset, 4*16 83 | QBNE read_loop, offset, length 84 | 85 | // Write the command to the other PRU 86 | // should give it the offset where we started 87 | SUB out_offset, out_offset, length 88 | SBBO out_offset, shared_ram, 0, 4 89 | 90 | // Wait for a new command 91 | QBA restart 92 | 93 | EXIT: 94 | #ifdef AM33XX 95 | // Send notification to Host for program completion 96 | MOV R31.b0, PRU0_ARM_INTERRUPT+16 97 | #else 98 | MOV R31.b0, PRU0_ARM_INTERRUPT 99 | #endif 100 | 101 | HALT 102 | -------------------------------------------------------------------------------- /pru-ssb/sdr.c: -------------------------------------------------------------------------------- 1 | /** \file 2 | * Software defined radio for the BeagleBone Black PRU. 3 | * 4 | * Generates a waveform and pokes it to the PRU for output. 5 | * Requires the userspace to shuffle the bits before handing 6 | * them to the PRU for output. 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "pru.h" 14 | 15 | static int sin_table[128]; 16 | 17 | static inline uint32_t 18 | now(void) 19 | { 20 | static uint32_t start_epoch; 21 | struct timeval tv; 22 | gettimeofday(&tv, NULL); 23 | if (start_epoch == 0) 24 | start_epoch = tv.tv_sec; 25 | return (tv.tv_sec - start_epoch) * 1000000 + tv.tv_usec; 26 | } 27 | 28 | static uint16_t 29 | shuffle( 30 | uint8_t x_in 31 | ) 32 | { 33 | uint16_t x = x_in >> 1; // ignore bottom bit; only have 7 wired 34 | uint16_t y = 0 35 | | (x & 0x0F) << 0 36 | | (x & 0x10) << 1 37 | | (x & 0x20) << 2 38 | | (x & 0xC0) << 8 39 | ; 40 | 41 | return y; 42 | } 43 | 44 | 45 | static const uint8_t psk_bits[] = "" 46 | "1010110100" // C 47 | "11101110100" // Q 48 | "100" // space 49 | "1010110100" // C 50 | "11101110100" // Q 51 | "100" // space 52 | "1101110100" // N 53 | "10111101100" // Y 54 | "1111111100" // 3 55 | "10101011100" // U 56 | "100" // space 57 | ; 58 | 59 | 60 | /** Output a PSK31 encoded bit. 61 | * Phase controls which sin/cos is used. 62 | * ramp_up will ramp up over a few ms 63 | * ramp_down will ramp down over a few ms 64 | */ 65 | static void 66 | generate_bit( 67 | uint16_t * const buf, 68 | int phase, 69 | int ramp_up, 70 | int ramp_down 71 | ) 72 | { 73 | // 32 ms per bit == 313 passes at 102.4 usec per pass 74 | for (unsigned j = 0 ; j < 313 ; j++) 75 | { 76 | int mag = 1024; 77 | 78 | if (ramp_up && j < 128) 79 | mag = sin_table[j/4]; 80 | if (ramp_down && j > 313 - 128) 81 | mag = sin_table[(313 - j - 1)/4]; 82 | 83 | for (unsigned i = 0 ; i < 4096 ; i++) 84 | { 85 | // 25 ns per output, 65536 outputs per cycle 86 | // 1638400 ns per cycle == 610 Hz 87 | unsigned sig_ti = ((j << 12) + i) / (313*8); 88 | if (phase) 89 | sig_ti += 64; 90 | 91 | int sig_sin = +sin_table[(sig_ti + 0) % 128]; 92 | int sig_cos = -sin_table[(sig_ti + 32) % 128]; 93 | 94 | // 25 ns per output, 16 outputs per cycle 95 | // 400 ns per cycle == 2.5 MHz 96 | // Carrier frequency == 1e9 / (25 ns * (128 / N)) 97 | // f = 1e9 * N / (128 * 25) 98 | // N = f * 128 * 25 / 1e9 99 | //int c_ti = i*16; // 5 MHz 100 | int c_ti = i*8; // 2.5 MHz 101 | int c_sin = +sin_table[(c_ti + 0) % 128]; 102 | int c_cos = -sin_table[(c_ti + 32) % 128]; 103 | 104 | // 102.4 us per pass 105 | // 512 Hz = 1953 us 106 | int ssb = (c_sin * sig_cos - c_cos * sig_sin) / 1024; 107 | 108 | // if we're ramping, scale it 109 | ssb = (ssb * mag) / 1024; 110 | 111 | // ssb has range -1024 to 1024; 112 | // convert to 0 to 256 113 | //buf[4096*j + i] = shuffle(ssb / 8 + 127); 114 | buf[4096*j + i] = shuffle(sin_table[(i*8) % 128] / 16 + 127); 115 | //int val = (c_sin/16 + 127); 116 | //if (i > 1500 && i < 2048) val = 32; 117 | //if (i > 2048 && i < 2400) val = 64+128; 118 | //buf[4096*j + i] = shuffle(val); 119 | //buf[4096*j + i] = shuffle(j & 1 ? (128 - j%64) : 128 + j%64); 120 | //int test = +sin_table[(sig_ti + 0) % 128]; 121 | //buf[4096*j + i] = shuffle(test / 8 + 127); 122 | //buf[4096*j + i] = shuffle(i & 0xFF); 123 | 124 | // mark an unused bit; the pru will zero 125 | // this when it is halfway through the buffer. 126 | } 127 | } 128 | } 129 | 130 | 131 | static void 132 | output_bit( 133 | volatile uintptr_t * const pru_cmd, 134 | volatile uint8_t * const pru_buf, 135 | const uintptr_t pru_buf_addr, 136 | const uint16_t * bit_buf 137 | ) 138 | { 139 | static unsigned frame_num = 0; 140 | static uint32_t last_out; 141 | 142 | // send every 3rd section. This keeps up with the correct 143 | // bit timings. total hack until we figure out a faster 144 | // way to send the data to the PRU. 145 | 146 | for (unsigned i = 0 ; i < 313 ; i++) 147 | { 148 | // wait for a signal that we're halfway 149 | // so that buf is free. 150 | 151 | const uint32_t buf_offset = (frame_num++ & 0x3) * 4096; 152 | //uint32_t delta = -now(); 153 | memcpy((void*)(pru_buf + buf_offset), &bit_buf[i*2048], 4096); 154 | 155 | //delta += now(); 156 | 157 | //last_out = now(); 158 | while(*pru_cmd) 159 | ; 160 | //uint32_t out_time = now(); 161 | 162 | *pru_cmd = pru_buf_addr + buf_offset; 163 | //printf("sent %d %u %u\n", frame_num, delta, out_time - last_out); 164 | //last_out = out_time; 165 | } 166 | } 167 | 168 | 169 | int 170 | main(void) 171 | { 172 | for (int i = 0 ; i < 128 ; i++) 173 | { 174 | sin_table[i] = sin(i * M_PI * 2 / 128) * 1024; 175 | } 176 | 177 | uint16_t * bits[8]; 178 | 179 | for (int i = 0 ; i < 8 ; i++) 180 | { 181 | bits[i] = calloc(2, 4096 * 313); 182 | if (bits[i] == NULL) 183 | fprintf(stderr, "failed\n"); 184 | } 185 | 186 | 187 | // we need bit streams for: 188 | // phase 0 ramp up 189 | // phase 0 solid 190 | // phase 0 ramp down 191 | // phase 1 ramp up 192 | // phase 1 solid 193 | // phase 1 ramp down 194 | generate_bit(bits[0], 0, 0, 0); 195 | generate_bit(bits[1], 0, 0, 1); 196 | generate_bit(bits[2], 0, 1, 0); 197 | generate_bit(bits[3], 0, 1, 1); 198 | generate_bit(bits[4], 1, 0, 0); 199 | generate_bit(bits[5], 1, 0, 1); 200 | generate_bit(bits[6], 1, 1, 0); 201 | generate_bit(bits[7], 1, 1, 1); 202 | 203 | pru_t * const pru0 = pru_init(0); 204 | pru_t * const pru1 = pru_init(1); 205 | 206 | uint32_t * const pru_cmd = pru1->data_ram; 207 | uint8_t * const vram = pru1->ddr; 208 | 209 | volatile uint16_t * const buf = (void*) pru_cmd; 210 | 211 | // ensure that both mailboxes are empty 212 | *(uint32_t*) pru0->data_ram = 0; 213 | *(uint32_t*) pru1->data_ram = 0; 214 | 215 | printf("pru %p %p\n", pru0, pru1); 216 | printf("cmd %p %p\n", pru0->data_ram, pru1->data_ram); 217 | printf("ddr %p (%08x) %p (%08x)\n", 218 | pru0->ddr, pru0->ddr_addr, 219 | pru1->ddr, pru1->ddr_addr 220 | ); 221 | 222 | pru_exec(pru0, "./sdr.bin"); 223 | pru_exec(pru1, "./sdr-helper.bin"); 224 | 225 | int offset = 0; 226 | const size_t len = sizeof(psk_bits) - 1; 227 | int old_bit = 0; 228 | int cur_bit = 0; 229 | int new_bit = 0; 230 | int phase = 0; 231 | 232 | while (1) 233 | { 234 | offset = (offset + 1) % len; 235 | old_bit = cur_bit; 236 | cur_bit = new_bit; 237 | new_bit = psk_bits[offset] == '1'; 238 | 239 | int ramp_up = 0; 240 | int ramp_down = 0; 241 | 242 | if (cur_bit == 0) 243 | { 244 | phase = !phase; 245 | ramp_up = 1; 246 | } 247 | 248 | if (new_bit == 0) 249 | ramp_down = 1; 250 | 251 | 252 | output_bit( 253 | pru_cmd, 254 | vram, 255 | pru1->ddr_addr, 256 | bits[ 0 257 | | phase << 2 258 | | ramp_up << 1 259 | | ramp_down << 0 260 | ] 261 | ); 262 | } 263 | 264 | return 0; 265 | } 266 | -------------------------------------------------------------------------------- /pru-ssb/sdr.p: -------------------------------------------------------------------------------- 1 | // \file 2 | /* PRU based SDR driver 3 | * 4 | */ 5 | .origin 0 6 | .entrypoint START 7 | 8 | #include "ws281x.hp" 9 | 10 | /** Register map */ 11 | #define zero r0 12 | #define data_addr r1 13 | #define offset r2 14 | #define length r3 15 | #define halfway r4 16 | #define shared_ram r5 17 | 18 | #define NOP ADD r0, r0, 0 19 | 20 | START: 21 | // Enable OCP master port 22 | // clear the STANDBY_INIT bit in the SYSCFG register, 23 | // otherwise the PRU will not be able to write outside the 24 | // PRU memory space and to the BeagleBon's pins. 25 | LBCO r0, C4, 4, 4 26 | CLR r0, r0, 4 27 | SBCO r0, C4, 4, 4 28 | 29 | // Configure the programmable pointer register for PRU0 by setting 30 | // c28_pointer[15:0] field to 0x0120. This will make C28 point to 31 | // 0x00012000 (PRU shared RAM). 32 | MOV r0, 0x00000120 33 | MOV r1, CTPPR_0 34 | ST32 r0, r1 35 | 36 | // Configure the programmable pointer register for PRU0 by setting 37 | // c31_pointer[15:0] field to 0x0010. This will make C31 point to 38 | // 0x80001000 (DDR memory). 39 | MOV r0, 0x00100000 40 | MOV r1, CTPPR_1 41 | ST32 r0, r1 42 | 43 | // Write a 0x1 into the response field so that they know we have started 44 | MOV r2, #0x1 45 | SBCO r2, CONST_PRUDRAM, 12, 4 46 | 47 | // ground all of our outputs 48 | MOV r30, 0 49 | MOV zero, 0 50 | 51 | // Clear out mailbox 52 | MOV shared_ram, 0x10000 53 | SBBO zero, shared_ram, 0, 4 54 | 55 | restart: 56 | // Wait for something in the shared ram region 57 | LBBO offset, shared_ram, 0, 4 58 | QBEQ restart, offset, 0 59 | 60 | MOV length, 4096 61 | ADD length, length, offset 62 | MOV halfway, 300 63 | ADD halfway, halfway, offset 64 | 65 | CLR r30, 15 66 | 67 | read_loop1: 68 | // Save some instructions -- read directly into the r30 output 69 | // each bit of output takes 25 ns; 70 | LBBO r30, shared_ram, offset, 2 // 15 ns? 71 | ADD offset, offset, 2 // 5 ns 72 | QBNE read_loop1, offset, halfway // 5 ns 73 | 74 | // signal that we're half-way through 75 | SBBO zero, shared_ram, 0, 4 76 | 77 | read_loop2: 78 | LBBO r30, shared_ram, offset, 2 // 15 ns? 79 | ADD offset, offset, 2 // 5 ns 80 | QBNE read_loop2, offset, length // 5 ns 81 | 82 | SET r30, 15 83 | QBA restart 84 | 85 | EXIT: 86 | #ifdef AM33XX 87 | // Send notification to Host for program completion 88 | MOV R31.b0, PRU0_ARM_INTERRUPT+16 89 | #else 90 | MOV R31.b0, PRU0_ARM_INTERRUPT 91 | #endif 92 | 93 | HALT 94 | -------------------------------------------------------------------------------- /teensy_ssb/teensy_ssb.ino: -------------------------------------------------------------------------------- 1 | /** \file 2 | * Software SSB transmitter for the Teensy 3.1 using the DMA engine 3 | * to drive an external DAC connected to PORTD. 4 | * 5 | * Good reference to the DMA engine: http://forum.pjrc.com/threads/23950-Parallel-GPIO-on-Teensy-3-0 6 | * GPIO mappings on the Teensy 3.1: http://forum.pjrc.com/threads/17532-Tutorial-on-digital-I-O-ATMega-PIN-PORT-DDR-D-B-registers-vs-ARM-GPIO_PDIR-_PDOR 7 | * 8 | * The DMA clocks out the bytes to the GPIO at 20 MHz. 9 | * This limits the possible output speed to about 1 MHz. 10 | * 11 | * The ramp up / ramp down for the PSK31 signal is 15 ms, which would require 12 | * too much memory to store (294 KB). Instead 64 512 byte snippets of 13 | * carrier are used, each at a different amplitude. 14 | * 15 | * The DMA engines ping-pongs between buffer 1 and buffer 2. 16 | */ 17 | 18 | #define POWER_LEVELS 32 19 | #define DMA_LENGTH 512 20 | #define FREQUENCY 64 // multiple of 40 KHz 21 | uint8_t carrier[2][POWER_LEVELS][DMA_LENGTH]; 22 | 23 | #define LED_PIN 13 24 | static inline void 25 | led(int val) 26 | { 27 | digitalWrite(LED_PIN, val ? HIGH : LOW); 28 | } 29 | 30 | 31 | void 32 | setup(void) 33 | { 34 | pinMode(LED_PIN, OUTPUT); 35 | led(1); 36 | 37 | for (int power = 0 ; power < POWER_LEVELS ; power++) 38 | { 39 | float p = sin(power * M_PI/POWER_LEVELS/2); 40 | //p = sqrt(p); // makes a faster ramp up 41 | 42 | for (int t = 0 ; t < DMA_LENGTH ; t++) 43 | { 44 | float c1 = sin(FREQUENCY * t * 2 * M_PI / DMA_LENGTH); 45 | float c2 = sin(FREQUENCY * t * 2 * M_PI / DMA_LENGTH + M_PI); 46 | //c1 = 1; 47 | //c2 = -1; 48 | if(1) { 49 | carrier[0][power][t] = c1 * p * 128 + 127; 50 | carrier[1][power][t] = c2 * p * 128 + 127; 51 | } else { 52 | carrier[0][power][t] = c1 * 128 + 127; 53 | carrier[1][power][t] = c2 * 128 + 127; 54 | } 55 | } 56 | } 57 | 58 | // configure the 8 output pins, which will be mapped 59 | // to the DMA engine. 60 | GPIOD_PCOR = 0xFF; 61 | pinMode(2, OUTPUT); // bit #0 62 | pinMode(14, OUTPUT); // bit #1 63 | pinMode(7, OUTPUT); // bit #2 64 | pinMode(8, OUTPUT); // bit #3 65 | pinMode(6, OUTPUT); // bit #4 66 | pinMode(20, OUTPUT); // bit #5 67 | pinMode(21, OUTPUT); // bit #6 68 | pinMode(5, OUTPUT); // bit #7 69 | 70 | // enable clocks to the DMA controller and DMAMUX 71 | SIM_SCGC7 |= SIM_SCGC7_DMA; 72 | //SIM_SCGC6 |= SIM_SCGC6_DMAMUX; 73 | 74 | DMA_CR = 0; 75 | 76 | DMA_ERQ |= DMA_ERQ_ERQ1 | DMA_ERQ_ERQ2; 77 | 78 | // operate byte at a time on reads and writes 79 | DMA_TCD1_ATTR = DMA_TCD_ATTR_SSIZE(0) | DMA_TCD_ATTR_DSIZE(0); 80 | DMA_TCD2_ATTR = DMA_TCD_ATTR_SSIZE(0) | DMA_TCD_ATTR_DSIZE(0); 81 | 82 | // configure both for a no power state 83 | DMA_TCD1_SADDR = carrier[0][0]; 84 | DMA_TCD2_SADDR = carrier[0][0]; 85 | 86 | DMA_TCD1_NBYTES_MLNO = DMA_LENGTH; 87 | DMA_TCD2_NBYTES_MLNO = DMA_LENGTH; 88 | 89 | // update byte at a time 90 | DMA_TCD1_SOFF = 1; 91 | DMA_TCD2_SOFF = 1; 92 | 93 | // go back to the start of the buffer after the major transfer 94 | DMA_TCD1_SLAST = -DMA_LENGTH; 95 | DMA_TCD2_SLAST = -DMA_LENGTH; 96 | 97 | // write the output to the PORTD 98 | DMA_TCD1_DADDR = &GPIOD_PDOR; 99 | DMA_TCD2_DADDR = &GPIOD_PDOR; 100 | 101 | // don't update destination at all; each byte writes to GPIOD_PDOR 102 | DMA_TCD1_DOFF = 0; 103 | DMA_TCD2_DOFF = 0; 104 | 105 | DMA_TCD1_DLASTSGA = 0; 106 | DMA_TCD2_DLASTSGA = 0; 107 | 108 | // BITER sets the number of inner loops to be run; 109 | // must be equal to CITER when start is set. 110 | DMA_TCD1_CITER_ELINKNO = 1; 111 | DMA_TCD1_BITER_ELINKNO = 1; 112 | 113 | DMA_TCD2_CITER_ELINKNO = 1; 114 | DMA_TCD2_BITER_ELINKNO = 1; 115 | 116 | // DMA1 links to DMA2 which links to DMA1 117 | DMA_TCD1_CSR = DMA_TCD_CSR_MAJORLINKCH(2) | DMA_TCD_CSR_MAJORELINK; 118 | DMA_TCD2_CSR = DMA_TCD_CSR_MAJORLINKCH(1) | DMA_TCD_CSR_MAJORELINK; 119 | 120 | // start DMA1 121 | DMA_TCD1_CSR |= DMA_TCD_CSR_START; 122 | } 123 | 124 | 125 | #if 0 126 | /** Configure DMA1 to chain to DMA2, which chains to DMA1. */ 127 | void 128 | dma_send( 129 | const void * const p1, 130 | const void * const p2, 131 | size_t len 132 | ) 133 | { 134 | DMA_TCD1_ATTR = DMA_TCD_ATTR_SSIZE(0) | DMA_TCD_ATTR_DSIZE(0); 135 | DMA_TCD1_NBYTES_MLNO = len; // one byte at a time 136 | 137 | DMA_TCD1_SADDR = p1; 138 | DMA_TCD1_SOFF = 1; // update byte at a time 139 | DMA_TCD1_SLAST = -len; // go back to the start of the buffer 140 | 141 | DMA_TCD1_DADDR = &GPIOD_PDOR; 142 | DMA_TCD1_DOFF = 0; // don't update destination 143 | DMA_TCD1_DLASTSGA = 0; 144 | 145 | // BITER sets the number of inner loops to be run; 146 | // must be equal to CITER when start is set. 147 | DMA_TCD1_CITER_ELINKNO = 1; 148 | DMA_TCD1_BITER_ELINKNO = 1; 149 | 150 | DMA_TCD1_CSR = DMA_TCD_CSR_START | (2 << 8) | (1<<5); 151 | //DMA_SSRT = DMA_SSRT_SSRT(1); 152 | 153 | DMA_TCD2_ATTR = DMA_TCD_ATTR_SSIZE(0) | DMA_TCD_ATTR_DSIZE(0); 154 | DMA_TCD2_NBYTES_MLNO = len; // one byte at a time 155 | 156 | DMA_TCD2_SADDR = p2; 157 | DMA_TCD2_SOFF = 1; // update byte at a time 158 | DMA_TCD2_SLAST = -len; // go back to the start of the buffer 159 | 160 | DMA_TCD2_DADDR = &GPIOD_PDOR; 161 | DMA_TCD2_DOFF = 0; // don't update destination 162 | DMA_TCD2_DLASTSGA = 0; 163 | 164 | // BITER sets the number of inner loops to be run; 165 | // must be equal to CITER when start is set. 166 | DMA_TCD2_CITER_ELINKNO = 1; 167 | DMA_TCD2_BITER_ELINKNO = 1; 168 | 169 | DMA_TCD2_CSR = (1 << 8) | (1<<5); 170 | } 171 | #endif 172 | 173 | 174 | /** 175 | * Swap the DMA engines safely. 176 | * Wait for dma1 to be "done" so that we can swap it safely. 177 | * then wait for dma2 to be "done". 178 | */ 179 | static void 180 | dma_swap( 181 | const void * const buf 182 | ) 183 | { 184 | // clear the DMA1 done flag and wait for it to be reset 185 | DMA_CDNE = DMA_CDNE_CDNE(1); 186 | while (!(DMA_TCD1_CSR & DMA_TCD_CSR_DONE)) 187 | ; 188 | // DMA1 is now safe to swap 189 | DMA_TCD1_SADDR = buf; 190 | 191 | // clear the DMA2 done flag and wait for it to be reset 192 | DMA_CDNE = DMA_CDNE_CDNE(2); 193 | while (!(DMA_TCD2_CSR & DMA_TCD_CSR_DONE)) 194 | ; 195 | // DMA2 is now safe to swap 196 | DMA_TCD2_SADDR = buf; 197 | } 198 | 199 | 200 | 201 | /* 202 | // CQ NY3U 203 | 1010110100 C 204 | 11101110100 Q 205 | 1010110100 C 206 | 11101110100 Q 207 | 100 208 | 1101110100 116 78 4E N 209 | 10111101100 131 89 59 Y 210 | 1111111100 063 51 33 3 211 | 10101011100 125 85 55 U 212 | 213 | 10101101 214 | 00111011 215 | 10100101 216 | 01101001 217 | 11011101 218 | 00100110 219 | 11101001 220 | 01111011 221 | 00111111 222 | 11001010 223 | 10111000 224 | */ 225 | 226 | 227 | 228 | static void 229 | send(int bit) 230 | { 231 | static int phase; 232 | 233 | if (bit == 0) 234 | { 235 | // ramp down for half the bit width 236 | for (int power = POWER_LEVELS-1 ; power > 0 ; power--) 237 | { 238 | dma_swap(carrier[phase][power]); 239 | delayMicroseconds(32000 / POWER_LEVELS / 2); 240 | } 241 | 242 | // and ramp back up on the other phase 243 | phase = !phase; 244 | led(phase); 245 | 246 | for (int power = 0 ; power < POWER_LEVELS ; power++) 247 | { 248 | dma_swap(carrier[phase][power]); 249 | delayMicroseconds(32000 / POWER_LEVELS / 2); 250 | } 251 | } else { 252 | // maintain the current phase for the full width 253 | dma_swap(carrier[phase][POWER_LEVELS-1]); 254 | delayMicroseconds(32000); 255 | } 256 | } 257 | 258 | 259 | static const uint8_t psk_bits[] = "" 260 | "1010110100" // C 261 | "11101110100" // Q 262 | "100" // space 263 | "1010110100" // C 264 | "11101110100" // Q 265 | "100" // space 266 | "1101110100" // N 267 | "10111101100" // Y 268 | "1111111100" // 3 269 | "10101011100" // U 270 | "100" // space 271 | ; 272 | 273 | 274 | void 275 | loop(void) 276 | { 277 | //dma_send(sin_table, sizeof(sin_table)); 278 | //dma_send(ramp_up_table, sizeof(ramp_up_table)); 279 | 280 | for (int i = 0 ; i < sizeof(psk_bits)-1 ; i++) 281 | { 282 | send(psk_bits[i] == '1'); 283 | } 284 | 285 | #if 0 286 | while (1) 287 | { 288 | led(0); delay(400); 289 | led(1); delay(200); 290 | led(0); delay(200); 291 | led(1); delay(200); 292 | } 293 | 294 | while (!dma_complete()) 295 | ; 296 | 297 | uint32_t sig = 0; 298 | int bit_offset = 0; 299 | const int bit_count = sizeof(bits); 300 | int do_ramp = 1; 301 | int cur_bit = 0; 302 | int new_bit = 0; 303 | int phase = 0; 304 | 305 | while (1) 306 | { 307 | #if 1 308 | // we want 31.250 Hz for our bit clock, 309 | // 960000 / 31.250 == 30720 cycles 310 | #define ramp_size 4096 311 | #define bit_len 16384 312 | 313 | if (sig == bit_len) 314 | { 315 | // time for the next bit 316 | if (do_ramp) 317 | phase ^= 1; 318 | 319 | sig = 0; 320 | cur_bit = new_bit; 321 | //digitalWriteFast(12, phase == 0); 322 | } 323 | 324 | // signal sin and cos 325 | uint16_t ps_i = sig >> 3; 326 | if (phase) 327 | ps_i += 64; 328 | 329 | uint16_t pc_i = ps_i + 32; 330 | 331 | int32_t ps = +sin_table[ps_i % 128]; 332 | int32_t pc = -sin_table[pc_i % 128]; 333 | 334 | if (1) 335 | { 336 | if (sig < ramp_size) 337 | { 338 | // ramp up if this bit is unchanged 339 | if (do_ramp) 340 | { 341 | int32_t mag = sig; 342 | ps = (ps * mag) / ramp_size; 343 | pc = (pc * mag) / ramp_size; 344 | } 345 | } 346 | if (sig == (bit_len - ramp_size)) 347 | { 348 | new_bit = bits[bit_offset] == '1'; 349 | if (new_bit == 0) 350 | do_ramp = 1; 351 | else 352 | do_ramp = 0; 353 | bit_offset++; 354 | if (bit_offset == bit_count) 355 | bit_offset = 0; 356 | } 357 | if (sig > (bit_len - ramp_size)) 358 | { 359 | // ramp down if this bit will not change 360 | if (do_ramp) 361 | { 362 | int32_t mag = bit_len - sig; 363 | ps = (ps * mag) / ramp_size; 364 | pc = (pc * mag) / ramp_size; 365 | } 366 | } 367 | } 368 | #else 369 | int mag = (sig >> 18) % 256; 370 | ps = (ps * mag) / 256; 371 | pc = (pc * mag) / 256; 372 | #endif 373 | 374 | while (!periodic_timer_expired()) 375 | ; 376 | 377 | #if 0 378 | dac_output(ps/2 + 2048); 379 | #else 380 | ssb_output(ps, pc); 381 | #endif 382 | 383 | sig++; 384 | } 385 | #endif 386 | } 387 | --------------------------------------------------------------------------------