kopia lustrzana https://github.com/F5OEO/WsprryPi
Use librpitx - Should improve clean spectrum
rodzic
dc84139cbc
commit
96276615b2
19
makefile
19
makefile
|
@ -1,28 +1,19 @@
|
||||||
prefix=/usr/local
|
prefix=/usr/local
|
||||||
|
|
||||||
CFLAGS += -Wall
|
CFLAGS += -Wall -Wno-unused-variable
|
||||||
CXXFLAGS += -D_GLIBCXX_DEBUG -std=c++11 -Wall -Werror -fmax-errors=5
|
CXXFLAGS += -Wall -Wall -Wno-unused-variable -std=c++11
|
||||||
LDLIBS += -lm
|
LDLIBS += -lm
|
||||||
|
|
||||||
ifeq ($(findstring armv6,$(shell uname -m)),armv6)
|
|
||||||
# Broadcom BCM2835 SoC with 700 MHz 32-bit ARM 1176JZF-S (ARMv6 arch)
|
|
||||||
PI_VERSION = -DRPI1
|
|
||||||
else
|
|
||||||
# Broadcom BCM2836 SoC with 900 MHz 32-bit quad-core ARM Cortex-A7 (ARMv7 arch)
|
|
||||||
# Broadcom BCM2837 SoC with 1.2 GHz 64-bit quad-core ARM Cortex-A53 (ARMv8 arch)
|
|
||||||
PI_VERSION = -DRPI23
|
|
||||||
endif
|
|
||||||
|
|
||||||
all: wspr gpioclk
|
all: wspr gpioclk
|
||||||
|
|
||||||
mailbox.o: mailbox.c mailbox.h
|
|
||||||
$(CC) $(CFLAGS) -c mailbox.c
|
|
||||||
|
|
||||||
wspr: mailbox.o wspr.cpp mailbox.h
|
wspr: mailbox.o wspr.cpp mailbox.h
|
||||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $(PI_VERSION) mailbox.o wspr.cpp -owspr
|
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) wspr.cpp librpitx/src/librpitx.a -owspr
|
||||||
|
|
||||||
gpioclk: gpioclk.cpp
|
gpioclk: gpioclk.cpp
|
||||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) $(PI_VERSION) gpioclk.cpp -ogpioclk
|
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(LDLIBS) gpioclk.cpp -ogpioclk
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(RM) *.o gpioclk wspr
|
$(RM) *.o gpioclk wspr
|
||||||
|
|
637
wspr.cpp
637
wspr.cpp
|
@ -44,86 +44,17 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
#include <sys/timex.h>
|
#include <sys/timex.h>
|
||||||
|
#include "librpitx/src/librpitx.h"
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#include "mailbox.h"
|
|
||||||
}
|
|
||||||
#endif /* __cplusplus */
|
|
||||||
|
|
||||||
|
|
||||||
// Note on accessing memory in RPi:
|
clkgpio *clk=NULL;
|
||||||
//
|
ngfmdmasync *ngfmtest=NULL;
|
||||||
// There are 3 (yes three) address spaces in the Pi:
|
|
||||||
// Physical addresses
|
|
||||||
// These are the actual address locations of the RAM and are equivalent
|
|
||||||
// to offsets into /dev/mem.
|
|
||||||
// The peripherals (DMA engine, PWM, etc.) are located at physical
|
|
||||||
// address 0x2000000 for RPi1 and 0x3F000000 for RPi2/3.
|
|
||||||
// Virtual addresses
|
|
||||||
// These are the addresses that a program sees and can read/write to.
|
|
||||||
// Addresses 0x00000000 through 0xBFFFFFFF are the addresses available
|
|
||||||
// to a program running in user space.
|
|
||||||
// Addresses 0xC0000000 and above are available only to the kernel.
|
|
||||||
// The peripherals start at address 0xF2000000 in virtual space but
|
|
||||||
// this range is only accessible by the kernel. The kernel could directly
|
|
||||||
// access peripherals from virtual addresses. It is not clear to me my
|
|
||||||
// a user space application running as 'root' does not have access to this
|
|
||||||
// memory range.
|
|
||||||
// Bus addresses
|
|
||||||
// This is a different (virtual?) address space that also maps onto
|
|
||||||
// physical memory.
|
|
||||||
// The peripherals start at address 0x7E000000 of the bus address space.
|
|
||||||
// The DRAM is also available in bus address space in 4 different locations:
|
|
||||||
// 0x00000000 "L1 and L2 cached alias"
|
|
||||||
// 0x40000000 "L2 cache coherent (non allocating)"
|
|
||||||
// 0x80000000 "L2 cache (only)"
|
|
||||||
// 0xC0000000 "Direct, uncached access"
|
|
||||||
//
|
|
||||||
// Accessing peripherals from user space (virtual addresses):
|
|
||||||
// The technique used in this program is that mmap is used to map portions of
|
|
||||||
// /dev/mem to an arbitrary virtual address. For example, to access the
|
|
||||||
// GPIO's, the gpio range of addresses in /dev/mem (physical addresses) are
|
|
||||||
// mapped to a kernel chosen virtual address. After the mapping has been
|
|
||||||
// set up, writing to the kernel chosen virtual address will actually
|
|
||||||
// write to the GPIO addresses in physical memory.
|
|
||||||
//
|
|
||||||
// Accessing RAM from DMA engine
|
|
||||||
// The DMA engine is programmed by accessing the peripheral registers but
|
|
||||||
// must use bus addresses to access memory. Thus, to use the DMA engine to
|
|
||||||
// move memory from one virtual address to another virtual address, one needs
|
|
||||||
// to first find the physical addresses that corresponds to the virtual
|
|
||||||
// addresses. Then, one needs to find the bus addresses that corresponds to
|
|
||||||
// those physical addresses. Finally, the DMA engine can be programmed. i.e.
|
|
||||||
// DMA engine access should use addresses starting with 0xC.
|
|
||||||
//
|
|
||||||
// The perhipherals in the Broadcom documentation are described using their bus
|
|
||||||
// addresses and structures are created and calculations performed in this
|
|
||||||
// program to figure out how to access them with virtual addresses.
|
|
||||||
|
|
||||||
#define ABORT(a) exit(a)
|
#define ABORT(a) exit(a)
|
||||||
// Used for debugging
|
// Used for debugging
|
||||||
#define MARK std::cout << "Currently in file: " << __FILE__ << " line: " << __LINE__ << std::endl
|
#define MARK std::cout << "Currently in file: " << __FILE__ << " line: " << __LINE__ << std::endl
|
||||||
|
typedef enum {WSPR,TONE} mode_type;
|
||||||
// PLLD clock frequency.
|
|
||||||
// For RPi1, after NTP converges, these is a 2.5 PPM difference between
|
|
||||||
// the PPM correction reported by NTP and the actual frequency offset of
|
|
||||||
// the crystal. This 2.5 PPM offset is not present in the RPi2 and RPi3.
|
|
||||||
// This 2.5 PPM offset is compensated for here, but only for the RPi1.
|
|
||||||
#ifdef RPI23
|
|
||||||
#define F_PLLD_CLK (500000000.0)
|
|
||||||
#else
|
|
||||||
#ifdef RPI1
|
|
||||||
#define F_PLLD_CLK (500000000.0*(1-2.500e-6))
|
|
||||||
#else
|
|
||||||
#error "RPI version macro is not defined"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
// Empirical value for F_PWM_CLK that produces WSPR symbols that are 'close' to
|
|
||||||
// 0.682s long. For some reason, despite the use of DMA, the load on the PI
|
|
||||||
// affects the TX length of the symbols. However, the varying symbol length is
|
|
||||||
// compensated for in the main loop.
|
|
||||||
#define F_PWM_CLK_INIT (31156186.6125761)
|
|
||||||
|
|
||||||
// WSRP nominal symbol time
|
// WSRP nominal symbol time
|
||||||
#define WSPR_SYMTIME (8192.0/12000.0)
|
#define WSPR_SYMTIME (8192.0/12000.0)
|
||||||
|
@ -132,216 +63,23 @@ extern "C" {
|
||||||
#define WSPR_RAND_OFFSET 80
|
#define WSPR_RAND_OFFSET 80
|
||||||
#define WSPR15_RAND_OFFSET 8
|
#define WSPR15_RAND_OFFSET 8
|
||||||
|
|
||||||
// Choose proper base address depending on RPI1/RPI23 macro from makefile.
|
|
||||||
// PERI_BASE_PHYS is the base address of the peripherals, in physical
|
|
||||||
// address space.
|
|
||||||
#ifdef RPI23
|
|
||||||
#define PERI_BASE_PHYS 0x3f000000
|
|
||||||
#define MEM_FLAG 0x04
|
|
||||||
#else
|
|
||||||
#ifdef RPI1
|
|
||||||
#define PERI_BASE_PHYS 0x20000000
|
|
||||||
#define MEM_FLAG 0x0c
|
|
||||||
#else
|
|
||||||
#error "RPI version macro is not defined"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define PAGE_SIZE (4*1024)
|
|
||||||
#define BLOCK_SIZE (4*1024)
|
|
||||||
|
|
||||||
// peri_base_virt is the base virtual address that a userspace program (this
|
|
||||||
// program) can use to read/write to the the physical addresses controlling
|
|
||||||
// the peripherals. This address is mapped at runtime using mmap and /dev/mem.
|
|
||||||
// This must be declared global so that it can be called by the atexit
|
|
||||||
// function.
|
|
||||||
volatile unsigned *peri_base_virt = NULL;
|
|
||||||
|
|
||||||
// Given an address in the bus address space of the peripherals, this
|
|
||||||
// macro calculates the appropriate virtual address to use to access
|
|
||||||
// the requested bus address space. It does this by first subtracting
|
|
||||||
// 0x7e000000 from the supplied bus address to calculate the offset into
|
|
||||||
// the peripheral address space. Then, this offset is added to peri_base_virt
|
|
||||||
// Which is the base address of the peripherals, in virtual address space.
|
|
||||||
#define ACCESS_BUS_ADDR(buss_addr) *(volatile int*)((long int)peri_base_virt+(buss_addr)-0x7e000000)
|
|
||||||
// Given a bus address in the peripheral address space, set or clear a bit.
|
|
||||||
#define SETBIT_BUS_ADDR(base, bit) ACCESS_BUS_ADDR(base) |= 1<<bit
|
|
||||||
#define CLRBIT_BUS_ADDR(base, bit) ACCESS_BUS_ADDR(base) &= ~(1<<bit)
|
|
||||||
|
|
||||||
// The following are all bus addresses.
|
|
||||||
#define GPIO_BUS_BASE (0x7E200000)
|
|
||||||
#define CM_GP0CTL_BUS (0x7e101070)
|
|
||||||
#define CM_GP0DIV_BUS (0x7e101074)
|
|
||||||
#define PADS_GPIO_0_27_BUS (0x7e10002c)
|
|
||||||
#define CLK_BUS_BASE (0x7E101000)
|
|
||||||
#define DMA_BUS_BASE (0x7E007000)
|
|
||||||
#define PWM_BUS_BASE (0x7e20C000) /* PWM controller */
|
|
||||||
|
|
||||||
// Convert from a bus address to a physical address.
|
|
||||||
#define BUS_TO_PHYS(x) ((x)&~0xC0000000)
|
|
||||||
|
|
||||||
typedef enum {WSPR,TONE} mode_type;
|
|
||||||
|
|
||||||
// Structure used to control clock generator
|
|
||||||
struct GPCTL {
|
|
||||||
char SRC : 4;
|
|
||||||
char ENAB : 1;
|
|
||||||
char KILL : 1;
|
|
||||||
char : 1;
|
|
||||||
char BUSY : 1;
|
|
||||||
char FLIP : 1;
|
|
||||||
char MASH : 2;
|
|
||||||
unsigned int : 13;
|
|
||||||
char PASSWD : 8;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Structure used to tell the DMA engine what to do
|
|
||||||
struct CB {
|
|
||||||
volatile unsigned int TI;
|
|
||||||
volatile unsigned int SOURCE_AD;
|
|
||||||
volatile unsigned int DEST_AD;
|
|
||||||
volatile unsigned int TXFR_LEN;
|
|
||||||
volatile unsigned int STRIDE;
|
|
||||||
volatile unsigned int NEXTCONBK;
|
|
||||||
volatile unsigned int RES1;
|
|
||||||
volatile unsigned int RES2;
|
|
||||||
};
|
|
||||||
|
|
||||||
// DMA engine status registers
|
|
||||||
struct DMAregs {
|
|
||||||
volatile unsigned int CS;
|
|
||||||
volatile unsigned int CONBLK_AD;
|
|
||||||
volatile unsigned int TI;
|
|
||||||
volatile unsigned int SOURCE_AD;
|
|
||||||
volatile unsigned int DEST_AD;
|
|
||||||
volatile unsigned int TXFR_LEN;
|
|
||||||
volatile unsigned int STRIDE;
|
|
||||||
volatile unsigned int NEXTCONBK;
|
|
||||||
volatile unsigned int DEBUG;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Virtual and bus addresses of a page of physical memory.
|
|
||||||
struct PageInfo {
|
|
||||||
void* b; // bus address
|
|
||||||
void* v; // virtual address
|
|
||||||
};
|
|
||||||
|
|
||||||
// Must be global so that exit handlers can access this.
|
|
||||||
static struct {
|
|
||||||
int handle; /* From mbox_open() */
|
|
||||||
unsigned mem_ref = 0; /* From mem_alloc() */
|
|
||||||
unsigned bus_addr; /* From mem_lock() */
|
|
||||||
unsigned char *virt_addr = NULL; /* From mapmem() */ //ha7ilm: originally uint8_t
|
|
||||||
unsigned pool_size;
|
|
||||||
unsigned pool_cnt;
|
|
||||||
} mbox;
|
|
||||||
|
|
||||||
// Use the mbox interface to allocate a single chunk of memory to hold
|
|
||||||
// all the pages we will need. The bus address and the virtual address
|
|
||||||
// are saved in the mbox structure.
|
|
||||||
void allocMemPool(unsigned numpages) {
|
|
||||||
// Allocate space.
|
|
||||||
mbox.mem_ref = mem_alloc(mbox.handle, 4096*numpages, 4096, MEM_FLAG);
|
|
||||||
// Lock down the allocated space and return its bus address.
|
|
||||||
mbox.bus_addr = mem_lock(mbox.handle, mbox.mem_ref);
|
|
||||||
// Conert the bus address to a physical address and map this to virtual
|
|
||||||
// (aka user) space.
|
|
||||||
mbox.virt_addr = (unsigned char*)mapmem(BUS_TO_PHYS(mbox.bus_addr), 4096*numpages);
|
|
||||||
// The number of pages in the pool. Never changes!
|
|
||||||
mbox.pool_size=numpages;
|
|
||||||
// How many of the created pages have actually been used.
|
|
||||||
mbox.pool_cnt=0;
|
|
||||||
//printf("allocMemoryPool bus_addr=%x virt_addr=%x mem_ref=%x\n",mbox.bus_addr,(unsigned)mbox.virt_addr,mbox.mem_ref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the virtual and bus address (NOT physical address!) of another
|
|
||||||
// page in the pool.
|
|
||||||
void getRealMemPageFromPool(void ** vAddr, void **bAddr) {
|
|
||||||
if (mbox.pool_cnt>=mbox.pool_size) {
|
|
||||||
std::cerr << "Error: unable to allocated more pages!" << std::endl;
|
|
||||||
ABORT(-1);
|
|
||||||
}
|
|
||||||
unsigned offset = mbox.pool_cnt*4096;
|
|
||||||
*vAddr = (void*)(((unsigned)mbox.virt_addr) + offset);
|
|
||||||
*bAddr = (void*)(((unsigned)mbox.bus_addr) + offset);
|
|
||||||
//printf("getRealMemoryPageFromPool bus_addr=%x virt_addr=%x\n", (unsigned)*pAddr,(unsigned)*vAddr);
|
|
||||||
mbox.pool_cnt++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free the memory pool
|
|
||||||
void deallocMemPool() {
|
|
||||||
if(mbox.virt_addr!=NULL) {
|
|
||||||
unmapmem(mbox.virt_addr, mbox.pool_size*4096);
|
|
||||||
}
|
|
||||||
if (mbox.mem_ref!=0) {
|
|
||||||
mem_unlock(mbox.handle, mbox.mem_ref);
|
|
||||||
mem_free(mbox.handle, mbox.mem_ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable the PWM clock and wait for it to become 'not busy'.
|
// Disable the PWM clock and wait for it to become 'not busy'.
|
||||||
void disable_clock() {
|
void disable_clock() {
|
||||||
// Check if mapping has been set up yet.
|
|
||||||
if (peri_base_virt==NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Disable the clock (in case it's already running) by reading current
|
|
||||||
// settings and only clearing the enable bit.
|
|
||||||
auto settings=ACCESS_BUS_ADDR(CM_GP0CTL_BUS);
|
|
||||||
// Clear enable bit and add password
|
|
||||||
settings=(settings&0x7EF)|0x5A000000;
|
|
||||||
// Disable
|
|
||||||
ACCESS_BUS_ADDR(CM_GP0CTL_BUS) = *((int*)&settings);
|
|
||||||
// Wait for clock to not be busy.
|
|
||||||
while (true) {
|
|
||||||
if (!(ACCESS_BUS_ADDR(CM_GP0CTL_BUS)&(1<<7))) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn on TX
|
// Turn on TX
|
||||||
void txon() {
|
void txon() {
|
||||||
// Set function select for GPIO4.
|
|
||||||
// Fsel 000 => input
|
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 7; //16mA +10.6dBm
|
||||||
// Fsel 001 => output
|
|
||||||
// Fsel 100 => alternate function 0
|
|
||||||
// Fsel 101 => alternate function 1
|
|
||||||
// Fsel 110 => alternate function 2
|
|
||||||
// Fsel 111 => alternate function 3
|
|
||||||
// Fsel 011 => alternate function 4
|
|
||||||
// Fsel 010 => alternate function 5
|
|
||||||
// Function select for GPIO is configured as 'b100 which selects
|
|
||||||
// alternate function 0 for GPIO4. Alternate function 0 is GPCLK0.
|
|
||||||
// See section 6.2 of Arm Peripherals Manual.
|
|
||||||
SETBIT_BUS_ADDR(GPIO_BUS_BASE , 14);
|
|
||||||
CLRBIT_BUS_ADDR(GPIO_BUS_BASE , 13);
|
|
||||||
CLRBIT_BUS_ADDR(GPIO_BUS_BASE , 12);
|
|
||||||
|
|
||||||
// Set GPIO drive strength, more info: http://www.scribd.com/doc/101830961/GPIO-Pads-Control2
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 0; //2mA -3.4dBm
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 1; //4mA +2.1dBm
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 2; //6mA +4.9dBm
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 3; //8mA +6.6dBm(default)
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 4; //10mA +8.2dBm
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 5; //12mA +9.2dBm
|
|
||||||
//ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 6; //14mA +10.0dBm
|
|
||||||
ACCESS_BUS_ADDR(PADS_GPIO_0_27_BUS) = 0x5a000018 + 7; //16mA +10.6dBm
|
|
||||||
|
|
||||||
disable_clock();
|
disable_clock();
|
||||||
|
|
||||||
// Set clock source as PLLD.
|
|
||||||
struct GPCTL setupword = {6/*SRC*/, 0, 0, 0, 0, 3,0x5a};
|
|
||||||
|
|
||||||
// Enable clock.
|
|
||||||
setupword = {6/*SRC*/, 1, 0, 0, 0, 3,0x5a};
|
|
||||||
ACCESS_BUS_ADDR(CM_GP0CTL_BUS) = *((int*)&setupword);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn transmitter on
|
// Turn transmitter on
|
||||||
void txoff() {
|
void txoff() {
|
||||||
//struct GPCTL setupword = {6/*SRC*/, 0, 0, 0, 0, 1,0x5a};
|
|
||||||
//ACCESS_BUS_ADDR(CM_GP0CTL_BUS) = *((int*)&setupword);
|
|
||||||
disable_clock();
|
disable_clock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -362,74 +100,12 @@ void txSym(
|
||||||
struct PageInfo & constPage,
|
struct PageInfo & constPage,
|
||||||
int & bufPtr
|
int & bufPtr
|
||||||
) {
|
) {
|
||||||
const int f0_idx=sym_num*2;
|
|
||||||
const int f1_idx=f0_idx+1;
|
|
||||||
const double f0_freq=dma_table_freq[f0_idx];
|
|
||||||
const double f1_freq=dma_table_freq[f1_idx];
|
|
||||||
const double tone_freq=center_freq-1.5*tone_spacing+sym_num*tone_spacing;
|
|
||||||
// Double check...
|
|
||||||
assert((tone_freq>=f0_freq)&&(tone_freq<=f1_freq));
|
|
||||||
const double f0_ratio=1.0-(tone_freq-f0_freq)/(f1_freq-f0_freq);
|
|
||||||
//cout << "f0_ratio = " << f0_ratio << std::endl;
|
|
||||||
assert ((f0_ratio>=0)&&(f0_ratio<=1));
|
|
||||||
const long int n_pwmclk_per_sym=round(f_pwm_clk*tsym);
|
|
||||||
|
|
||||||
long int n_pwmclk_transmitted=0;
|
|
||||||
long int n_f0_transmitted=0;
|
|
||||||
//printf("<instrs[bufPtr] begin=%x>",(unsigned)&instrs[bufPtr]);
|
|
||||||
while (n_pwmclk_transmitted<n_pwmclk_per_sym) {
|
|
||||||
// Number of PWM clocks for this iteration
|
|
||||||
long int n_pwmclk=PWM_CLOCKS_PER_ITER_NOMINAL;
|
|
||||||
// Iterations may produce spurs around the main peak based on the iteration
|
|
||||||
// frequency. Randomize the iteration period so as to spread this peak
|
|
||||||
// around.
|
|
||||||
n_pwmclk+=round((rand()/((double)RAND_MAX+1.0)-.5)*n_pwmclk)*1;
|
|
||||||
if (n_pwmclk_transmitted+n_pwmclk>n_pwmclk_per_sym) {
|
|
||||||
n_pwmclk=n_pwmclk_per_sym-n_pwmclk_transmitted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate number of clocks to transmit f0 during this iteration so
|
|
||||||
// that the long term average is as close to f0_ratio as possible.
|
|
||||||
const long int n_f0=round(f0_ratio*(n_pwmclk_transmitted+n_pwmclk))-n_f0_transmitted;
|
|
||||||
const long int n_f1=n_pwmclk-n_f0;
|
|
||||||
|
|
||||||
// Configure the transmission for this iteration
|
|
||||||
// Set GPIO pin to transmit f0
|
|
||||||
bufPtr++;
|
|
||||||
while( ACCESS_BUS_ADDR(DMA_BUS_BASE + 0x04 /* CurBlock*/) == (long int)(instrs[bufPtr].b)) usleep(100);
|
|
||||||
((struct CB*)(instrs[bufPtr].v))->SOURCE_AD = (long int)constPage.b + f0_idx*4;
|
|
||||||
|
|
||||||
// Wait for n_f0 PWM clocks
|
|
||||||
bufPtr++;
|
|
||||||
while( ACCESS_BUS_ADDR(DMA_BUS_BASE + 0x04 /* CurBlock*/) == (long int)(instrs[bufPtr].b)) usleep(100);
|
|
||||||
((struct CB*)(instrs[bufPtr].v))->TXFR_LEN = n_f0;
|
|
||||||
|
|
||||||
// Set GPIO pin to transmit f1
|
|
||||||
bufPtr++;
|
|
||||||
while( ACCESS_BUS_ADDR(DMA_BUS_BASE + 0x04 /* CurBlock*/) == (long int)(instrs[bufPtr].b)) usleep(100);
|
|
||||||
((struct CB*)(instrs[bufPtr].v))->SOURCE_AD = (long int)constPage.b + f1_idx*4;
|
|
||||||
|
|
||||||
// Wait for n_f1 PWM clocks
|
|
||||||
bufPtr=(bufPtr+1) % (1024);
|
|
||||||
while( ACCESS_BUS_ADDR(DMA_BUS_BASE + 0x04 /* CurBlock*/) == (long int)(instrs[bufPtr].b)) usleep(100);
|
|
||||||
((struct CB*)(instrs[bufPtr].v))->TXFR_LEN = n_f1;
|
|
||||||
|
|
||||||
// Update counters
|
|
||||||
n_pwmclk_transmitted+=n_pwmclk;
|
|
||||||
n_f0_transmitted+=n_f0;
|
|
||||||
}
|
|
||||||
//printf("<instrs[bufPtr]=%x %x>",(unsigned)instrs[bufPtr].v,(unsigned)instrs[bufPtr].b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn off (reset) DMA engine
|
// Turn off (reset) DMA engine
|
||||||
void unSetupDMA(){
|
void unSetupDMA(){
|
||||||
// Check if mapping has been set up yet.
|
|
||||||
if (peri_base_virt==NULL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
//cout << "Exiting!" << std::endl;
|
|
||||||
struct DMAregs* DMA0 = (struct DMAregs*)&(ACCESS_BUS_ADDR(DMA_BUS_BASE));
|
|
||||||
DMA0->CS =1<<31; // reset dma controller
|
|
||||||
txoff();
|
txoff();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,139 +117,7 @@ double bit_trunc(
|
||||||
return floor(d/pow(2.0,lsb))*pow(2.0,lsb);
|
return floor(d/pow(2.0,lsb))*pow(2.0,lsb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Program the tuning words into the DMA table.
|
|
||||||
void setupDMATab(
|
|
||||||
const double & center_freq_desired,
|
|
||||||
const double & tone_spacing,
|
|
||||||
const double & plld_actual_freq,
|
|
||||||
std::vector <double> & dma_table_freq,
|
|
||||||
double & center_freq_actual,
|
|
||||||
struct PageInfo & constPage
|
|
||||||
){
|
|
||||||
// Make sure that all the WSPR tones can be produced solely by
|
|
||||||
// varying the fractional part of the frequency divider.
|
|
||||||
center_freq_actual=center_freq_desired;
|
|
||||||
double div_lo=bit_trunc(plld_actual_freq/(center_freq_desired-1.5*tone_spacing),-12)+pow(2.0,-12);
|
|
||||||
double div_hi=bit_trunc(plld_actual_freq/(center_freq_desired+1.5*tone_spacing),-12);
|
|
||||||
if (floor(div_lo)!=floor(div_hi)) {
|
|
||||||
center_freq_actual=plld_actual_freq/floor(div_lo)-1.6*tone_spacing;
|
|
||||||
std::stringstream temp;
|
|
||||||
temp << std::setprecision(6) << std::fixed << " Warning: center frequency has been changed to " << center_freq_actual/1e6 << " MHz" << std::endl;
|
|
||||||
std::cout << temp.str();
|
|
||||||
std::cout << " because of hardware limitations!" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create DMA table of tuning words. WSPR tone i will use entries 2*i and
|
|
||||||
// 2*i+1 to generate the appropriate tone.
|
|
||||||
double tone0_freq=center_freq_actual-1.5*tone_spacing;
|
|
||||||
std::vector <long int> tuning_word(1024);
|
|
||||||
for (int i=0;i<8;i++) {
|
|
||||||
double tone_freq=tone0_freq+(i>>1)*tone_spacing;
|
|
||||||
double div=bit_trunc(plld_actual_freq/tone_freq,-12);
|
|
||||||
if (i%2==0) {
|
|
||||||
div=div+pow(2.0,-12);
|
|
||||||
}
|
|
||||||
tuning_word[i]=((int)(div*pow(2.0,12)));
|
|
||||||
}
|
|
||||||
// Fill the remaining table, just in case...
|
|
||||||
for (int i=8;i<1024;i++) {
|
|
||||||
double div=500+i;
|
|
||||||
tuning_word[i]=((int)(div*pow(2.0,12)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Program the table
|
|
||||||
dma_table_freq.resize(1024);
|
|
||||||
for (int i=0;i<1024;i++) {
|
|
||||||
dma_table_freq[i]=plld_actual_freq/(tuning_word[i]/pow(2.0,12));
|
|
||||||
((int*)(constPage.v))[i] = (0x5a<<24)+tuning_word[i];
|
|
||||||
if ((i%2==0)&&(i<8)) {
|
|
||||||
assert((tuning_word[i]&(~0xfff))==(tuning_word[i+1]&(~0xfff)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the memory structures needed by the DMA engine and perform initial
|
|
||||||
// clock configuration.
|
|
||||||
void setupDMA(
|
|
||||||
struct PageInfo & constPage,
|
|
||||||
struct PageInfo & instrPage,
|
|
||||||
struct PageInfo instrs[]
|
|
||||||
){
|
|
||||||
allocMemPool(1025);
|
|
||||||
|
|
||||||
// Allocate a page of ram for the constants
|
|
||||||
getRealMemPageFromPool(&constPage.v, &constPage.b);
|
|
||||||
|
|
||||||
// Create 1024 instructions allocating one page at a time.
|
|
||||||
// Even instructions target the GP0 Clock divider
|
|
||||||
// Odd instructions target the PWM FIFO
|
|
||||||
int instrCnt = 0;
|
|
||||||
while (instrCnt<1024) {
|
|
||||||
// Allocate a page of ram for the instructions
|
|
||||||
getRealMemPageFromPool(&instrPage.v, &instrPage.b);
|
|
||||||
|
|
||||||
// make copy instructions
|
|
||||||
// Only create as many instructions as will fit in the recently
|
|
||||||
// allocated page. If not enough space for all instructions, the
|
|
||||||
// next loop will allocate another page.
|
|
||||||
struct CB* instr0= (struct CB*)instrPage.v;
|
|
||||||
int i;
|
|
||||||
for (i=0; i<(signed)(4096/sizeof(struct CB)); i++) {
|
|
||||||
instrs[instrCnt].v = (void*)((long int)instrPage.v + sizeof(struct CB)*i);
|
|
||||||
instrs[instrCnt].b = (void*)((long int)instrPage.b + sizeof(struct CB)*i);
|
|
||||||
instr0->SOURCE_AD = (unsigned long int)constPage.b+2048;
|
|
||||||
instr0->DEST_AD = PWM_BUS_BASE+0x18 /* FIF1 */;
|
|
||||||
instr0->TXFR_LEN = 4;
|
|
||||||
instr0->STRIDE = 0;
|
|
||||||
//instr0->NEXTCONBK = (int)instrPage.b + sizeof(struct CB)*(i+1);
|
|
||||||
instr0->TI = (1/* DREQ */<<6) | (5 /* PWM */<<16) | (1<<26/* no wide*/) ;
|
|
||||||
instr0->RES1 = 0;
|
|
||||||
instr0->RES2 = 0;
|
|
||||||
|
|
||||||
// Shouldn't this be (instrCnt%2) ???
|
|
||||||
if (i%2) {
|
|
||||||
instr0->DEST_AD = CM_GP0DIV_BUS;
|
|
||||||
instr0->STRIDE = 4;
|
|
||||||
instr0->TI = (1<<26/* no wide*/) ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instrCnt!=0) ((struct CB*)(instrs[instrCnt-1].v))->NEXTCONBK = (long int)instrs[instrCnt].b;
|
|
||||||
instr0++;
|
|
||||||
instrCnt++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create a circular linked list of instructions
|
|
||||||
((struct CB*)(instrs[1023].v))->NEXTCONBK = (long int)instrs[0].b;
|
|
||||||
|
|
||||||
// set up a clock for the PWM
|
|
||||||
ACCESS_BUS_ADDR(CLK_BUS_BASE + 40*4 /*PWMCLK_CNTL*/) = 0x5A000026; // Source=PLLD and disable
|
|
||||||
usleep(1000);
|
|
||||||
//ACCESS_BUS_ADDR(CLK_BUS_BASE + 41*4 /*PWMCLK_DIV*/) = 0x5A002800;
|
|
||||||
ACCESS_BUS_ADDR(CLK_BUS_BASE + 41*4 /*PWMCLK_DIV*/) = 0x5A002000; // set PWM div to 2, for 250MHz
|
|
||||||
ACCESS_BUS_ADDR(CLK_BUS_BASE + 40*4 /*PWMCLK_CNTL*/) = 0x5A000016; // Source=PLLD and enable
|
|
||||||
usleep(1000);
|
|
||||||
|
|
||||||
// set up pwm
|
|
||||||
ACCESS_BUS_ADDR(PWM_BUS_BASE + 0x0 /* CTRL*/) = 0;
|
|
||||||
usleep(1000);
|
|
||||||
ACCESS_BUS_ADDR(PWM_BUS_BASE + 0x4 /* status*/) = -1; // clear errors
|
|
||||||
usleep(1000);
|
|
||||||
// Range should default to 32, but it is set at 2048 after reset on my RPi.
|
|
||||||
ACCESS_BUS_ADDR(PWM_BUS_BASE + 0x10)=32;
|
|
||||||
ACCESS_BUS_ADDR(PWM_BUS_BASE + 0x20)=32;
|
|
||||||
ACCESS_BUS_ADDR(PWM_BUS_BASE + 0x0 /* CTRL*/) = -1; //(1<<13 /* Use fifo */) | (1<<10 /* repeat */) | (1<<9 /* serializer */) | (1<<8 /* enable ch */) ;
|
|
||||||
usleep(1000);
|
|
||||||
ACCESS_BUS_ADDR(PWM_BUS_BASE + 0x8 /* DMAC*/) = (1<<31 /* DMA enable */) | 0x0707;
|
|
||||||
|
|
||||||
//activate dma
|
|
||||||
struct DMAregs* DMA0 = (struct DMAregs*)&(ACCESS_BUS_ADDR(DMA_BUS_BASE));
|
|
||||||
DMA0->CS =1<<31; // reset
|
|
||||||
DMA0->CONBLK_AD=0;
|
|
||||||
DMA0->TI=0;
|
|
||||||
DMA0->CONBLK_AD = (unsigned long int)(instrPage.b);
|
|
||||||
DMA0->CS =(1<<0)|(255 <<16); // enable bit = 0, clear end flag = 1, prio=19-16
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert string to uppercase
|
// Convert string to uppercase
|
||||||
void to_upper(
|
void to_upper(
|
||||||
|
@ -1055,21 +599,11 @@ void timeval_print(struct timeval *tv) {
|
||||||
printf("%s.%03ld", buffer, (tv->tv_usec+500)/1000);
|
printf("%s.%03ld", buffer, (tv->tv_usec+500)/1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the mbox special files and open mbox.
|
|
||||||
void open_mbox() {
|
|
||||||
mbox.handle = mbox_open();
|
|
||||||
if (mbox.handle < 0) {
|
|
||||||
std::cerr << "Failed to open mailbox." << std::endl;
|
|
||||||
ABORT(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called when exiting or when a signal is received.
|
// Called when exiting or when a signal is received.
|
||||||
void cleanup() {
|
void cleanup() {
|
||||||
disable_clock();
|
if(clk!=NULL) {delete clk;clk=NULL;}
|
||||||
unSetupDMA();
|
if(ngfmtest!=NULL) {delete ngfmtest;ngfmtest=NULL;}
|
||||||
deallocMemPool();
|
|
||||||
unlink(LOCAL_DEVICE_FILE_NAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Called when a signal is received. Automatically calls cleanup().
|
// Called when a signal is received. Automatically calls cleanup().
|
||||||
|
@ -1079,43 +613,7 @@ void cleanupAndExit(int sig) {
|
||||||
ABORT(-1);
|
ABORT(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setSchedPriority(int priority) {
|
|
||||||
//In order to get the best timing at a decent queue size, we want the kernel
|
|
||||||
//to avoid interrupting us for long durations. This is done by giving our
|
|
||||||
//process a high priority. Note, must run as super-user for this to work.
|
|
||||||
struct sched_param sp;
|
|
||||||
sp.sched_priority=priority;
|
|
||||||
int ret = pthread_setschedparam(pthread_self(), SCHED_FIFO, &sp);
|
|
||||||
if (ret) {
|
|
||||||
std::cerr << "Warning: pthread_setschedparam (increase thread priority) returned non-zero: " << ret << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the memory map between virtual memory and the peripheral range
|
|
||||||
// of physical memory.
|
|
||||||
void setup_peri_base_virt(
|
|
||||||
volatile unsigned * & peri_base_virt
|
|
||||||
) {
|
|
||||||
int mem_fd;
|
|
||||||
// open /dev/mem
|
|
||||||
if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
|
|
||||||
std::cerr << "Error: can't open /dev/mem" << std::endl;
|
|
||||||
ABORT (-1);
|
|
||||||
}
|
|
||||||
peri_base_virt = (unsigned *)mmap(
|
|
||||||
NULL,
|
|
||||||
0x01000000, //len
|
|
||||||
PROT_READ|PROT_WRITE,
|
|
||||||
MAP_SHARED,
|
|
||||||
mem_fd,
|
|
||||||
PERI_BASE_PHYS //base
|
|
||||||
);
|
|
||||||
if ((long int)peri_base_virt==-1) {
|
|
||||||
std::cerr << "Error: peri_base_virt mmap error!" << std::endl;
|
|
||||||
ABORT(-1);
|
|
||||||
}
|
|
||||||
close(mem_fd);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(const int argc, char * const argv[]) {
|
int main(const int argc, char * const argv[]) {
|
||||||
//catch all signals (like ctrl+c, ctrl+z, ...) to ensure DMA is disabled
|
//catch all signals (like ctrl+c, ctrl+z, ...) to ensure DMA is disabled
|
||||||
|
@ -1126,17 +624,9 @@ int main(const int argc, char * const argv[]) {
|
||||||
sigaction(i, &sa, NULL);
|
sigaction(i, &sa, NULL);
|
||||||
}
|
}
|
||||||
atexit(cleanup);
|
atexit(cleanup);
|
||||||
setSchedPriority(30);
|
|
||||||
|
|
||||||
#ifdef RPI1
|
|
||||||
std::cout << "Detected Raspberry Pi version 1" << std::endl;
|
|
||||||
#else
|
|
||||||
#ifdef RPI23
|
|
||||||
std::cout << "Detected Raspberry Pi version 2/3" << std::endl;
|
|
||||||
#else
|
|
||||||
#error "RPI version macro is not defined"
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Initialize the RNG
|
// Initialize the RNG
|
||||||
srand(time(NULL));
|
srand(time(NULL));
|
||||||
|
@ -1172,18 +662,13 @@ int main(const int argc, char * const argv[]) {
|
||||||
);
|
);
|
||||||
int nbands=center_freq_set.size();
|
int nbands=center_freq_set.size();
|
||||||
|
|
||||||
// Initial configuration
|
|
||||||
struct PageInfo constPage;
|
|
||||||
struct PageInfo instrPage;
|
|
||||||
struct PageInfo instrs[1024];
|
|
||||||
setup_peri_base_virt(peri_base_virt);
|
|
||||||
// Set up DMA
|
|
||||||
open_mbox();
|
|
||||||
txon();
|
|
||||||
setupDMA(constPage,instrPage,instrs);
|
|
||||||
txoff();
|
|
||||||
|
|
||||||
if (mode==TONE) {
|
if (mode==TONE) {
|
||||||
|
if(clk==NULL)
|
||||||
|
clk=new clkgpio;
|
||||||
|
clk->SetAdvancedPllMode(true);
|
||||||
// Test tone mode...
|
// Test tone mode...
|
||||||
double wspr_symtime = WSPR_SYMTIME;
|
double wspr_symtime = WSPR_SYMTIME;
|
||||||
double tone_spacing=1.0/wspr_symtime;
|
double tone_spacing=1.0/wspr_symtime;
|
||||||
|
@ -1195,31 +680,15 @@ int main(const int argc, char * const argv[]) {
|
||||||
|
|
||||||
txon();
|
txon();
|
||||||
int bufPtr=0;
|
int bufPtr=0;
|
||||||
std::vector <double> dma_table_freq;
|
|
||||||
// Set to non-zero value to ensure setupDMATab is called at least once.
|
// Set to non-zero value to ensure setupDMATab is called at least once.
|
||||||
double ppm_prev=123456;
|
double ppm_prev=123456;
|
||||||
double center_freq_actual;
|
double center_freq_actual;
|
||||||
while (true) {
|
//SetTone
|
||||||
if (self_cal) {
|
clk->SetCenterFrequency(test_tone,100);
|
||||||
update_ppm(ppm);
|
clk->enableclk(4);
|
||||||
}
|
clk->SetFrequency(000);
|
||||||
if (ppm!=ppm_prev) {
|
while(true) usleep(1000000);
|
||||||
setupDMATab(test_tone+1.5*tone_spacing,tone_spacing,F_PLLD_CLK*(1-ppm/1e6),dma_table_freq,center_freq_actual,constPage);
|
|
||||||
//cout << std::setprecision(30) << dma_table_freq[0] << std::endl;
|
|
||||||
//cout << std::setprecision(30) << dma_table_freq[1] << std::endl;
|
|
||||||
//cout << std::setprecision(30) << dma_table_freq[2] << std::endl;
|
|
||||||
//cout << std::setprecision(30) << dma_table_freq[3] << std::endl;
|
|
||||||
if (center_freq_actual!=test_tone+1.5*tone_spacing) {
|
|
||||||
std::cout << " Warning: because of hardware limitations, test tone will be transmitted on" << std::endl;
|
|
||||||
std::stringstream temp;
|
|
||||||
temp << std::setprecision(6) << std::fixed << " frequency: " << (center_freq_actual-1.5*tone_spacing)/1e6 << " MHz" << std::endl;
|
|
||||||
std::cout << temp.str();
|
|
||||||
}
|
|
||||||
ppm_prev=ppm;
|
|
||||||
}
|
|
||||||
txSym(0, center_freq_actual, tone_spacing, 60, dma_table_freq, F_PWM_CLK_INIT, instrs, constPage, bufPtr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should never get here...
|
// Should never get here...
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -1281,7 +750,8 @@ int main(const int argc, char * const argv[]) {
|
||||||
std::vector <double> dma_table_freq;
|
std::vector <double> dma_table_freq;
|
||||||
double center_freq_actual;
|
double center_freq_actual;
|
||||||
if (center_freq_desired) {
|
if (center_freq_desired) {
|
||||||
setupDMATab(center_freq_desired,tone_spacing,F_PLLD_CLK*(1-ppm/1e6),dma_table_freq,center_freq_actual,constPage);
|
center_freq_actual=center_freq_desired;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
center_freq_actual=center_freq_desired;
|
center_freq_actual=center_freq_desired;
|
||||||
}
|
}
|
||||||
|
@ -1299,24 +769,43 @@ int main(const int argc, char * const argv[]) {
|
||||||
struct timeval sym_start;
|
struct timeval sym_start;
|
||||||
struct timeval diff;
|
struct timeval diff;
|
||||||
int bufPtr=0;
|
int bufPtr=0;
|
||||||
txon();
|
int SR=100*1/wspr_symtime;
|
||||||
for (int i = 0; i < 162; i++) {
|
int FifoSize=1000;
|
||||||
gettimeofday(&sym_start,NULL);
|
if(ngfmtest==NULL)
|
||||||
timeval_subtract(&diff, &sym_start, &tvBegin);
|
ngfmtest=new ngfmdmasync(center_freq_actual,SR,14,FifoSize);
|
||||||
double elapsed=diff.tv_sec+diff.tv_usec/1e6;
|
|
||||||
//elapsed=(i)*wspr_symtime;
|
|
||||||
double sched_end=(i+1)*wspr_symtime;
|
|
||||||
//cout << "symbol " << i << " " << wspr_symtime << std::endl;
|
|
||||||
//cout << sched_end-elapsed << std::endl;
|
for (int i = 0; i < 162; i++)
|
||||||
double this_sym=sched_end-elapsed;
|
{
|
||||||
this_sym=(this_sym<.2)?.2:this_sym;
|
double tone_freq=-1.5*tone_spacing+symbols[i]*tone_spacing;
|
||||||
this_sym=(this_sym>2*wspr_symtime)?2*wspr_symtime:this_sym;
|
int Nbtx=0;
|
||||||
txSym(symbols[i], center_freq_actual, tone_spacing, sched_end-elapsed, dma_table_freq, F_PWM_CLK_INIT, instrs, constPage, bufPtr);
|
while(Nbtx<100)
|
||||||
}
|
{
|
||||||
|
usleep(100);
|
||||||
|
int Available=ngfmtest->GetBufferAvailable();
|
||||||
|
if(Available>FifoSize/2)
|
||||||
|
{
|
||||||
|
int Index=ngfmtest->GetUserMemIndex();
|
||||||
|
if(Available>100-Nbtx) Available=100-Nbtx;
|
||||||
|
//printf("GetIndex=%d\n",Index);
|
||||||
|
for(int j=0;j<Available;j++)
|
||||||
|
{
|
||||||
|
//ngfmtest.SetFrequencySample(Index,((i%10000)>5000)?1000:0);
|
||||||
|
ngfmtest->SetFrequencySample(Index+j,tone_freq/*+(rand()/((double)RAND_MAX)-.5)*8.0*/);
|
||||||
|
Nbtx++;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
n_tx++;
|
n_tx++;
|
||||||
|
|
||||||
// Turn transmitter off
|
// Turn transmitter off
|
||||||
txoff();
|
ngfmtest->stop();
|
||||||
|
|
||||||
// End timestamp
|
// End timestamp
|
||||||
gettimeofday(&tvEnd, NULL);
|
gettimeofday(&tvEnd, NULL);
|
||||||
|
|
Ładowanie…
Reference in New Issue