mirror of
https://github.com/cnlohr/lolra.git
synced 2026-06-21 18:29:28 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e79b88354 | |||
| 3bb8c87519 | |||
| 77cfc9f3b7 | |||
| ae63b4208f | |||
| 1940782c3d | |||
| e5cd85ebf8 | |||
| 7f57e4b003 | |||
| 966076d5ed | |||
| ce7b9c05d1 |
@@ -4,7 +4,7 @@ Transmit 900MHz LoRa frames surprisingly far without a radio (And other radio sh
|
||||
|
||||
If you are looking for the Hackaday 2024 microcontroller radio talk, you can <a href=https://cnlohr.github.io/lolra_talk>click here</a>.
|
||||
|
||||
If you are looking for LoLRa Merch (Like t-shirts, etc.), <a href=https://cnlohr-shop.fourthwall.com/?source=dashboard>click here</a>.
|
||||
If you are looking for LoLRa Merch (Like t-shirts, etc.), <a href="https://cnlohr-shop.fourthwall.com/">click here</a>.
|
||||
|
||||
* [Introduct and repo overview](#introduction-and-repo-overview)
|
||||
* LoRa
|
||||
@@ -298,6 +298,13 @@ Low Overhead Radios Using Side-Channels](https://dl.acm.org/doi/abs/10.1145/3583
|
||||
* [Airspy Mini SDR](https://v3.airspy.us/product/a-airspy-mini/)
|
||||
* [LILYGO® T-Beam Meshtastic](https://www.lilygo.cc/products/t-beam-v1-1-esp32-lora-module)
|
||||
|
||||
### LoLRa Like Things
|
||||
* [Wirelessly control 49 MHz toy with I/O line flipping on Pi Pico](https://www.youtube.com/watch?v=K-6dos8Hvm8)
|
||||
|
||||
### Other Interesting Radio Links
|
||||
|
||||
* [Radio station snafu in Seattle bricks some Mazda infotainment systems](https://arstechnica.com/cars/2022/02/radio-station-snafu-in-seattle-bricks-some-mazda-infotainment-systems/)
|
||||
|
||||
## Special Thanks
|
||||
* @MustardTiger for a crazy amount of support work in this project
|
||||
* Willmore for the editing work
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
all : flash
|
||||
|
||||
TARGET:=adcrx
|
||||
TARGET_MCU:=CH32V003
|
||||
CH32V003FUN:=../ch32v003fun/ch32v003fun
|
||||
|
||||
ADDITIONAL_C_FILES+=../rv003usb/rv003usb/rv003usb.S ../rv003usb/rv003usb/rv003usb.c
|
||||
EXTRA_CFLAGS:=-I../rv003usb/lib -I../rv003usb/rv003usb -mstrict-align -Wno-unused-function
|
||||
|
||||
include ../ch32v003fun/ch32v003fun/ch32v003fun.mk
|
||||
|
||||
programmerclock :
|
||||
$(MINICHLINK)/minichlink -X ECLK 1:0:0:8:3
|
||||
|
||||
flash : cv_flash
|
||||
clean : cv_clean
|
||||
rm -rf rf_data_gen chirpbuff.dat chirpbuff.h chirpbuffinfo.h
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
THIS DOES NOT WORK DO NOT USE IT YET
|
||||
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
|
||||
MIT-like-non-ai-license
|
||||
|
||||
Copyright (c) 2024 Charles Lohr "CNLohr"
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the two following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
In addition the following restrictions apply:
|
||||
|
||||
1. The Software and any modifications made to it may not be used for the
|
||||
purpose of training or improving machine learning algorithms, including but not
|
||||
limited to artificial intelligence, natural language processing, or data
|
||||
mining. This condition applies to any derivatives, modifications, or updates
|
||||
based on the Software code. Any usage of the Software in an AI-training dataset
|
||||
is considered a breach of this License.
|
||||
|
||||
2. The Software may not be included in any dataset used for training or
|
||||
improving machine learning algorithms, including but not limited to artificial
|
||||
intelligence, natural language processing, or data mining.
|
||||
|
||||
|
||||
3. Any person or organization found to be in violation of these restrictions
|
||||
will be subject to legal action and may be held liable for any damages
|
||||
resulting from such use.
|
||||
|
||||
If any term is unenforcable, other terms remain in-force.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
**/
|
||||
|
||||
// NOT LORA!!! -- but experimenting with the possibility of rx.
|
||||
|
||||
|
||||
#include "ch32v003fun.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "rv003usb.h"
|
||||
|
||||
uint8_t scratchout[15];
|
||||
volatile int outready = 0;
|
||||
uint8_t scratchin[255];
|
||||
volatile int inready = 0;
|
||||
|
||||
|
||||
#define PWM_PERIOD (28-1) //For 27.000500MHz
|
||||
#define QUADRATURE
|
||||
|
||||
uint32_t TQ = 128;
|
||||
|
||||
#define ADC_BUFFSIZE 256
|
||||
volatile uint16_t adc_buffer[ADC_BUFFSIZE];
|
||||
|
||||
void SetupADC()
|
||||
{
|
||||
// Reset the ADC to init all regs
|
||||
RCC->APB2PRSTR |= RCC_APB2Periph_ADC1;
|
||||
RCC->APB2PRSTR &= ~RCC_APB2Periph_ADC1;
|
||||
|
||||
// ADCCLK = 12 MHz => RCC_ADCPRE divide by 4
|
||||
RCC->CFGR0 &= ~RCC_ADCPRE; // Clear out the bis in case they were set
|
||||
RCC->CFGR0 |= RCC_ADCPRE_DIV2; // Fastest possible (divide-by-2) NOTE: This is OUTSIDE the specified value in the datasheet.
|
||||
|
||||
// Set up single conversion on chl 7
|
||||
ADC1->RSQR1 = 0;
|
||||
ADC1->RSQR2 = 0;
|
||||
ADC1->RSQR3 = 6; // 0-9 for 8 ext inputs and two internals /// 7 or 6 means one of the ADC inputs.
|
||||
|
||||
// Not using injection group.
|
||||
|
||||
// PD4 is analog input chl 7 + 6
|
||||
GPIOD->CFGLR &= ~(0xf<<(4*4)); // CNF = 00: Analog, MODE = 00: Input
|
||||
GPIOD->CFGLR &= ~(0xf<<(4*6)); // CNF = 00: Analog, MODE = 00: Input
|
||||
|
||||
// Sampling time for channels. Careful: This has PID tuning implications.
|
||||
// Note that with 3 and 3,the full loop (and injection) runs at 138kHz.
|
||||
ADC1->SAMPTR2 = (0<<(3*7));
|
||||
|
||||
// Turn on ADC and set rule group to sw trig
|
||||
// 0 = Use TRGO event for Timer 1 to fire ADC rule.
|
||||
ADC1->CTLR2 = ADC_ADON | ADC_EXTTRIG | ADC_DMA;
|
||||
|
||||
// Reset calibration
|
||||
ADC1->CTLR2 |= ADC_RSTCAL;
|
||||
while(ADC1->CTLR2 & ADC_RSTCAL);
|
||||
|
||||
// Calibrate ADC
|
||||
ADC1->CTLR2 |= ADC_CAL;
|
||||
while(ADC1->CTLR2 & ADC_CAL);
|
||||
|
||||
// ADC_SCAN: Allow scanning.
|
||||
ADC1->CTLR1 = ADC_SCAN;
|
||||
|
||||
// Turn on DMA
|
||||
RCC->AHBPCENR |= RCC_AHBPeriph_DMA1;
|
||||
|
||||
//DMA1_Channel1 is for ADC
|
||||
DMA1_Channel1->PADDR = (uint32_t)&ADC1->RDATAR;
|
||||
DMA1_Channel1->MADDR = (uint32_t)adc_buffer;
|
||||
DMA1_Channel1->CNTR = ADC_BUFFSIZE;
|
||||
DMA1_Channel1->CFGR =
|
||||
DMA_M2M_Disable |
|
||||
DMA_Priority_VeryHigh |
|
||||
DMA_MemoryDataSize_HalfWord |
|
||||
DMA_PeripheralDataSize_HalfWord |
|
||||
DMA_MemoryInc_Enable |
|
||||
DMA_Mode_Circular |
|
||||
DMA_DIR_PeripheralSRC;
|
||||
|
||||
// Turn on DMA channel 1
|
||||
DMA1_Channel1->CFGR |= DMA_CFGR1_EN;
|
||||
|
||||
// Enable continuous conversion and DMA
|
||||
//ADC1->CTLR2 |= ADC_DMA | ADC_EXTSEL; //ADC_CONT
|
||||
|
||||
// start conversion
|
||||
ADC1->CTLR2 |= ADC_SWSTART;
|
||||
|
||||
}
|
||||
|
||||
static void SetupTimer1()
|
||||
{
|
||||
// Enable Timer 1
|
||||
RCC->APB2PRSTR |= RCC_APB2Periph_TIM1;
|
||||
RCC->APB2PRSTR &= ~RCC_APB2Periph_TIM1;
|
||||
|
||||
TIM1->PSC = 0x0000; // Prescalar to 0x0000 (so, 48MHz base clock)
|
||||
TIM1->ATRLR = PWM_PERIOD;
|
||||
|
||||
#ifdef PWM_OUTPUT
|
||||
GPIOC->CFGLR &= ~(0xf<<(4*4));
|
||||
GPIOC->CFGLR |= (GPIO_Speed_10MHz | GPIO_CNF_OUT_PP_AF)<<(4*4);
|
||||
|
||||
TIM1->CCER = TIM_CC4E | TIM_CC4P;
|
||||
TIM1->CHCTLR2 = TIM_OC4M_2 | TIM_OC4M_1;
|
||||
TIM1->CH4CVR = 5; // Actual duty cycle (Off to begin with)
|
||||
#endif
|
||||
|
||||
// Setup TRGO for ADC. This makes is to the ADC will trigger on timer
|
||||
// reset, so we trigger at the same position every time relative to the
|
||||
// FET turning on.
|
||||
TIM1->CTLR2 = TIM_MMS_1;
|
||||
|
||||
// Enable TIM1 outputs
|
||||
TIM1->BDTR = TIM_MOE;
|
||||
TIM1->CTLR1 = TIM_CEN;
|
||||
}
|
||||
|
||||
|
||||
void InnerLoop() __attribute__((noreturn));
|
||||
|
||||
|
||||
void InnerLoop()
|
||||
{
|
||||
int i = 0;
|
||||
int q = 0;
|
||||
int tpl = 0;
|
||||
int Q = TQ;
|
||||
|
||||
// Timer goes backwards when we are moving forwards.
|
||||
volatile uint16_t * adc_buffer_end = 0;
|
||||
volatile uint16_t * adc_buffer_top = adc_buffer + ADC_BUFFSIZE;
|
||||
volatile uint16_t * adc = adc_buffer;
|
||||
|
||||
int frcnt = 0;
|
||||
|
||||
int tstart = 0;
|
||||
|
||||
#ifdef DUMPBUFF
|
||||
uint16_t shadowbuff[Q+16];
|
||||
int shadowplace = 0;
|
||||
#define SHADOWSTORE(X) shadowbuff[frcnt+X] = t;
|
||||
#else
|
||||
#define SHADOWSTORE(X)
|
||||
#endif
|
||||
|
||||
while( 1 )
|
||||
{
|
||||
tpl = ADC_BUFFSIZE - DMA1_Channel1->CNTR; // Warning, sometimes this is == to the base, or == 0 (i.e. might be 256, if top is 255)
|
||||
if( tpl == ADC_BUFFSIZE ) tpl = 0;
|
||||
|
||||
adc_buffer_end = adc_buffer + ( ( tpl / 4) * 4 );
|
||||
//printf( "%3d %4d %d %04x\n", DMA1_Channel1->CNTR, TIM1->CNT, ADC1->RDATAR, ADC1->STATR );
|
||||
while( adc != adc_buffer_end )
|
||||
{
|
||||
int32_t t = adc[0]; SHADOWSTORE(0);
|
||||
i += t; q += t;
|
||||
t = adc[1]; SHADOWSTORE(1);
|
||||
i -= t; q += t;
|
||||
t = adc[2]; SHADOWSTORE(2);
|
||||
i -= t; q -= t;
|
||||
t = adc[3]; SHADOWSTORE(3);
|
||||
i += t; q -= t;
|
||||
adc += 4;
|
||||
frcnt += 4;
|
||||
|
||||
if( adc == adc_buffer_top ) adc = adc_buffer;
|
||||
if( frcnt >= Q ) break;
|
||||
}
|
||||
|
||||
|
||||
if( frcnt >= Q )
|
||||
{
|
||||
|
||||
int ti = i>>3;
|
||||
int tq = q>>3;
|
||||
int is = (ti*ti + tq*tq)>>8;
|
||||
|
||||
int s = 1<<( ( 32 - __builtin_clz(is) )/2);
|
||||
s = (s + is/s)/2;
|
||||
|
||||
//int tv = (i>>PWM_OUTPUT) + (PWM_PERIOD/2);
|
||||
//if( tv < 0 ) tv = 0;
|
||||
//if( tv >= PWM_PERIOD ) tv = PWM_PERIOD-1;
|
||||
//TIM1->CH4CVR = tv;
|
||||
|
||||
//printf( "%d\n", s );
|
||||
frcnt = 0;
|
||||
i = 0;
|
||||
q = 0;
|
||||
tpl = ADC_BUFFSIZE - DMA1_Channel1->CNTR;
|
||||
adc = adc_buffer + ( ( tpl / 4) * 4 );
|
||||
tstart = SysTick->CNT;
|
||||
}
|
||||
/*
|
||||
Delay_Us( 100 );
|
||||
int end = DMA1_Channel1->CNTR;
|
||||
int v0 = adc_buffer[0];
|
||||
int v1 = adc_buffer[1];
|
||||
int v2 = adc_buffer[2];
|
||||
int v3 = adc_buffer[3];
|
||||
printf( "%d %d %d %d %d\n", (uint8_t)(start-end), v0, v1, v2, v3 );
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
// REQUIRES External 24MHz oscillator
|
||||
printf( "Initializing\n" );
|
||||
|
||||
SystemInit();
|
||||
|
||||
Delay_Ms(10);
|
||||
|
||||
printf( "System On\n" );
|
||||
|
||||
// Enable Peripherals
|
||||
RCC->APB2PCENR |= RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOC |
|
||||
RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1 | RCC_APB2Periph_ADC1 |
|
||||
RCC_APB2Periph_AFIO;
|
||||
|
||||
RCC->APB1PCENR = RCC_APB1Periph_TIM2;
|
||||
|
||||
// Disable HSI
|
||||
RCC->CTLR &= ~(RCC_HSION);
|
||||
|
||||
printf( "CTLR: %08lx CFGR0: %08lx\n", RCC->CTLR, RCC->CFGR0 );
|
||||
|
||||
SetupADC();
|
||||
|
||||
while(1)
|
||||
printf( "ADC Setup\n" );
|
||||
|
||||
#if 0
|
||||
EXTEN->EXTEN_CTR |= EXTEN_OPA_EN; // turn on the op-amp
|
||||
EXTEN->EXTEN_CTR |= EXTEN_OPA_PSEL; // select op-amp pos pin: 0 = PA2, 1 = PD7
|
||||
EXTEN->EXTEN_CTR |= EXTEN_OPA_NSEL; // select op-amp neg pin: 0 = PA1, 1 = PD0
|
||||
#endif
|
||||
|
||||
// SetupTimer1();
|
||||
printf( "Timer 1 setup\n" );
|
||||
InnerLoop();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void usb_handle_user_in_request( struct usb_endpoint * e, uint8_t * scratchpad, int endp, uint32_t sendtok, struct rv003usb_internal * ist )
|
||||
{
|
||||
// Make sure we only deal with control messages. Like get/set feature reports.
|
||||
if( endp )
|
||||
{
|
||||
usb_send_empty( sendtok );
|
||||
}
|
||||
}
|
||||
|
||||
void usb_handle_user_data( struct usb_endpoint * e, int current_endpoint, uint8_t * data, int len, struct rv003usb_internal * ist )
|
||||
{
|
||||
if( outready )
|
||||
{
|
||||
// Send NACK (can't accept any more data right now)
|
||||
usb_send_data( 0, 0, 2, 0x5A );
|
||||
return;
|
||||
}
|
||||
|
||||
usb_send_data( 0, 0, 2, 0xD2 ); // Send ACK
|
||||
int offset = e->count<<3;
|
||||
int torx = e->max_len - offset;
|
||||
if( torx > len ) torx = len;
|
||||
if( torx > 0 )
|
||||
{
|
||||
memcpy( scratchout + offset, data, torx );
|
||||
e->count++;
|
||||
if( ( e->count << 3 ) >= e->max_len )
|
||||
{
|
||||
outready = e->max_len;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void usb_handle_hid_get_report_start( struct usb_endpoint * e, int reqLen, uint32_t lValueLSBIndexMSB )
|
||||
{
|
||||
if( reqLen > sizeof( scratchin ) ) reqLen = sizeof( scratchin );
|
||||
|
||||
// You can check the lValueLSBIndexMSB word to decide what you want to do here
|
||||
// But, whatever you point this at will be returned back to the host PC where
|
||||
// it calls hid_get_feature_report.
|
||||
//
|
||||
// Please note, that on some systems, for this to work, your return length must
|
||||
// match the length defined in HID_REPORT_COUNT, in your HID report, in usb_config.h
|
||||
|
||||
if( reqLen > inready ) inready = inready;
|
||||
e->opaque = scratchin;
|
||||
e->max_len = reqLen;
|
||||
}
|
||||
|
||||
void usb_handle_hid_set_report_start( struct usb_endpoint * e, int reqLen, uint32_t lValueLSBIndexMSB )
|
||||
{
|
||||
// Here is where you get an alert when the host PC calls hid_send_feature_report.
|
||||
//
|
||||
// You can handle the appropriate message here. Please note that in this
|
||||
// example, the data is chunked into groups-of-8-bytes.
|
||||
//
|
||||
// Note that you may need to make this match HID_REPORT_COUNT, in your HID
|
||||
// report, in usb_config.h
|
||||
|
||||
if( outready ) reqLen = 0;
|
||||
if( reqLen > sizeof( scratchout ) ) reqLen = sizeof( scratchout );
|
||||
e->opaque = scratchout;
|
||||
e->max_len = reqLen;
|
||||
}
|
||||
|
||||
|
||||
void usb_handle_other_control_message( struct usb_endpoint * e, struct usb_urb * s, struct rv003usb_internal * ist )
|
||||
{
|
||||
LogUEvent( SysTick->CNT, s->wRequestTypeLSBRequestMSB, s->lValueLSBIndexMSB, s->wLength );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef _FUNCONFIG_H
|
||||
#define _FUNCONFIG_H
|
||||
|
||||
#define FUNCONF_USE_DEBUGPRINTF 1
|
||||
#define FUNCONF_USE_UARTPRINTF 0
|
||||
#define FUNCONF_USE_HSE 1
|
||||
#define FUNCONF_SYSTICK_USE_HCLK 1
|
||||
|
||||
#endif
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,155 @@
|
||||
#ifndef _USB_CONFIG_H
|
||||
#define _USB_CONFIG_H
|
||||
|
||||
//Defines the number of endpoints for this device. (Always add one for EP0). For two EPs, this should be 3.
|
||||
#define ENDPOINTS 2
|
||||
|
||||
#define USB_PORT D // [A,C,D] GPIO Port to use with D+, D- and DPU
|
||||
#define USB_PIN_DP 3 // [0-4] GPIO Number for USB D+ Pin
|
||||
#define USB_PIN_DM 4 // [0-4] GPIO Number for USB D- Pin
|
||||
#define USB_PIN_DPU 5 // [0-7] GPIO for feeding the 1.5k Pull-Up on USB D- Pin; Comment out if not used / tied to 3V3!
|
||||
|
||||
#define RV003USB_DEBUG_TIMING 0
|
||||
#define RV003USB_OPTIMIZE_FLASH 1
|
||||
#define RV003USB_EVENT_DEBUGGING 0
|
||||
#define RV003USB_HANDLE_IN_REQUEST 1
|
||||
#define RV003USB_OTHER_CONTROL 0
|
||||
#define RV003USB_HANDLE_USER_DATA 1
|
||||
#define RV003USB_HID_FEATURES 1
|
||||
#define RV003USB_USER_DATA_HANDLES_TOKEN 1
|
||||
|
||||
#ifndef __ASSEMBLER__
|
||||
|
||||
#include <tinyusb_hid.h>
|
||||
|
||||
#ifdef INSTANCE_DESCRIPTORS
|
||||
|
||||
//Taken from http://www.usbmadesimple.co.uk/ums_ms_desc_dev.htm
|
||||
static const uint8_t device_descriptor[] = {
|
||||
18, //Length
|
||||
1, //Type (Device)
|
||||
0x10, 0x01, //Spec
|
||||
0x0, //Device Class
|
||||
0x0, //Device Subclass
|
||||
0x0, //Device Protocol (000 = use config descriptor)
|
||||
0x08, //Max packet size for EP0 (This has to be 8 because of the USB Low-Speed Standard)
|
||||
0xcd, 0xab, //ID Vendor
|
||||
0x11, 0x11, //ID Product
|
||||
0x02, 0x00, //ID Rev
|
||||
1, //Manufacturer string
|
||||
2, //Product string
|
||||
3, //Serial string
|
||||
1, //Max number of configurations
|
||||
};
|
||||
|
||||
static const uint8_t special_hid_desc[] = {
|
||||
HID_USAGE_PAGE ( 0xff ), // Vendor-defined page.
|
||||
HID_USAGE ( 0x00 ),
|
||||
HID_REPORT_SIZE ( 8 ),
|
||||
HID_COLLECTION ( HID_COLLECTION_LOGICAL ),
|
||||
HID_REPORT_COUNT ( 255 ), // IN
|
||||
HID_REPORT_ID ( 0xa4 )
|
||||
HID_USAGE ( 0x01 ),
|
||||
HID_FEATURE ( HID_DATA | HID_VARIABLE | HID_ABSOLUTE ) ,
|
||||
HID_REPORT_COUNT_N ( 256, 2 ), // OUT
|
||||
HID_REPORT_ID ( 0xad )
|
||||
HID_USAGE ( 0x01 ),
|
||||
HID_COLLECTION_END,
|
||||
};
|
||||
|
||||
static const uint8_t config_descriptor[] = {
|
||||
// configuration descriptor, USB spec 9.6.3, page 264-266, Table 9-10
|
||||
9, // bLength;
|
||||
2, // bDescriptorType;
|
||||
0x22, 0x00, // wTotalLength
|
||||
|
||||
//34, 0x00, //for just the one descriptor
|
||||
|
||||
0x01, // bNumInterfaces (Normally 1)
|
||||
0x01, // bConfigurationValue
|
||||
0x00, // iConfiguration
|
||||
0x80, // bmAttributes (was 0xa0)
|
||||
0x64, // bMaxPower (200mA)
|
||||
|
||||
//Class FF device.
|
||||
9, // bLength
|
||||
4, // bDescriptorType
|
||||
0, // bInterfaceNumber = 1 instead of 0 -- well make it second.
|
||||
0, // bAlternateSetting
|
||||
1, // bNumEndpoints
|
||||
0x03, // bInterfaceClass (0x03 = HID)
|
||||
0x00, // bInterfaceSubClass
|
||||
0xff, // bInterfaceProtocol (1 = Keyboard, 2 = Mouse)
|
||||
0, // iInterface
|
||||
|
||||
9, // bLength
|
||||
0x21, // bDescriptorType (HID)
|
||||
0x10,0x01, // bcd 1.1
|
||||
0x00, //country code
|
||||
0x01, // Num descriptors
|
||||
0x22, // DescriptorType[0] (HID)
|
||||
sizeof(special_hid_desc), 0x00,
|
||||
|
||||
7, // endpoint descriptor (For endpoint 1)
|
||||
0x05, // Endpoint Descriptor (Must be 5)
|
||||
0x81, // Endpoint Address
|
||||
0x03, // Attributes
|
||||
0x01, 0x00, // Size (We aren't using it)
|
||||
100, // Interval (We don't use it.)
|
||||
};
|
||||
|
||||
#define STR_MANUFACTURER u"CNLohr"
|
||||
#define STR_PRODUCT u"RV003 RVSWDIO Programmer"
|
||||
#define STR_SERIAL u"RVSWDIO003-01"
|
||||
|
||||
struct usb_string_descriptor_struct {
|
||||
uint8_t bLength;
|
||||
uint8_t bDescriptorType;
|
||||
uint16_t wString[];
|
||||
};
|
||||
const static struct usb_string_descriptor_struct string0 __attribute__((section(".rodata"))) = {
|
||||
4,
|
||||
3,
|
||||
{0x0409}
|
||||
};
|
||||
const static struct usb_string_descriptor_struct string1 __attribute__((section(".rodata"))) = {
|
||||
sizeof(STR_MANUFACTURER),
|
||||
3,
|
||||
STR_MANUFACTURER
|
||||
};
|
||||
const static struct usb_string_descriptor_struct string2 __attribute__((section(".rodata"))) = {
|
||||
sizeof(STR_PRODUCT),
|
||||
3,
|
||||
STR_PRODUCT
|
||||
};
|
||||
const static struct usb_string_descriptor_struct string3 __attribute__((section(".rodata"))) = {
|
||||
sizeof(STR_SERIAL),
|
||||
3,
|
||||
STR_SERIAL
|
||||
};
|
||||
|
||||
// This table defines which descriptor data is sent for each specific
|
||||
// request from the host (in wValue and wIndex).
|
||||
const static struct descriptor_list_struct {
|
||||
uint32_t lIndexValue;
|
||||
const uint8_t *addr;
|
||||
uint8_t length;
|
||||
} descriptor_list[] = {
|
||||
{0x00000100, device_descriptor, sizeof(device_descriptor)},
|
||||
{0x00000200, config_descriptor, sizeof(config_descriptor)},
|
||||
// interface number // 2200 for hid descriptors.
|
||||
{0x00002200, special_hid_desc, sizeof(special_hid_desc)},
|
||||
{0x00002100, config_descriptor + 18, 9 }, // Not sure why, this seems to be useful for Windows + Android.
|
||||
|
||||
{0x00000300, (const uint8_t *)&string0, 4},
|
||||
{0x04090301, (const uint8_t *)&string1, sizeof(STR_MANUFACTURER)},
|
||||
{0x04090302, (const uint8_t *)&string2, sizeof(STR_PRODUCT)},
|
||||
{0x04090303, (const uint8_t *)&string3, sizeof(STR_SERIAL)}
|
||||
};
|
||||
#define DESCRIPTOR_LIST_ENTRIES ((sizeof(descriptor_list))/(sizeof(struct descriptor_list_struct)) )
|
||||
|
||||
#endif // INSTANCE_DESCRIPTORS
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@ all : flash
|
||||
|
||||
TARGET:=loratest
|
||||
TARGET_MCU:=CH32V203
|
||||
TARGET_MCU_PACKAGE:=C8
|
||||
CH32V003FUN:=../ch32v003fun/ch32v003fun
|
||||
|
||||
EXTRA_ELF_DEPENDENCIES:=chirpbuff.h
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
all : genericgen
|
||||
|
||||
genericgen : genericgen.c
|
||||
gcc -g -o $@ $^
|
||||
|
||||
clean :
|
||||
rm -rf genericgen
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
|
||||
MIT-like-non-ai-license
|
||||
|
||||
Copyright (c) 2024 Charles Lohr "CNLohr"
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the two following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
In addition the following restrictions apply:
|
||||
|
||||
1. The Software and any modifications made to it may not be used for the
|
||||
purpose of training or improving machine learning algorithms, including but not
|
||||
limited to artificial intelligence, natural language processing, or data
|
||||
mining. This condition applies to any derivatives, modifications, or updates
|
||||
based on the Software code. Any usage of the Software in an AI-training dataset
|
||||
is considered a breach of this License.
|
||||
|
||||
2. The Software may not be included in any dataset used for training or
|
||||
improving machine learning algorithms, including but not limited to artificial
|
||||
intelligence, natural language processing, or data mining.
|
||||
|
||||
3. Any person or organization found to be in violation of these restrictions
|
||||
will be subject to legal action and may be held liable for any damages
|
||||
resulting from such use.
|
||||
|
||||
If any term is unenforcable, other terms remain in-force.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
**/
|
||||
|
||||
// Designed for generating chirp buffer to mimic hackaday badge... The idea is
|
||||
// you can feed this data you want to send... and it feeds you a generic
|
||||
// language of quarter chirps telling you when to up or down chirp.
|
||||
//
|
||||
// If the output value is negative, you need to emit a down-chirp. If it is
|
||||
// positive, you output an up-chirp.
|
||||
//
|
||||
// Down chirps are represented in one's compliment.
|
||||
|
||||
|
||||
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "../lib/LoRa-SDR-Code.h"
|
||||
|
||||
// Optionally send LoRaWAN messages. Be sure to copy your keys from the things network.
|
||||
//#define LORAWAN
|
||||
#define HACKADAY
|
||||
|
||||
|
||||
#ifdef LORAWAN
|
||||
#include "lorawan_simple.h"
|
||||
static const uint8_t payload_key[AES_BLOCKLEN] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // AppSKey Big Endian
|
||||
static const uint8_t network_skey[AES_BLOCKLEN] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // NwkSKey Big Endian
|
||||
static const uint8_t devaddress[4] = { 0x00, 0x00, 0x00, 0x00 }; // Device address Little Endian LSB (Written backwards from Device address default view)
|
||||
#endif
|
||||
|
||||
#ifdef HACKADAY
|
||||
#define SF_NUMBER 7
|
||||
#define CHIRPLENGTH_WORDS 128
|
||||
#endif
|
||||
|
||||
#if !defined( LORAWAN ) && !defined( HACKADAY )
|
||||
#error Need to define LORWAN or HACKADAY
|
||||
#endif
|
||||
|
||||
// Bits are shifted out MSBit first, then to LSBit
|
||||
|
||||
|
||||
#define MAX_BYTES 160
|
||||
#define MAX_SYMBOLS (MAX_BYTES*2+16)
|
||||
|
||||
// Our table is bespoke for the specific SF.
|
||||
#define CHIPSSPREAD CHIRPLENGTH_WORDS // QUARTER_CHIRP_LENGTH_WORDS (TODO: Use the quater value elsewhere in the code)
|
||||
#define MARK_FROM_SF0 (1<<SF_NUMBER) // SF7
|
||||
|
||||
#define PREAMBLE_CHIRPS 16
|
||||
#define CODEWORD_LENGTH 2
|
||||
|
||||
uint32_t quadsetcount;
|
||||
int16_t quadsets[MAX_SYMBOLS*4+PREAMBLE_CHIRPS*4+9+CODEWORD_LENGTH*4];
|
||||
int runningcount_bits = 0;
|
||||
volatile int fxcycle;
|
||||
volatile int quadsetplace = -1;
|
||||
|
||||
|
||||
|
||||
|
||||
int16_t * AddChirp( int16_t * qso, int offset, int verneer )
|
||||
{
|
||||
offset = offset * CHIPSSPREAD / (MARK_FROM_SF0);
|
||||
offset += verneer;
|
||||
*(qso++) = (CHIPSSPREAD * 0 / 4 + offset + CHIPSSPREAD ) % CHIPSSPREAD;
|
||||
*(qso++) = (CHIPSSPREAD * 1 / 4 + offset + CHIPSSPREAD ) % CHIPSSPREAD;
|
||||
*(qso++) = (CHIPSSPREAD * 2 / 4 + offset + CHIPSSPREAD ) % CHIPSSPREAD;
|
||||
*(qso++) = (CHIPSSPREAD * 3 / 4 + offset + CHIPSSPREAD ) % CHIPSSPREAD;
|
||||
return qso;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
|
||||
#if 0
|
||||
SystemInit();
|
||||
|
||||
funGpioInitAll();
|
||||
|
||||
// Force HCLK to be nodiv. ch32vfun sets it to be a reasonbly high div.
|
||||
RCC->CFGR0 &= ~RCC_PPRE1_DIV16;
|
||||
|
||||
// MCO for testing.
|
||||
// funPinMode( PA8, GPIO_CFGLR_OUT_50Mhz_AF_PP ); RCC->CFGR0 |= RCC_CFGR0_MCO_PLL;
|
||||
|
||||
printf( "Switching to HSE\n" );
|
||||
Delay_Ms( 100 );
|
||||
|
||||
// Disable clock security system.
|
||||
RCC->CTLR &= ~RCC_CSSON;
|
||||
|
||||
#ifdef USE_EXTERNAL_CLOCK
|
||||
// No crystal - use clock.
|
||||
RCC->CTLR |= RCC_HSEBYP;
|
||||
#endif
|
||||
|
||||
// Enable external crystal
|
||||
RCC->CTLR |= RCC_HSEON;
|
||||
|
||||
// Set System Clock Source to be 0.
|
||||
RCC->CFGR0 = (RCC->CFGR0 & ~RCC_SW) | 0;
|
||||
|
||||
// Disable PLL
|
||||
RCC->CTLR &= ~RCC_PLLON;
|
||||
|
||||
|
||||
#ifdef USE_EXTERNAL_CLOCK
|
||||
// Set PLL to 9x not 18x
|
||||
RCC->CFGR0 = ( RCC->CFGR0 & ~RCC_PLLMULL18 ) | RCC_PLLMULL9;
|
||||
#endif
|
||||
|
||||
// Switch to HSE
|
||||
RCC->CFGR0 |= RCC_PLLSRC;
|
||||
|
||||
// Enable PLL
|
||||
RCC->CTLR |= RCC_PLLON;
|
||||
|
||||
// Wait for HSE to become ready.
|
||||
while( !( RCC->CTLR & RCC_HSERDY) );
|
||||
while( !( RCC->CTLR & RCC_PLLRDY) );
|
||||
RCC->CFGR0 |= RCC_SW_1; // Switch system clock to PLL
|
||||
printf( "HSE Switched\n" );
|
||||
Delay_Ms( 10 );
|
||||
RCC->CTLR &= ~RCC_HSION;
|
||||
Delay_Ms( 10 );
|
||||
printf( "HSI Off [%08lx %08lx]\n", RCC->CTLR, RCC->CFGR0 );
|
||||
|
||||
// funPinMode( PB12, GPIO_CFGLR_OUT_50Mhz_AF_PP ); // NSS
|
||||
// funPinMode( PB13, GPIO_CFGLR_OUT_50Mhz_AF_PP ); // SCK
|
||||
// funPinMode( PB14, GPIO_CFGLR_OUT_50Mhz_AF_PP ); // MISO
|
||||
funPinMode( PB15, GPIO_CFGLR_OUT_50Mhz_AF_PP ); // MOSI
|
||||
|
||||
RCC->APB1PRSTR = RCC_SPI2RST;
|
||||
RCC->APB1PRSTR = 0;
|
||||
RCC->APB1PCENR |= RCC_APB1Periph_SPI2;
|
||||
RCC->AHBPCENR |= RCC_AHBPeriph_DMA1;
|
||||
|
||||
// Configure SPI
|
||||
SPI2->CTLR1 =
|
||||
SPI_NSS_Soft | SPI_CPHA_1Edge | SPI_CPOL_Low | SPI_DataSize_16b |
|
||||
SPI_Mode_Master | SPI_Direction_1Line_Tx |
|
||||
0 |
|
||||
0<<3; // Divisior = 0
|
||||
|
||||
// If using DMA may need this.
|
||||
SPI2->CTLR2 = SPI_CTLR2_TXDMAEN;
|
||||
|
||||
|
||||
SPI2->HSCR = 1; // High-speed enable.
|
||||
|
||||
SPI2->CTLR1 |= CTLR1_SPE_Set;
|
||||
//SPI2->DATAR = 0x55aa; // Set SPI line Low.
|
||||
|
||||
//DMA1_Channel5 is for SPI2TX
|
||||
DMA1_Channel5->PADDR = (uint32_t)&SPI2->DATAR;
|
||||
DMA1_Channel5->MADDR = (uint32_t)sendbuff;
|
||||
DMA1_Channel5->CNTR = 0;// sizeof( bufferset )/2; // Number of unique copies. (Don't start, yet!)
|
||||
DMA1_Channel5->CFGR =
|
||||
DMA_M2M_Disable |
|
||||
DMA_Priority_VeryHigh |
|
||||
DMA_MemoryDataSize_HalfWord |
|
||||
DMA_PeripheralDataSize_HalfWord |
|
||||
DMA_MemoryInc_Enable |
|
||||
DMA_Mode_Normal | // OR DMA_Mode_Circular or DMA_Mode_Normal
|
||||
DMA_DIR_PeripheralDST |
|
||||
DMA_IT_TC | DMA_IT_HT; // Transmission Complete + Half Empty Interrupts.
|
||||
|
||||
NVIC_EnableIRQ( DMA1_Channel5_IRQn );
|
||||
DMA1_Channel5->CFGR |= DMA_CFGR1_EN;
|
||||
|
||||
|
||||
memset( sendbuff, 0x00, sizeof( sendbuff ) );
|
||||
|
||||
// Enter critical section.
|
||||
DMA1_Channel5->CNTR = 0;
|
||||
DMA1_Channel5->MADDR = (uint32_t)sendbuff;
|
||||
DMA1_Channel5->CNTR = SENDBUFF_WORDS; // Number of unique uint16_t entries.
|
||||
DMA1_Channel5->CFGR |= DMA_Mode_Circular;
|
||||
#endif
|
||||
|
||||
uint16_t lora_symbols[MAX_SYMBOLS];
|
||||
int lora_symbols_count;
|
||||
|
||||
//while(1)
|
||||
{
|
||||
|
||||
//Delay_Ms( 1000 );
|
||||
|
||||
|
||||
#ifdef LORAWAN
|
||||
//Delay_Ms( 1000 );
|
||||
static uint32_t frame = 0;
|
||||
|
||||
// Send a message with LoraWan. Formatted specifically for thethings.network.
|
||||
uint8_t inner_payload_raw[24];
|
||||
int inner_payload_len = snprintf( (char*)inner_payload_raw, 24, "meow%lu ", frame%10 );
|
||||
inner_payload_len = 5;
|
||||
|
||||
// Just some random data.
|
||||
uint8_t raw_payload_with_b0[259+8] = { };
|
||||
uint8_t * payload_in = raw_payload_with_b0 + 16;
|
||||
uint8_t * pl = payload_in;
|
||||
int payload_in_size = 0;
|
||||
|
||||
pl += GenerateLoRaWANPacket( raw_payload_with_b0, inner_payload_raw, inner_payload_len, payload_key, network_skey, devaddress, frame++);
|
||||
|
||||
payload_in_size = pl - payload_in;
|
||||
|
||||
lora_symbols_count = 0;
|
||||
#else
|
||||
|
||||
// Just some random data.
|
||||
|
||||
uint8_t payload_in[128] = {
|
||||
0x07, 0xe9, 0x23, 0xee, 0x03, 0x80, 0xff, 0xff, 0xff, 0xff, 0x16, 0xca,
|
||||
0x51, 0xd0, 0x06, 0x00, 0x02, 0x59, 0x6c, 0x6f, 0x6c, 0x72, 0x61, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x61, 0x61, 0x61, 0x61, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
};
|
||||
|
||||
int payload_in_size = sizeof(payload_in);
|
||||
|
||||
static int msgno = 0;
|
||||
payload_in[4] = msgno++;
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
lora_symbols_count = 0;
|
||||
// int r = CreateMessageFromPayload( lora_symbols, &lora_symbols_count, MAX_SYMBOLS, SF_NUMBER, 4, payload_in, payload_in_size );
|
||||
|
||||
// I think the Hackaday swadge uses 5/4, not 8/4
|
||||
int r = CreateMessageFromPayload( lora_symbols, &lora_symbols_count, MAX_SYMBOLS, SF_NUMBER, 1, payload_in, payload_in_size );
|
||||
|
||||
if( r < 0 )
|
||||
{
|
||||
printf( "Failed to generate message (%d)\n", r );
|
||||
// Failed
|
||||
return -1;
|
||||
}
|
||||
|
||||
int j;
|
||||
|
||||
quadsetcount = 0;
|
||||
int16_t * qso = quadsets;
|
||||
for( j = 0; j < PREAMBLE_CHIRPS; j++ )
|
||||
{
|
||||
qso = AddChirp( qso, 0, 0 );
|
||||
}
|
||||
|
||||
// Hackaday Syncword.
|
||||
uint8_t syncword = 0x21;
|
||||
|
||||
#define CODEWORD_SHIFT 3
|
||||
|
||||
if( CODEWORD_LENGTH > 0 )
|
||||
qso = AddChirp( qso, ( ( syncword & 0xf ) << CODEWORD_SHIFT ), 0 );
|
||||
if( CODEWORD_LENGTH > 1 )
|
||||
qso = AddChirp( qso, ( ( ( syncword & 0xf0 ) >> 4 ) << CODEWORD_SHIFT ), 0);
|
||||
|
||||
*(qso++) = -(CHIPSSPREAD * 0 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 1 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 2 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 3 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 0 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 1 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 2 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 3 / 4 )-1;
|
||||
*(qso++) = -(CHIPSSPREAD * 0 / 4 )-1;
|
||||
|
||||
if( SF_NUMBER <= 6 )
|
||||
{
|
||||
// Two additional upchirps with SF6 https://github.com/tapparelj/gr-lora_sdr/issues/74#issuecomment-1891569580
|
||||
for( j = 0; j < 2; j++ )
|
||||
qso = AddChirp( qso, 0, 0 );
|
||||
}
|
||||
|
||||
for( j = 0; j < lora_symbols_count; j++ )
|
||||
{
|
||||
int ofs = lora_symbols[j];
|
||||
//ofs = ofs ^ ((MARK_FROM_SF6<<6) -1);
|
||||
//ofs &= (MARK_FROM_SF6<<6) -1;
|
||||
qso = AddChirp( qso, ofs, 0 );
|
||||
//printf( "0x%03x, ", ofs );
|
||||
}
|
||||
printf( "\n" );
|
||||
|
||||
printf( "const int16_t quadstarts[%ld] = {", qso - quadsets );
|
||||
for (j = 0; j < qso - quadsets; j++ )
|
||||
{
|
||||
if( ( j & 0xf ) == 0 ) printf( "\n\t" );
|
||||
printf( "%4d, ", quadsets[j] );
|
||||
}
|
||||
printf( "};\n" );
|
||||
//int16_t * qso = quadsets
|
||||
|
||||
|
||||
runningcount_bits = 0;
|
||||
|
||||
// This tells the interrupt we have data.
|
||||
quadsetcount = qso - quadsets + 0;
|
||||
printf( "--- %d [%d] %d\n", (int)lora_symbols_count, (int)quadsetcount, CHIPSSPREAD/4 );
|
||||
|
||||
quadsetplace = 0;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user