Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
Enc28j60Eth.cpp
00001 /* 00002 Enc28J60Network.cpp 00003 UIPEthernet network driver for Microchip ENC28J60 Ethernet Interface. 00004 00005 Copyright (c) 2013 Norbert Truchsess <norbert.truchsess@t-online.de> 00006 All rights reserved. 00007 00008 based on enc28j60.c file from the AVRlib library by Pascal Stang. 00009 For AVRlib See http://www.procyonengineering.com/ 00010 00011 Modified (ported to mbed) by Zoltan Hudak <hudakz@inbox.com> 00012 00013 This program is free software: you can redistribute it and/or modify 00014 it under the terms of the GNU General Public License as published by 00015 the Free Software Foundation, either version 3 of the License, or 00016 (at your option) any later version. 00017 00018 This program is distributed in the hope that it will be useful, 00019 but WITHOUT ANY WARRANTY; without even the implied warranty of 00020 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00021 GNU General Public License for more details. 00022 00023 You should have received a copy of the GNU General Public License 00024 along with this program. If not, see <http://www.gnu.org/licenses/>. 00025 */ 00026 #include "Enc28j60Eth.h" 00027 #include "mbed.h" 00028 #include "mbed_version.h" 00029 00030 extern "C" 00031 { 00032 #include "enc28j60.h" 00033 #include "uip.h" 00034 } 00035 00036 // Static member initialization 00037 uint16_t Enc28j60Eth::nextPacketPtr; 00038 uint8_t Enc28j60Eth::bank = 0xff; 00039 struct memblock Enc28j60Eth::receivePkt; 00040 00041 /** 00042 * @brief 00043 * @note 00044 * @param 00045 * @retval 00046 */ 00047 Enc28j60Eth::Enc28j60Eth(PinName mosi, PinName miso, PinName sclk, PinName cs) : 00048 MemPool(), 00049 _spi(mosi, miso, sclk), 00050 _cs(cs) 00051 { } 00052 00053 /** 00054 * @brief 00055 * @note 00056 * @param 00057 * @retval 00058 */ 00059 void Enc28j60Eth::init(uint8_t* macaddr) 00060 { 00061 MemPool::init(); // 1 byte in between RX_STOP_INIT and pool to allow prepending of controlbyte 00062 00063 // initialize SPI interface 00064 _cs = 1; 00065 _spi.format(8, 0); // 8-bit, mode 0 00066 _spi.frequency(10000000); // 10 Mbit/s 00067 #if MBED_MAJOR_VERSION == 2 00068 wait_ms(100); 00069 #else 00070 wait_us(100000); 00071 #endif 00072 00073 // perform system reset 00074 writeOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET); 00075 00076 // check CLKRDY bit to see if reset is complete 00077 // while(!(readReg(ESTAT) & ESTAT_CLKRDY)); 00078 // The CLKRDY does not work. See Rev. B4 Silicon Errata point. 00079 // Just wait. 00080 #if MBED_MAJOR_VERSION == 2 00081 wait_ms(50); 00082 #else 00083 wait_us(50000); 00084 #endif 00085 00086 // do bank 0 stuff 00087 // initialize receive buffer 00088 // 16-bit transfers, must write low byte first 00089 // set receive buffer start address 00090 nextPacketPtr = RXSTART_INIT; 00091 00092 // Rx start 00093 writeRegPair(ERXSTL, RXSTART_INIT); 00094 00095 // set receive pointer address 00096 writeRegPair(ERXRDPTL, RXSTART_INIT); 00097 00098 // RX end 00099 writeRegPair(ERXNDL, RXEND_INIT); 00100 00101 //All memory which is not used by the receive buffer is considered the transmission buffer. 00102 // No explicit action is required to initialize the transmission buffer. 00103 // TX start 00104 //writeRegPair(ETXSTL, TXSTART_INIT); 00105 // TX end 00106 //writeRegPair(ETXNDL, TXEND_INIT); 00107 // However, he host controller should leave at least seven bytes between each 00108 // packet and the beginning of the receive buffer. 00109 00110 // do bank 1 stuff, packet filter: 00111 // For broadcast packets we allow only ARP packtets 00112 // All other packets should be unicast only for our mac (MAADR) 00113 // 00114 // The pattern to match is therefore 00115 // Type ETH.DST 00116 // ARP BROADCAST 00117 // 06 08 -- ff ff ff ff ff ff -> ip checksum for theses bytes=f7f9 00118 // in binary these poitions are:11 0000 0011 1111 00119 // This is hex 303F->EPMM0=0x3f,EPMM1=0x30 00120 //TODO define specific pattern to receive dhcp-broadcast packages instead of setting ERFCON_BCEN! 00121 writeReg(ERXFCON, ERXFCON_UCEN | ERXFCON_CRCEN | ERXFCON_PMEN | ERXFCON_BCEN); 00122 writeRegPair(EPMM0, 0x303f); 00123 writeRegPair(EPMCSL, 0xf7f9); 00124 00125 // 00126 // 00127 // do bank 2 stuff, 00128 // enable MAC receive 00129 // and bring MAC out of reset (writes 0x00 to MACON2) 00130 writeRegPair(MACON1, MACON1_MARXEN | MACON1_TXPAUS | MACON1_RXPAUS); 00131 00132 // enable automatic padding to 60bytes and CRC operations 00133 writeOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0 | MACON3_TXCRCEN | MACON3_FRMLNEN); 00134 00135 // set inter-frame gap (non-back-to-back) 00136 writeRegPair(MAIPGL, 0x0C12); 00137 00138 // set inter-frame gap (back-to-back) 00139 writeReg(MABBIPG, 0x12); 00140 00141 // Set the maximum packet size which the controller will accept 00142 // Do not send packets longer than MAX_FRAMELEN: 00143 writeRegPair(MAMXFLL, MAX_FRAMELEN); 00144 00145 // do bank 3 stuff 00146 // write MAC address 00147 // NOTE: MAC address in ENC28J60 is byte-backward 00148 writeReg(MAADR5, macaddr[0]); 00149 writeReg(MAADR4, macaddr[1]); 00150 writeReg(MAADR3, macaddr[2]); 00151 writeReg(MAADR2, macaddr[3]); 00152 writeReg(MAADR1, macaddr[4]); 00153 writeReg(MAADR0, macaddr[5]); 00154 00155 // no loopback of transmitted frames 00156 phyWrite(PHCON2, PHCON2_HDLDIS); 00157 00158 // switch to bank 0 00159 setBank(ECON1); 00160 00161 // enable interrutps 00162 writeOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE | EIE_PKTIE); 00163 00164 // enable packet reception 00165 writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN); 00166 00167 //Configure leds 00168 phyWrite(PHLCON, 0x476); 00169 } 00170 00171 /** 00172 * @brief 00173 * @note 00174 * @param 00175 * @retval 00176 */ 00177 memhandle Enc28j60Eth::receivePacket() 00178 { 00179 uint8_t rxstat; 00180 uint16_t len; 00181 // check if a packet has been received and buffered 00182 //if( !(readReg(EIR) & EIR_PKTIF) ){ 00183 // The above does not work. See Rev. B4 Silicon Errata point 6. 00184 if (readReg(EPKTCNT) != 0) { 00185 uint16_t readPtr = nextPacketPtr + 00186 6 > RXEND_INIT ? nextPacketPtr + 00187 6 - 00188 RXEND_INIT + 00189 RXSTART_INIT : nextPacketPtr + 00190 6; 00191 // Set the read pointer to the start of the received packet 00192 writeRegPair(ERDPTL, nextPacketPtr); 00193 00194 // read the next packet pointer 00195 nextPacketPtr = readOp(ENC28J60_READ_BUF_MEM, 0); 00196 nextPacketPtr |= readOp(ENC28J60_READ_BUF_MEM, 0) << 8; 00197 00198 // read the packet length (see datasheet page 43) 00199 len = readOp(ENC28J60_READ_BUF_MEM, 0); 00200 len |= readOp(ENC28J60_READ_BUF_MEM, 0) << 8; 00201 len -= 4; //remove the CRC count 00202 // read the receive status (see datasheet page 43) 00203 rxstat = readOp(ENC28J60_READ_BUF_MEM, 0); 00204 00205 //rxstat |= readOp(ENC28J60_READ_BUF_MEM, 0) << 8; 00206 #ifdef ENC28J60DEBUG 00207 printf 00208 ( 00209 "receivePacket [%d-%d], next: %d, stat: %d, count: %d -> ", 00210 readPtr, 00211 (readPtr + len) % (RXEND_INIT + 1), 00212 nextPacketPtr, 00213 rxstat, 00214 readReg(EPKTCNT) 00215 ); 00216 (rxstat & 0x80) != 0 ? printf("OK") : printf("failed"); 00217 printf("\r\n"); 00218 #endif 00219 // decrement the packet counter indicate we are done with this packet 00220 00221 writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC); 00222 00223 // check CRC and symbol errors (see datasheet page 44, table 7-3): 00224 // The ERXFCON.CRCEN is set by default. Normally we should not 00225 // need to check this. 00226 if ((rxstat & 0x80) != 0) { 00227 receivePkt.begin = readPtr; 00228 receivePkt.size = len; 00229 return UIP_RECEIVEBUFFERHANDLE; 00230 } 00231 00232 // Move the RX read pointer to the start of the next received packet 00233 // This frees the memory we just read out 00234 setERXRDPT(); 00235 } 00236 00237 return(NOBLOCK); 00238 } 00239 00240 /** 00241 * @brief 00242 * @note 00243 * @param 00244 * @retval 00245 */ 00246 void Enc28j60Eth::setERXRDPT() 00247 { 00248 writeRegPair(ERXRDPTL, nextPacketPtr == RXSTART_INIT ? RXEND_INIT : nextPacketPtr - 1); 00249 } 00250 00251 /** 00252 * @brief 00253 * @note 00254 * @param 00255 * @retval 00256 */ 00257 size_t Enc28j60Eth::blockSize(memhandle handle) 00258 { 00259 return handle == NOBLOCK ? 0 : handle == UIP_RECEIVEBUFFERHANDLE ? receivePkt.size : blocks[handle].size; 00260 } 00261 00262 /** 00263 * @brief 00264 * @note 00265 * @param 00266 * @retval 00267 */ 00268 void Enc28j60Eth::sendPacket(memhandle handle) 00269 { 00270 memblock* packet = &blocks[handle]; 00271 uint16_t start = packet->begin - 1; 00272 uint16_t end = start + packet->size; 00273 00274 // backup data at control-byte position 00275 uint8_t data = readByte(start); 00276 // write control-byte (if not 0 anyway) 00277 if (data) 00278 writeByte(start, 0); 00279 00280 #ifdef ENC28J60DEBUG 00281 printf("sendPacket(%d) [%d-%d]: ", handle, start, end); 00282 for (uint16_t i = start; i <= end; i++) { 00283 printf("%d ", readByte(i)); 00284 } 00285 00286 printf("\r\n"); 00287 #endif 00288 // TX start 00289 00290 writeRegPair(ETXSTL, start); 00291 00292 // Set the TXND pointer to correspond to the packet size given 00293 writeRegPair(ETXNDL, end); 00294 00295 // send the contents of the transmit buffer onto the network 00296 writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS); 00297 00298 // Reset the transmit logic problem. See Rev. B4 Silicon Errata point 12. 00299 if ((readReg(EIR) & EIR_TXERIF)) { 00300 writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS); 00301 } 00302 00303 //restore data on control-byte position 00304 if (data) 00305 writeByte(start, data); 00306 } 00307 00308 /** 00309 * @brief 00310 * @note 00311 * @param 00312 * @retval 00313 */ 00314 uint16_t Enc28j60Eth::setReadPtr(memhandle handle, memaddress position, uint16_t len) 00315 { 00316 memblock* packet = handle == UIP_RECEIVEBUFFERHANDLE ? &receivePkt : &blocks[handle]; 00317 memaddress start = handle == UIP_RECEIVEBUFFERHANDLE && 00318 packet->begin + 00319 position > RXEND_INIT ? packet->begin + 00320 position - 00321 RXEND_INIT + 00322 RXSTART_INIT : packet->begin + 00323 position; 00324 00325 writeRegPair(ERDPTL, start); 00326 00327 if (len > packet->size - position) 00328 len = packet->size - position; 00329 return len; 00330 } 00331 00332 /** 00333 * @brief 00334 * @note 00335 * @param 00336 * @retval 00337 */ 00338 uint16_t Enc28j60Eth::readPacket(memhandle handle, memaddress position, uint8_t* buffer, uint16_t len) 00339 { 00340 len = setReadPtr(handle, position, len); 00341 readBuffer(len, buffer); 00342 return len; 00343 } 00344 00345 /** 00346 * @brief 00347 * @note 00348 * @param 00349 * @retval 00350 */ 00351 uint16_t Enc28j60Eth::writePacket(memhandle handle, memaddress position, uint8_t* buffer, uint16_t len) 00352 { 00353 memblock* packet = &blocks[handle]; 00354 uint16_t start = packet->begin + position; 00355 00356 writeRegPair(EWRPTL, start); 00357 00358 if (len > packet->size - position) 00359 len = packet->size - position; 00360 writeBuffer(len, buffer); 00361 return len; 00362 } 00363 00364 /** 00365 * @brief 00366 * @note 00367 * @param 00368 * @retval 00369 */ 00370 uint8_t Enc28j60Eth::readByte(uint16_t addr) 00371 { 00372 uint8_t result; 00373 00374 writeRegPair(ERDPTL, addr); 00375 00376 _cs = 0; 00377 00378 // issue read command 00379 _spi.write(ENC28J60_READ_BUF_MEM); 00380 00381 // read data 00382 result = _spi.write(0x00); 00383 00384 _cs = 1; 00385 00386 return(result); 00387 } 00388 00389 /** 00390 * @brief 00391 * @note 00392 * @param 00393 * @retval 00394 */ 00395 void Enc28j60Eth::writeByte(uint16_t addr, uint8_t data) 00396 { 00397 writeRegPair(EWRPTL, addr); 00398 00399 _cs = 0; 00400 00401 // issue write command 00402 _spi.write(ENC28J60_WRITE_BUF_MEM); 00403 00404 // write data 00405 _spi.write(data); 00406 00407 _cs = 1; 00408 } 00409 00410 /** 00411 * @brief 00412 * @note 00413 * @param 00414 * @retval 00415 */ 00416 void Enc28j60Eth::copyPacket 00417 ( 00418 memhandle dest_pkt, 00419 memaddress dest_pos, 00420 memhandle src_pkt, 00421 memaddress src_pos, 00422 uint16_t len 00423 ) 00424 { 00425 memblock* dest = &blocks[dest_pkt]; 00426 memblock* src = src_pkt == UIP_RECEIVEBUFFERHANDLE ? &receivePkt : &blocks[src_pkt]; 00427 memaddress start = src_pkt == UIP_RECEIVEBUFFERHANDLE && 00428 src->begin + 00429 src_pos > RXEND_INIT ? src->begin + 00430 src_pos - 00431 RXEND_INIT + 00432 RXSTART_INIT : src->begin + 00433 src_pos; 00434 enc28j60_mempool_block_move_callback(dest->begin + dest_pos, start, len); 00435 00436 // Move the RX read pointer to the start of the next received packet 00437 // This frees the memory we just read out 00438 //setERXRDPT(); 00439 } 00440 00441 /** 00442 * @brief 00443 * @note 00444 * @param 00445 * @retval 00446 */ 00447 void Enc28j60Eth::freePacket() 00448 { 00449 setERXRDPT(); 00450 } 00451 00452 /** 00453 * @brief 00454 * @note 00455 * @param 00456 * @retval 00457 */ 00458 uint8_t Enc28j60Eth::readOp(uint8_t op, uint8_t address) 00459 { 00460 uint8_t result; 00461 00462 _cs = 0; 00463 00464 // issue read command 00465 _spi.write(op | (address & ADDR_MASK)); 00466 00467 // read data 00468 result = _spi.write(0x00); 00469 00470 // do dummy read if needed (for mac and mii, see datasheet page 29) 00471 if (address & 0x80) 00472 result = _spi.write(0x00); 00473 00474 _cs = 1; 00475 return(result); 00476 } 00477 00478 /** 00479 * @brief 00480 * @note 00481 * @param 00482 * @retval 00483 */ 00484 void Enc28j60Eth::writeOp(uint8_t op, uint8_t address, uint8_t data) 00485 { 00486 _cs = 0; 00487 00488 // issue write command 00489 _spi.write(op | (address & ADDR_MASK)); 00490 00491 // write data 00492 _spi.write(data); 00493 00494 _cs = 1; 00495 } 00496 00497 /** 00498 * @brief 00499 * @note 00500 * @param 00501 * @retval 00502 */ 00503 void Enc28j60Eth::readBuffer(uint16_t len, uint8_t* data) 00504 { 00505 _cs = 0; 00506 00507 // issue read command 00508 _spi.write(ENC28J60_READ_BUF_MEM); 00509 00510 // read data 00511 while (len) { 00512 len--; 00513 *data = _spi.write(0x00); 00514 data++; 00515 } 00516 00517 *data = '\0'; 00518 00519 _cs = 1; 00520 } 00521 00522 /** 00523 * @brief 00524 * @note 00525 * @param 00526 * @retval 00527 */ 00528 void Enc28j60Eth::writeBuffer(uint16_t len, uint8_t* data) 00529 { 00530 _cs = 0; 00531 00532 // issue write command 00533 _spi.write(ENC28J60_WRITE_BUF_MEM); 00534 00535 // write data 00536 while (len) { 00537 len--; 00538 _spi.write(*data); 00539 data++; 00540 } 00541 00542 _cs = 1; 00543 } 00544 00545 /** 00546 * @brief 00547 * @note 00548 * @param 00549 * @retval 00550 */ 00551 void Enc28j60Eth::setBank(uint8_t address) 00552 { 00553 // set the bank (if needed) 00554 if ((address & BANK_MASK) != bank) { 00555 // set the bank 00556 writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1 | ECON1_BSEL0)); 00557 writeOp(ENC28J60_BIT_FIELD_SET, ECON1, (address & BANK_MASK) >> 5); 00558 bank = (address & BANK_MASK); 00559 } 00560 } 00561 00562 /** 00563 * @brief 00564 * @note 00565 * @param 00566 * @retval 00567 */ 00568 uint8_t Enc28j60Eth::readReg(uint8_t address) 00569 { 00570 // set the bank 00571 setBank(address); 00572 00573 // do the read 00574 return readOp(ENC28J60_READ_CTRL_REG, address); 00575 } 00576 00577 /** 00578 * @brief 00579 * @note 00580 * @param 00581 * @retval 00582 */ 00583 void Enc28j60Eth::writeReg(uint8_t address, uint8_t data) 00584 { 00585 // set the bank 00586 setBank(address); 00587 00588 // do the write 00589 writeOp(ENC28J60_WRITE_CTRL_REG, address, data); 00590 } 00591 00592 /** 00593 * @brief 00594 * @note 00595 * @param 00596 * @retval 00597 */ 00598 void Enc28j60Eth::writeRegPair(uint8_t address, uint16_t data) 00599 { 00600 // set the bank 00601 setBank(address); 00602 00603 // do the write 00604 writeOp(ENC28J60_WRITE_CTRL_REG, address, (data & 0xFF)); 00605 writeOp(ENC28J60_WRITE_CTRL_REG, address + 1, (data) >> 8); 00606 } 00607 00608 /** 00609 * @brief 00610 * @note 00611 * @param 00612 * @retval 00613 */ 00614 void Enc28j60Eth::phyWrite(uint8_t address, uint16_t data) 00615 { 00616 // set the PHY register address 00617 writeReg(MIREGADR, address); 00618 00619 // write the PHY data 00620 writeRegPair(MIWRL, data); 00621 00622 // wait until the PHY write completes 00623 while (readReg(MISTAT) & MISTAT_BUSY) { 00624 wait_us(15); 00625 } 00626 } 00627 00628 /** 00629 * @brief 00630 * @note 00631 * @param 00632 * @retval 00633 */ 00634 uint16_t Enc28j60Eth::phyRead(uint8_t address) 00635 { 00636 writeReg(MIREGADR, address); 00637 writeReg(MICMD, MICMD_MIIRD); 00638 00639 // wait until the PHY read completes 00640 while (readReg(MISTAT) & MISTAT_BUSY) { 00641 wait_us(15); 00642 } //and MIRDH 00643 00644 writeReg(MICMD, 0); 00645 return(readReg(MIRDL) | readReg(MIRDH) << 8); 00646 } 00647 00648 /** 00649 * @brief 00650 * @note 00651 * @param 00652 * @retval 00653 */ 00654 void Enc28j60Eth::clkout(uint8_t clk) 00655 { 00656 //setup clkout: 2 is 12.5MHz: 00657 writeReg(ECOCON, clk & 0x7); 00658 } 00659 00660 // read the revision of the chip: 00661 uint8_t Enc28j60Eth::getrev() 00662 { 00663 return(readReg(EREVID)); 00664 } 00665 00666 /** 00667 * @brief 00668 * @note 00669 * @param 00670 * @retval 00671 */ 00672 uint16_t Enc28j60Eth::chksum(uint16_t sum, memhandle handle, memaddress pos, uint16_t len) 00673 { 00674 uint8_t spdr; 00675 uint16_t t; 00676 uint16_t i; 00677 00678 len = setReadPtr(handle, pos, len) - 1; 00679 _cs = 0; 00680 00681 // issue read command 00682 spdr = _spi.write(ENC28J60_READ_BUF_MEM); 00683 for (i = 0; i < len; i += 2) { 00684 // read data 00685 spdr = _spi.write(0x00); 00686 t = spdr << 8; 00687 spdr = _spi.write(0x00); 00688 t += spdr; 00689 sum += t; 00690 if (sum < t) { 00691 sum++; /* carry */ 00692 } 00693 } 00694 00695 if (i == len) { 00696 spdr = _spi.write(0x00); 00697 t = (spdr << 8) + 0; 00698 sum += t; 00699 if (sum < t) { 00700 sum++; /* carry */ 00701 } 00702 } 00703 00704 _cs = 1; 00705 00706 /* Return sum in host byte order. */ 00707 return sum; 00708 } 00709 00710 /** 00711 * @brief 00712 * @note 00713 * @param 00714 * @retval 00715 */ 00716 void Enc28j60Eth::powerOff() 00717 { 00718 writeOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_RXEN); 00719 #if MBED_MAJOR_VERSION == 2 00720 wait_ms(50); 00721 #else 00722 wait_us(50000); 00723 #endif 00724 writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_VRPS); 00725 #if MBED_MAJOR_VERSION == 2 00726 wait_ms(50); 00727 #else 00728 wait_us(50000); 00729 #endif 00730 writeOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PWRSV); 00731 } 00732 00733 /** 00734 * @brief 00735 * @note 00736 * @param 00737 * @retval 00738 */ 00739 void Enc28j60Eth::powerOn() 00740 { 00741 writeOp(ENC28J60_BIT_FIELD_CLR, ECON2, ECON2_PWRSV); 00742 #if MBED_MAJOR_VERSION == 2 00743 wait_ms(50); 00744 #else 00745 wait_us(50000); 00746 #endif 00747 writeOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN); 00748 #if MBED_MAJOR_VERSION == 2 00749 wait_ms(50); 00750 #else 00751 wait_us(50000); 00752 #endif 00753 } 00754 00755 /** 00756 * @brief 00757 * @note 00758 * @param 00759 * @retval 00760 */ 00761 bool Enc28j60Eth::linkStatus() 00762 { 00763 return(phyRead(PHSTAT2) & 0x0400) > 0; 00764 }
Generated on Sun Feb 26 2023 10:14:23 by
1.7.2