add usart
This commit is contained in:
parent
b01b96ce02
commit
03e2f96768
11 changed files with 472 additions and 0 deletions
|
@ -2701,6 +2701,7 @@ struct USART1_Type { /*!< Universal synchronous asynchronous receiver transmitte
|
|||
} B;
|
||||
__IO uint32_t R;
|
||||
explicit STATR_DEF () noexcept { R = 0x000000c0u; }
|
||||
explicit STATR_DEF (volatile STATR_DEF & o) noexcept { R = o.R; };
|
||||
template<typename F> void setbit (F f) volatile {
|
||||
STATR_DEF r;
|
||||
R = f (r);
|
||||
|
|
76
ch32v003/usartclass.cpp
Normal file
76
ch32v003/usartclass.cpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
#include "system.h"
|
||||
#include "usartclass.h"
|
||||
static UsartClass * pInstance = nullptr;
|
||||
static constexpr unsigned HCLK = 48'000'000u;
|
||||
extern "C" void USART1_IRQHandler (void) __attribute__((interrupt));
|
||||
void USART1_IRQHandler (void) {
|
||||
if (pInstance) pInstance->irq();
|
||||
};
|
||||
|
||||
UsartClass::UsartClass(const unsigned int _baud) noexcept : BaseLayer (), tx_ring () {
|
||||
pInstance = this;
|
||||
// 1. Clock Enable
|
||||
RCC.APB2PCENR.modify([](RCC_Type::APB2PCENR_DEF & r) -> auto {
|
||||
r.B.USART1EN = SET;
|
||||
r.B.IOPDEN = SET;
|
||||
return r.R;
|
||||
});
|
||||
// 2. GPIO Alternate Config - default TX/PD5, RX/PD6
|
||||
GPIOD.CFGLR.modify([](GPIOA_Type::CFGLR_DEF & r) -> auto {
|
||||
r.B.MODE5 = 1u;
|
||||
r.B.CNF5 = 2u; // or 3u for open drain
|
||||
r.B.MODE6 = 0u;
|
||||
r.B.CNF6 = 1u; // floating input
|
||||
return r.R;
|
||||
});
|
||||
// 4. NVIC
|
||||
NVIC.EnableIRQ (USART1_IRQn);
|
||||
// 5. USART registry 8.bit bez parity
|
||||
USART1.CTLR1.modify([] (USART1_Type::CTLR1_DEF & r) -> auto {
|
||||
r.B.RE = SET;
|
||||
r.B.TE = SET;
|
||||
r.B.RXNEIE = SET;
|
||||
return r.R;
|
||||
});
|
||||
USART1.CTLR2.R = 0;
|
||||
//USART1.CTLR3.B.OVRDIS = SET;
|
||||
const uint32_t tmp = HCLK / _baud;
|
||||
USART1.BRR.R = tmp;
|
||||
USART1.CTLR1.B.UE = SET; // nakonec povolit globálně
|
||||
}
|
||||
void UsartClass::irq () {
|
||||
volatile USART1_Type::STATR_DEF status (USART1.STATR); // načti status přerušení
|
||||
char rdata, tdata;
|
||||
|
||||
if (status.B.TXE) { // od vysílače
|
||||
if (tx_ring.Read (tdata)) { // pokud máme data
|
||||
USART1.DATAR.B.DR = (uint8_t) tdata; // zapíšeme do výstupu
|
||||
} else { // pokud ne
|
||||
// Předpoklad je half-duplex i.e. RS485, jinak jen zakázat TXEIE
|
||||
rdata = (USART1.DATAR.B.DR); // dummy read
|
||||
USART1.CTLR1.modify([](USART1_Type::CTLR1_DEF & r) -> auto {
|
||||
r.B.RE = SET; // povol prijem
|
||||
r.B.TXEIE = RESET; // je nutné zakázat přerušení od vysílače
|
||||
return r.R;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (status.B.RXNE) { // od přijímače
|
||||
rdata = (USART1.DATAR.B.DR); // načteme data
|
||||
Up (&rdata, 1u); // a pošleme dál
|
||||
}
|
||||
}
|
||||
uint32_t UsartClass::Down(const char * data, const uint32_t len) {
|
||||
unsigned n = 0u;
|
||||
for (n=0u; n<len; n++) {
|
||||
if (!tx_ring.Write(data[n])) break;
|
||||
}
|
||||
USART1.CTLR1.modify([](USART1_Type::CTLR1_DEF & r) -> auto {
|
||||
r.B.RE = RESET;
|
||||
r.B.TXEIE = SET; // po povolení přerušení okamžitě přeruší
|
||||
return r.R;
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
|
14
ch32v003/usartclass.h
Normal file
14
ch32v003/usartclass.h
Normal file
|
@ -0,0 +1,14 @@
|
|||
#ifndef USARTCLASS_H
|
||||
#define USARTCLASS_H
|
||||
#include "baselayer.h"
|
||||
#include "fifo.h"
|
||||
|
||||
class UsartClass : public BaseLayer {
|
||||
FIFO<char, 64u> tx_ring;
|
||||
public:
|
||||
explicit UsartClass (const unsigned _baud) noexcept;
|
||||
uint32_t Down (const char * data, const uint32_t len) override;
|
||||
void irq ();
|
||||
};
|
||||
|
||||
#endif // USARTCLASS_H
|
74
common/baselayer.h
Normal file
74
common/baselayer.h
Normal file
|
@ -0,0 +1,74 @@
|
|||
#ifndef BASELAYER_H
|
||||
#define BASELAYER_H
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __arm__
|
||||
#define debug(...)
|
||||
#else // ARCH_CM0
|
||||
#ifdef DEBUG
|
||||
#define debug printf
|
||||
#else // DEBUG
|
||||
#define debug(...)
|
||||
#endif // DEBUG
|
||||
#endif // ARCH_CM0
|
||||
/** @brief Bázová třída pro stack trochu obecnějšího komunikačního protokolu.
|
||||
*
|
||||
* @class BaseLayer
|
||||
* @brief Od této třídy budeme dále odvozovat ostatní.
|
||||
*
|
||||
*/
|
||||
class BaseLayer {
|
||||
public:
|
||||
/** Konstruktor
|
||||
*/
|
||||
explicit constexpr BaseLayer () noexcept : pUp(nullptr), pDown(nullptr) {};
|
||||
/** Virtuální metoda, přesouvající data směrem nahoru, pokud s nimi nechceme dělat něco jiného.
|
||||
@param data ukazatel na pole dat
|
||||
@param len delka dat v bytech
|
||||
@return počet přenesených bytů
|
||||
*/
|
||||
virtual uint32_t Up (const char * data, const uint32_t len) {
|
||||
if (pUp) return pUp->Up (data, len);
|
||||
return 0;
|
||||
};
|
||||
/** Virtuální metoda, přesouvající data směrem dolů, pokud s nimi nechceme dělat něco jiného.
|
||||
@param data ukazatel na pole dat
|
||||
@param len delka dat v bytech
|
||||
@return počet přenesených bytů
|
||||
*/
|
||||
virtual uint32_t Down (const char * data, const uint32_t len) {
|
||||
if (pDown) return pDown->Down (data, len);
|
||||
return len;
|
||||
};
|
||||
/** @brief Zřetězení stacku.
|
||||
* Tohle je vlastně to nejdůležitější. V čistém C by se musely
|
||||
* nastavovat ukazatele na callback funkce, tady je to čitší - pro uživatele neviditelné,
|
||||
* ale je to to samé.
|
||||
@param bl Třída, ležící pod, spodní
|
||||
@return Odkaz na tuto třídu (aby se to dalo řetězit)
|
||||
*/
|
||||
virtual BaseLayer & operator += (BaseLayer & bl) {
|
||||
bl.setUp (this); // ta spodní bude volat při Up tuto třídu
|
||||
setDown (& bl); // a tato třída bude volat při Down tu spodní
|
||||
return * this;
|
||||
};
|
||||
/** Getter pro pDown
|
||||
@return pDown
|
||||
*/
|
||||
BaseLayer * getDown (void) const { return pDown; };
|
||||
protected:
|
||||
/** Lokální setter pro pUp
|
||||
@param p Co budeme do pUp dávat
|
||||
*/
|
||||
void setUp (BaseLayer * p) { pUp = p; };
|
||||
/** Lokální setter pro pDown
|
||||
@param p Co budeme do pDown dávat
|
||||
*/
|
||||
void setDown (BaseLayer * p) { pDown = p; };
|
||||
private:
|
||||
// Ono to je vlastně oboustranně vázaný spojový seznam.
|
||||
BaseLayer * pUp; //!< Ukazatel na třídu, která bude dále volat Up
|
||||
BaseLayer * pDown; //!< Ukazatel na třídu, která bude dále volat Down
|
||||
};
|
||||
|
||||
#endif // BASELAYER_H
|
73
common/fifo.h
Normal file
73
common/fifo.h
Normal file
|
@ -0,0 +1,73 @@
|
|||
#ifndef FIFO_H
|
||||
#define FIFO_H
|
||||
/** Typ dbus_w_t je podobně definován jako sig_atomic_t v hlavičce signal.h.
|
||||
* Je to prostě největší typ, ke kterému je "atomický" přístup. V GCC je definováno
|
||||
* __SIG_ATOMIC_TYPE__, šlo by použít, ale je znaménkový.
|
||||
* */
|
||||
#ifdef __SIG_ATOMIC_TYPE__
|
||||
typedef unsigned __SIG_ATOMIC_TYPE__ dbus_w_t;
|
||||
#else
|
||||
typedef unsigned int dbus_w_t; // pro AVR by to měl být uint8_t (šířka datové sběrnice)
|
||||
#endif //__SIG_ATOMIC_TYPE__
|
||||
/// Tahle podivná rekurzívní formule je použita pro validaci délky bufferu.
|
||||
static constexpr bool isValidM (const int N, const dbus_w_t M) {
|
||||
// constexpr má raději rekurzi než cyklus (c++11)
|
||||
return (N > 12) ? false : (((1u << N) == M) ? true : isValidM (N+1, M));
|
||||
}
|
||||
/** @class FIFO
|
||||
* @brief Jednoduchá fronta (kruhový buffer).
|
||||
*
|
||||
* V tomto přikladu je vidět, že synchronizace mezi přerušením a hlavní smyčkou programu
|
||||
* může být tak jednoduchá, že je v podstatě neviditelná. Využívá se toho, že pokud
|
||||
* do kruhového buferu zapisujeme jen z jednoho bodu a čteme také jen z jednoho bodu
|
||||
* (vlákna), zápis probíhá nezávisle pomocí indexu m_head a čtení pomocí m_tail.
|
||||
* Délka dat je dána rozdílem tt. indexů, pokud v průběhu výpočtu délky dojde k přerušení,
|
||||
* v zásadě se nic špatného neděje, maximálně je délka určena špatně a to tak,
|
||||
* že zápis nebo čtení je nutné opakovat. Důležité je, že po výpočtu se nová délka zapíše
|
||||
* do paměti "atomicky". Takže např. pro 8-bit procesor musí být indexy jen 8-bitové.
|
||||
* To není moc velké omezení, protože tyto procesory obvykle mají dost malou RAM, takže
|
||||
* velikost bufferu stejně nebývá být větší než nějakých 64 položek.
|
||||
* Opět nijak nevadí že přijde přerušení při zápisu nebo čtení položky - to se provádí
|
||||
* dříve než změna indexu, zápis a čtení je vždy na jiném místě RAM. Celé je to uděláno
|
||||
* jako šablona, takže je možné řadit do fronty i složitější věci než je pouhý byte.
|
||||
* Druhým parametrem šablony je délka bufferu (aby to šlo konstruovat jako statický objekt),
|
||||
* musí to být mocnina dvou v rozsahu 8 až 4096, default je 64. Mocnina 2 je zvolena proto,
|
||||
* aby se místo zbytku po dělení mohl použít jen bitový and, což je rychlejší.
|
||||
* */
|
||||
template<typename T, const dbus_w_t M = 64> class FIFO {
|
||||
T m_data [M];
|
||||
volatile dbus_w_t m_head; //!< index pro zápis (hlava)
|
||||
volatile dbus_w_t m_tail; //!< index pro čtení (ocas)
|
||||
/// vrací skutečnou délku dostupných dat
|
||||
constexpr dbus_w_t lenght () const { return (M + m_head - m_tail) & (M - 1); };
|
||||
/// zvětší a saturuje index, takže se tento motá v kruhu @param n index
|
||||
void sat_inc (volatile dbus_w_t & n) const { n = (n + 1) & (M - 1); };
|
||||
public:
|
||||
/// Konstruktor
|
||||
explicit constexpr FIFO<T,M> () noexcept {
|
||||
// pro 8-bit architekturu může být byte jako index poměrně malý
|
||||
static_assert (1ul << (8 * sizeof(dbus_w_t) - 1) >= M, "atomic type too small");
|
||||
// a omezíme pro jistotu i delku buferu na nějakou rozumnou delku
|
||||
static_assert (isValidM (3, M), "M must be power of two in range <8,4096> or <8,128> for 8-bit data bus (AVR)");
|
||||
m_head = 0;
|
||||
m_tail = 0;
|
||||
}
|
||||
/// Čtení položky
|
||||
/// @return true, pokud se úspěšně provede
|
||||
const bool Read (T & c) {
|
||||
if (lenght() == 0) return false;
|
||||
c = m_data [m_tail];
|
||||
sat_inc (m_tail);
|
||||
return true;
|
||||
}
|
||||
/// Zápis položky
|
||||
/// @return true, pokud se úspěšně provede
|
||||
const bool Write (const T & c) {
|
||||
if (lenght() >= (M - 1)) return false;
|
||||
m_data [m_head] = c;
|
||||
sat_inc (m_head);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // FIFO_H
|
85
common/print.cpp
Normal file
85
common/print.cpp
Normal file
|
@ -0,0 +1,85 @@
|
|||
#include "print.h"
|
||||
|
||||
#define sleep()
|
||||
|
||||
static const char * hexStr = "0123456789ABCDEF";
|
||||
static const uint16_t numLen[] = {1, 32, 1, 11, 8, 0};
|
||||
|
||||
Print::Print (PrintBases b) : BaseLayer () {
|
||||
base = b;
|
||||
}
|
||||
// Výstup blokujeme podle toho, co se vrací ze spodní vrstvy
|
||||
uint32_t Print::BlockDown (const char * buf, uint32_t len) {
|
||||
uint32_t n, ofs = 0, req = len;
|
||||
for (;;) {
|
||||
// spodní vrstva může vrátit i nulu, pokud je FIFO plné
|
||||
n = BaseLayer::Down (buf + ofs, req);
|
||||
ofs += n; // Posuneme ukazatel
|
||||
req -= n; // Zmenšíme další požadavek
|
||||
if (!req) break;
|
||||
sleep(); // A klidně můžeme spát
|
||||
}
|
||||
return ofs;
|
||||
}
|
||||
|
||||
Print& Print::operator<< (const char * str) {
|
||||
uint32_t i = 0;
|
||||
while (str[i++]); // strlen
|
||||
BlockDown (str, --i);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Print& Print::operator<< (const int num) {
|
||||
uint32_t i = BUFLEN;
|
||||
|
||||
if (base == DEC) {
|
||||
unsigned int u;
|
||||
if (num < 0) u = -num;
|
||||
else u = num;
|
||||
do {
|
||||
// Knihovní div() je nevhodné - dělí 2x.
|
||||
// Přímočaré a funkční řešení
|
||||
uint32_t rem;
|
||||
rem = u % (unsigned) DEC; // 1.dělení
|
||||
u = u / (unsigned) DEC; // 2.dělení
|
||||
buf [--i] = hexStr [rem];
|
||||
} while (u);
|
||||
if (num < 0) buf [--i] = '-';
|
||||
} else {
|
||||
uint32_t m = (1U << (uint32_t) base) - 1U;
|
||||
uint32_t l = (uint32_t) numLen [(int) base];
|
||||
uint32_t u = (uint32_t) num;
|
||||
for (unsigned n=0; n<l; n++) {
|
||||
buf [--i] = hexStr [u & m];
|
||||
u >>= (unsigned) base;
|
||||
}
|
||||
if (base == BIN) buf [--i] = 'b';
|
||||
if (base == HEX) buf [--i] = 'x';
|
||||
buf [--i] = '0';
|
||||
}
|
||||
BlockDown (buf+i, BUFLEN-i);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Print& Print::operator<< (const PrintBases num) {
|
||||
base = num;
|
||||
return *this;
|
||||
}
|
||||
void Print::out (const void * p, uint32_t l) {
|
||||
const unsigned char* q = (const unsigned char*) p;
|
||||
unsigned char uc;
|
||||
uint32_t k, n = 0;
|
||||
for (uint32_t i=0; i<l; i++) {
|
||||
uc = q[i];
|
||||
buf[n++] = '<';
|
||||
k = uc >> 4;
|
||||
buf[n++] = hexStr [k];
|
||||
k = uc & 0x0f;
|
||||
buf[n++] = hexStr [k];
|
||||
buf[n++] = '>';
|
||||
}
|
||||
buf[n++] = '\r';
|
||||
buf[n++] = '\n';
|
||||
BlockDown (buf, n);
|
||||
}
|
||||
|
73
common/print.h
Normal file
73
common/print.h
Normal file
|
@ -0,0 +1,73 @@
|
|||
#ifndef PRINT_H
|
||||
#define PRINT_H
|
||||
|
||||
#include "baselayer.h"
|
||||
|
||||
#define EOL "\r\n"
|
||||
#define BUFLEN 64
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @brief Něco jako ostream.
|
||||
*
|
||||
*/
|
||||
|
||||
/// Základy pro zobrazení čísla.
|
||||
enum PrintBases {
|
||||
BIN=1, OCT=3, DEC=10, HEX=4
|
||||
};
|
||||
|
||||
/**
|
||||
* @class Print
|
||||
* @brief Třída pro výpisy do Down().
|
||||
*
|
||||
*
|
||||
* V main pak přibude jen definice instance této třídy
|
||||
* @code
|
||||
static Print print;
|
||||
* @endcode
|
||||
* a ukázka, jak se s tím pracuje:
|
||||
* @snippet main.cpp Main print example
|
||||
* Nic na tom není - operátor << má přetížení pro string, číslo a volbu formátu čísla (enum PrintBases).
|
||||
* Výstup je pak do bufferu a aby nám to "neutíkalo", tedy aby se vypsalo vše,
|
||||
* zavedeme blokování, vycházející z toho, že spodní třída vrátí jen počet bytů,
|
||||
* které skutečně odeslala. Při čekání spí, takže nepoužívat v přerušení.
|
||||
* @snippet src/print.cpp Block example
|
||||
* Toto blokování pak není použito ve vrchních třídách stacku,
|
||||
* blokovaná metoda je BlockDown(). Pokud bychom použili přímo Down(), blokování by pak
|
||||
* používaly všechny vrstvy nad tím. A protože mohou Down() používat v přerušení, byl by problém.
|
||||
*
|
||||
* Metody pro výpisy jsou sice dost zjednodušené, ale zase to nezabere
|
||||
* moc místa - pro ladění se to použít dá. Délka vypisovaného stringu není omezena
|
||||
* délkou použitého buferu.
|
||||
*
|
||||
*/
|
||||
|
||||
class Print : public BaseLayer {
|
||||
public:
|
||||
/// Konstruktor @param b Default decimální výpisy.
|
||||
Print (PrintBases b = DEC);
|
||||
/// Blokování výstupu
|
||||
/// @param buf Ukazatel na data
|
||||
/// @param len Délka přenášených dat
|
||||
/// @return Počet přenesených bytů (rovno len)
|
||||
uint32_t BlockDown (const char * buf, uint32_t len);
|
||||
/// Výstup řetězce bytů
|
||||
/// @param str Ukazatel na řetězec
|
||||
/// @return Odkaz na tuto třídu kvůli řetězení.
|
||||
Print & operator << (const char * str);
|
||||
/// Výstup celého čísla podle base
|
||||
/// @param num Číslo
|
||||
/// @return Odkaz na tuto třídu kvůli řetězení.
|
||||
Print & operator << (const int num);
|
||||
/// Změna základu pro výstup čísla
|
||||
/// @param num enum PrintBases
|
||||
/// @return Odkaz na tuto třídu kvůli řetězení.
|
||||
Print & operator << (const PrintBases num);
|
||||
void out (const void* p, uint32_t l);
|
||||
private:
|
||||
PrintBases base; //!< Základ pro výstup čísla.
|
||||
char buf[BUFLEN]; //!< Buffer pro výstup čísla.
|
||||
};
|
||||
|
||||
#endif // PRINT_H
|
53
serial/Makefile
Normal file
53
serial/Makefile
Normal file
|
@ -0,0 +1,53 @@
|
|||
# ch32v003
|
||||
TARGET?= ch32v003
|
||||
TOOL ?= gcc
|
||||
|
||||
PRJ = example
|
||||
|
||||
VPATH = . ./$(TARGET) ./common
|
||||
BLD = ./build/
|
||||
DFLAGS = -d
|
||||
LFLAGS = -g
|
||||
LDLIBS =
|
||||
BFLAGS = --strip-unneeded
|
||||
|
||||
CFLAGS = -MMD -Wall -ggdb -fno-exceptions -ffunction-sections -fdata-sections
|
||||
CFLAGS+= -I. -I./common -I./$(TARGET) -I/usr/include/newlib
|
||||
DEL = rm -f
|
||||
|
||||
# zdrojaky
|
||||
OBJS = main.o usartclass.o print.o
|
||||
|
||||
include $(TARGET)/$(TOOL).mk
|
||||
BOBJS = $(addprefix $(BLD),$(OBJS))
|
||||
|
||||
all: $(BLD) $(PRJ).elf
|
||||
# ... atd.
|
||||
-include $(BLD)*.d
|
||||
# linker
|
||||
$(PRJ).elf: $(BOBJS)
|
||||
-@echo [LD $(TOOL),$(TARGET)] $@
|
||||
@$(LD) $(LFLAGS) -o $(PRJ).elf $(BOBJS) $(LDLIBS)
|
||||
-@echo "size:"
|
||||
@$(SIZE) $(PRJ).elf
|
||||
-@echo "listing:"
|
||||
$(DUMP) $(DFLAGS) $(PRJ).elf > $(PRJ).lst
|
||||
-@echo "OK."
|
||||
$(COPY) $(BFLAGS) -O binary $(PRJ).elf $(PRJ).bin
|
||||
# preloz co je potreba
|
||||
$(BLD)%.o: %.c
|
||||
-@echo [CC $(TOOL),$(TARGET)] $@
|
||||
@$(CC) -c $(CFLAGS) $< -o $@
|
||||
$(BLD)%.o: %.cpp
|
||||
-@echo [CX $(TOOL),$(TARGET)] $@
|
||||
@$(CXX) -std=c++17 -fno-rtti -c $(CFLAGS) $< -o $@
|
||||
$(BLD):
|
||||
mkdir $(BLD)
|
||||
sin.c: sin.py
|
||||
./sin.py
|
||||
flash: $(PRJ).elf
|
||||
minichlink -w $(PRJ).bin flash -b
|
||||
# vycisti
|
||||
clean:
|
||||
$(DEL) $(BLD)* *.lst *.bin *.elf *.map sin.c *~
|
||||
.PHONY: all clean
|
1
serial/ch32v003
Symbolic link
1
serial/ch32v003
Symbolic link
|
@ -0,0 +1 @@
|
|||
../ch32v003/
|
1
serial/common
Symbolic link
1
serial/common
Symbolic link
|
@ -0,0 +1 @@
|
|||
../common/
|
21
serial/main.cpp
Normal file
21
serial/main.cpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "gpio.h"
|
||||
#include "usartclass.h"
|
||||
#include "print.h"
|
||||
//////////////////////////////////////
|
||||
/* Usart demo + abstraktní třídy C++
|
||||
* */
|
||||
//////////////////////////////////////
|
||||
static GpioClass led (GPIOD, 4u);
|
||||
static UsartClass serial (9600u);
|
||||
static Print cout (DEC);
|
||||
int main () {
|
||||
int n = 0;
|
||||
cout += serial;
|
||||
for (;;) {
|
||||
cout << "Hello world " << n << EOL;
|
||||
const bool b = (n & 4) == 0;
|
||||
led << b;
|
||||
n += 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
Loading…
Reference in a new issue