Помощь - Поиск - Пользователи - Календарь
Полная версия этой страницы: WinAVR: Как правильно размещать и работать с таблицами данных во Flash (памяти программ) ?
Форум разработчиков электроники ELECTRONIX.ru > Сайт и форум > В помощь начинающему > MCS51, AVR, PIC, STM8, 8bit
MaxiMuz
Доброго времяни суток!
Видимо опять столкнулся с кривизной WinAVR sad.gif
Пример: нужно читать данные из памяти программ, и задействовать из в программе. Чтобы небыло лишних вопросов привожу весь текст программы:
Код
#include <avr/io.h>
#include <avr/interrupt.h>     // задает макросы sei() , cli()
#include <inttypes.h>
#include <avr/pgmspace.h>

uint8_t Cnt1;// фоновый счетчик длительности переключения свдиода
volatile int16_t *p;// указатель

volatile register struct
{
  uint8_t bOne : 1;
  uint8_t bTwo : 1;
  uint8_t bThree : 1;
  uint8_t bFour : 1;
} RF asm ("r17");

#define sbi(p,b) (p |= (1<<b)) //Установить бит

#define DIRB 0b00010001
#define PUPB 0b00000111
#define Led PB0    /* линия светодиода ("1" - вкл. ч/з резистор на общ.) */

//;----------------------------------------------------------------------------------------------------------------------------------------
//Определение констант:


#define Vl_DBKCnt 101
#define Vl_FLCnt 5


const uint8_t a[] PROGMEM={22,15,233,40,69,39,203,2,1};

//=====================================================================
ISR( TIM0_COMPA_vect)
{
    if (!(--Cnt1))
    {
     RF.bOne=0;
     sbi (PINB,Led);// ________ Переключение свдиода !
    }
}

//_______________ПОДПРОГРАММЫ_________________
void init (void)
{
PORTB=PUPB; //иницализация порта B
DDRB=DIRB; // задание направления для порта B
TIMSK0=(1<<OCIE0A); /* установка разр. прер-ия по совпадению т/сч.0 с регистром OCR0A */
OCR0A=234;    //загрузка регистра совпадения OCR0A коэф. деления
TCCR0A= (1<<WGM01);    //установка режима СТС - обнуление Т/С0 при совпадении с регистром OCR0A
TCCR0B=(1<<CS02)|(1<<CS00);    // <---- конфигурация и запуск сч-ка в реж. СТС с предделителем ckl/1024
RF.bOne=0;
Cnt1=100; //задание начальных значений для счетчиков

p=&a; // Установка указателя на начало таблицы a
}

//=============================================================================
int main (void)
{
//_________________________ ИНИЦИАЛИЗАЦИЯ _____________________________
uint16_t temp;

init();
sei ();// Разрешение общего прерывания
while (1)
    {
    if (RF.bOne==0)
        { RF.bOne=1;
            temp=*p;
            Cnt1=(uint8_t)temp;
            ++p;
        }
    }    
}

После использования команды PROGMEM в задании таблицы из программы удалилась секция где массив копировался в ОЗУ, что собственно и требовалось. Но обращение к таблице не поменялось.
Вопрос вызывает только место:
Код
temp=*p;
  a6:    81 91           ld    r24, Z+
  a8:    91 91           ld    r25, Z+
Cnt1=(uint8_t)temp;
  aa:    80 93 62 00     sts    0x0062, r24

Читает неизвестно откуда , только не из программы!
Как прочитать данные из программы?
Сергей Борщ
QUOTE (MaxiMuz @ Dec 7 2011, 19:58) *
Видимо опять столкнулся с кривизной WinAVR sad.gif
Видимо опять поленились прочитать документацию.

QUOTE (MaxiMuz @ Dec 7 2011, 19:58) *
CODE
        temp=*p;

Читает неизвестно откуда , только не из программы!
Читает известно откуда - из ОЗУ, как вы и попросили. Посмотрите в папке doc от вашего WinAVR папочку с документацией на avr-libc. В том же файле, в котором описано использованное вами магическое слово PROGMEM, описано и как работать с данными, размещенными во флеш при помощи этого слова.
Палыч
Цитата(MaxiMuz @ Dec 7 2011, 21:58) *
Как прочитать данные из программы?

Коль поменяли размещение массива а, то нужно изменить и указатель р таким образом, чтобы он указывал не на ОЗУ, а на flash (PROGMEM добавьте).
Сергей Борщ
QUOTE (Палыч @ Dec 7 2011, 20:49) *
(PROGMEM добавьте).
Не поможет. В WinAVR (avr-gcc) пока нет поддержки адресных пространств.
Genadi Zawidowski
Может, поможет:

Код
void uc1601s_put_str_P(const char * str)
{
    char c;

    uc1601s_put_char_begin();
    while((c = pgm_read_byte(str ++)) != '\0')
        uc1601s_put_char(c);
    uc1601s_put_char_end();
}
MaxiMuz
Цитата(Genadi Zawidowski @ Dec 8 2011, 11:21) *
Может, поможет:

Код
void uc1601s_put_str_P(const char * str)
{
    char c;

    uc1601s_put_char_begin();
    while((c = pgm_read_byte(str ++)) != '\0')
        uc1601s_put_char(c);
    uc1601s_put_char_end();
}

Ничего не понимаю !!! Уже пробывал и с pgm_read_byte и с _LPM результата никакого !

Цитата(Палыч @ Dec 7 2011, 21:49) *
Коль поменяли размещение массива а, то нужно изменить и указатель р таким образом, чтобы он указывал не на ОЗУ, а на flash (PROGMEM добавьте).

Куда конкретно добавлять PROGMEM ?

Цитата(Сергей Борщ @ Dec 8 2011, 09:57) *
Не поможет. В WinAVR (avr-gcc) пока нет поддержки адресных пространств.

чето я к такому же выводу прихожу, хотя в дока Data in Program Space четко описано как читать с Flash !
Вы вывод сделали на собственном опыте или по чужому ?
Сергей Борщ
QUOTE (MaxiMuz @ Dec 8 2011, 13:36) *
Ничего не понимаю !!! Уже пробывал и с pgm_read_byte и с _LPM результата никакого !
Показывайте исходник и листинг с pgm_read_byte().
QUOTE (MaxiMuz @ Dec 8 2011, 13:36) *
чето я к такому же выводу прихожу, хотя в дока Data in Program Space четко описано как читать с Flash !
И оно таки работает именно так, как описано в доке.
QUOTE (MaxiMuz @ Dec 8 2011, 13:36) *
Вы вывод сделали на собственном опыте или по чужому ?
По своему, естетсвенно.
Genadi Zawidowski
А немного пофантазируем...
Скорее всего, человек написал
const char PROGMEM * str = "1234";
и надеется на что-то...
ВОТ ТАК ДЕЛАТЬ НЕНАДО. тут Ни одного байта из СЕМИ занятых не попадёт по FLASH.
MaxiMuz
Код
#include <avr/io.h>
#include <avr/interrupt.h>  
#include <inttypes.h>
#include <avr/pgmspace.h>

//=====================================================================
uint8_t Cnt1;// фоновый счетчик длительности переключения свдиода
uint16_t *p;

volatile register struct
{
  uint8_t bOne : 1;
  uint8_t bTwo : 1;
  uint8_t bThree : 1;
  uint8_t bFour : 1;
} RF asm ("r17");

#define sbi(p,b) (p |= (1<<b)) //Установить бит

#define DIRB 0b00010001
#define PUPB 0b00000111

//;-------------------------------------------------------------------------------
//Определение контактов
#define Control1 PB4 /* линия управления VT (1 - откр; 0 - закр ) */
#define Led PB0 /* линия светодиода ("1" - вкл. ч/з резистор на общ.) */

uint8_t a[] PROGMEM={22,15,233,40,69,39,203,2,1};

//=====================================================================
ISR( TIM0_COMPA_vect)
{
if (!(--Cnt1))
{ RF.bOne=0;
   sbi (PINB,Led);// ________ Переключение свдиода !
}
}

//_______________ПОДПРОГРАММЫ_________________
void init (void)
{
PORTB=PUPB; //иницализация порта B
DDRB=DIRB; // задание направления для порта B
TIMSK0=(1<<OCIE0A); /* установка разр. прер-ия по совпадению т/сч.0 с регистром OCR0A */
OCR0A=234; //загрузка регистра совпадения OCR0A коэф. деления
TCCR0A= (1<<WGM01); //установка режима СТС - обнуление Т/С0 при совпадении с регистром OCR0A
TCCR0B=(1<<CS02)|(1<<CS00); // <---- конфигурация и запуск сч-ка в реж. СТС с предделителем ckl/1024
RF.bOne=0;
RF.bTwo=0;
Cnt1=100; //задание начальных значений для счетчиков
p=&(a);
}

//=============================================================================
//=============================================================================
int main (void)
{
init();
sei ();

while (1)
{
if (RF.bOne==0)
  { RF.bOne=1;
   //Cnt1=pgm_read_byte(&(a[p])); // варианте 2
   //Cnt1=__LPM(*p);                   // варианте 3
   Cnt1=pgm_read_byte(p);
   ++p;
  }
}
}

вот текст программы. были опробованы различные варианты и комбинации , результатов это не принесло !
MaxiMuz
_

Сергей Борщ
QUOTE (MaxiMuz @ Dec 9 2011, 14:06) *
вот текст программы.
Здесь все, что касается PROGMEM правильно.
А вот uint8_t Cnt1; должен быть volatile. В вашем же случае компилятор имеет право строку Cnt1=pgm_read_byte(p); выкинуть совсем.

И последний вопрос - файл компилируется как C или как C++?

QUOTE
CODE
volatile register

QUOTE
Следуя вашему FAQ "Как варить яйцо в микроволновке" я стал его варить, но оно взорвалось и сильно испачкало мне аппарат!
Genadi Zawidowski
А это было вообще "шедевр"
Код
   //Cnt1=pgm_read_byte(&(a[p])); // варианте 2

Вы хоть немного читайте диагностику компилятора. А то на asm ("r17"); Вас хватило, а на адресную арифметику нет...

В приведённом тексте ++p будет через пропускать каждый второй элемент массива a.
MaxiMuz
Цитата(Сергей Борщ @ Dec 9 2011, 16:51) *
И последний вопрос - файл компилируется как C или как C++?

Я не знаю как определить как компилируется файл! Есть скопированный makefile, меняю в нем тип МК, имяфайла, оптимизацию , и все.

Про яйцо и микроволновку смешно sm.gif)) Вы хотите сказать что там где не нужно использовать volatile он был использован и наоборот? sm.gif
demiurg_spb
Цитата(MaxiMuz @ Dec 12 2011, 10:09) *
Я не знаю как определить как компилируется файл! Есть скопированный makefile, меняю в нем тип МК, имяфайла, оптимизацию , и все.
Приведите строки вызова компилятора.

Цитата
Вы хотите сказать что там где не нужно использовать volatile он был использован и наоборот? sm.gif
Об этом всё в том же FAQ на avr-libc написано.
MaxiMuz
Цитата(Genadi Zawidowski @ Dec 9 2011, 23:29) *
А это было вообще "шедевр"
Код
   //Cnt1=pgm_read_byte(&(a[p])); // варианте 2

Кстати, это тоже рабочий вариант и ни какой не шедевр! Если правильно задать массив , то работает, но байт информации в массиве пакуется в слово с 00h в ст.байте:
Код
uint8_t *a[] PROGMEM={22,15,233,40,69,39,203,2,1};
00000014 <a>:
  14: 16 00 0f 00 e9 00 28 00 45 00 27 00 cb 00 02 00     ......(.E.'.....
  24: 01 00  
uint16_t p;
p=0;
......
......
int main (void)
{
init();
  9c: ea df           rcall .-44    ; 0x72 <init>
sei ();
  9e: 78 94          sei
  a0: e0 91 60 00  lds r30, 0x0060
  a4: f0 91 61 00  lds r31, 0x0061
  a8: ee 0f           add r30, r30
  aa: ff 1f            adc r31, r31
  ac: ec 5e           subi r30, 0xEC; 236
  ae: ff 4f            sbci r31, 0xFF; 255
if (RF.bOne==0)
  b0: 10 fd           sbrc r17, 0
  b2: fe cf            rjmp .-4     ; 0xb0 <__stack+0x11>
  { RF.bOne=1;
  b4: 11 60           ori r17, 0x01; 1
   Cnt1=pgm_read_byte(&(a[p++]));
  b6: 84 91           lpm r24, Z+
  b8: 80 93 62 00   sts 0x0062, r24
  bc: 32 96           adiw r30, 0x02; 2
  be: f8 cf            rjmp .-16    ; 0xb0 <__stack+0x11>


Здесь я обнаружил неточную трансляцию команд, в месте :
Код
b6: 84 91           lpm r24, Z+

на самом деле заменяется командой lpm r24,Z !!! Что меня в начале смутило , так как инкремент судя по листингу происходит 3 раза.

Цитата(demiurg_spb @ Dec 12 2011, 10:56) *
Приведите строки вызова компилятора.

Код
> "make.exe" all

-------- begin --------
avr-gcc (WinAVR 20100110) 4.3.3
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.


Size before:
AVR Memory Usage
----------------
Device: attiny13a

Program:     196 bytes (19.1% Full)
(.text + .data + .bootloader)

Data:          3 bytes (4.7% Full)
(.data + .bss + .noinit)




Compiling C: table_Fsh.c
avr-gcc -c -mmcu=attiny13a -I. -gdwarf-2 -DF_CPU=8000000UL -Os -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums -Wall -Wstrict-prototypes -Wa,-adhlns=./table_Fsh.lst  -std=gnu99 -MMD -MP -MF .dep/table_Fsh.o.d table_Fsh.c -o table_Fsh.o
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast
table_Fsh.c:56: warning: initialization makes pointer from integer without a cast

Linking: table_Fsh.elf
avr-gcc -mmcu=attiny13a -I. -gdwarf-2 -DF_CPU=8000000UL -Os -funsigned-char -funsigned-bitfields -fpack-struct -fshort-enums -Wall -Wstrict-prototypes -Wa,-adhlns=table_Fsh.o  -std=gnu99 -MMD -MP -MF .dep/table_Fsh.elf.d table_Fsh.o --output table_Fsh.elf -Wl,-Map=table_Fsh.map,--cref,-gc-sections     -lm

Creating load file for Flash: table_Fsh.hex
avr-objcopy -O ihex -R .eeprom -R .fuse -R .lock -R .signature table_Fsh.elf table_Fsh.hex

Creating load file for EEPROM: table_Fsh.eep
avr-objcopy -j .eeprom --set-section-flags=.eeprom="alloc,load" \
--change-section-lma .eeprom=0 --no-change-warnings -O ihex table_Fsh.elf table_Fsh.eep || exit 0

Creating Extended Listing: table_Fsh.lss
avr-objdump -h -S -z table_Fsh.elf > table_Fsh.lss

Creating Symbol Table: table_Fsh.sym
avr-nm -n table_Fsh.elf > table_Fsh.sym

Size after:
AVR Memory Usage
----------------
Device: attiny13a

Program:     196 bytes (19.1% Full)
(.text + .data + .bootloader)

Data:          3 bytes (4.7% Full)
(.data + .bss + .noinit)



-------- end --------


> Process Exit Code: 0
> Time Taken: 00:04





MaxiMuz
Вообщем я понял, из за неприспосбленного и непродуманного компилятора WinAVR и кривизны оптимизатора код с заранее настроенным на адрес регистром:
Код
int16_t *p;
p=&(a);
Cnt1=pgm_read_byte(p++);
- почемуто не работает , увеличение p происходит на 2 байта вместо одного!
Единственный правильный вариант:
Код
volatile uint8_t Cnt1;//фоновый счетчик длительности переключения свдиода
uint16_t p=0;

uint8_t a[] PROGMEM={22,15,233,40,69,39,203,2,1};
......
......
Cnt1=pgm_read_byte(&(a[p++]));

Хотя тоже корявый:
Код
            Cnt1=pgm_read_byte(&(a[p++]));
  aa:    84 91           lpm    r24, Z+
  ac:    80 93 62 00     sts    0x0062, r24
  b0:    31 96           adiw    r30, 0x01; 1

Сергей Борщ
QUOTE (MaxiMuz @ Dec 12 2011, 12:20) *
почемуто не работает , увеличение p происходит на 2 байта вместо одного!
А с чего бы ему увеличиваться на один, если вы его объявили как указатель на uint16_t. Он честно, как вы и просите, увеличивается на sizeof(uint16_t). Пассаж про зеркалокомпилятор опустим.
MaxiMuz
Цитата(Сергей Борщ @ Dec 12 2011, 13:53) *
А с чего бы ему увеличиваться на один, если вы его объявили как указатель на uint16_t. Он честно, как вы и просите, увеличивается на sizeof(uint16_t). Пассаж про зеркалокомпилятор опустим.

Да, действительно! Я все провожу паралели с Ассемблером и посчитал что тип_данных перед указателем есть размер ячейки памяти которая используется для указания адреса.
Genadi Zawidowski
Кст
Цитата
ати, это тоже рабочий вариант и ни какой не шедевр! Если правильно задать массив , то работает, но байт информации в массиве пакуется в слово с 00h в ст.байте:

Вы описали массив указателей на int8_t, инициализируя их целыми числами. Компилятор ругнулся по поводу инициализации десяток раз (или сколько у Вас там инициализаторов), а Вы и не заметили...

Причём, в первых двух выкладываниях текста программы было всё нормально а в последнем (там где дизассемблерный листинг с кусочками исходного текста) эта ошибка уже внесена. Естественно, при перескоке через байт всё заработало.

MaxiMuz
Цитата(Genadi Zawidowski @ Dec 13 2011, 01:05) *
Вы описали массив указателей на int8_t, инициализируя их целыми числами. Компилятор ругнулся по поводу инициализации десяток раз (или сколько у Вас там инициализаторов), а Вы и не заметили...

Теперь понял какую я ерунду написал.
Почему то в строчке p=&a;
Код
volatile uint8_t Cnt1;//фоновый счетчик длительности переключения свдиода
uint8_t *p;
......
uint8_t a[] PROGMEM={22,15,233,40,69,39,203,2,1};
p=&a;
......
компил. выдает предупреждение:
Код
warning: assignment from incompatible pointer type
?
Сергей Борщ
QUOTE (MaxiMuz @ Dec 13 2011, 13:05) *
warning: assignment from incompatible pointer type[/code] ?
p - указатель на uint8_t. Имя массива является указателем на его нулевой член. Оператор & возвращает указательна свой аргумент. Т.е. запись &a имеет тип "указатель на указатель на uint8_t". Вам же надо просто "указатель на uint8_t". Поэтому либо берите адрес нулевого элемента массива p = &a[0], либо берите адрес массива p = a.

И еще - ваш массив находится во флеш, значит является константным по условию. Значит ваш указатель нужно делать указателем на константу: uint8_t const * p. И неважно, что компилятор сейчас позволяет объявить указатель на неконстантный объект - во первых, это уже исправлено в последних версиях, а во-вторых - это предохранит вас от ошибочной попытки изменить данные, на которые указывает p простым присваиванием, компилятор выдаст сообщение об ошибке.
MaxiMuz
Заменил p=&a; на p=&a[0]; действительно предупреждение пропало, спасибо Сергею!
Но возник другой вопрос. Я немного поменял алгоритм основной программы на:
Код
p=&a[0];
........................................
........................................
int main (void)
{
uint8_t temp;
init();
sei ();
while (1)
    {
    if (RF.bOne==0)
        { RF.bOne=1;
            temp=pgm_read_byte(p++);
            if (temp!=0)
                {Cnt1=pgm_read_byte(p++);}
        }
    }    
}

И получил код:
Код
p=&a[0];
  8a:    84 e1           ldi    r24, 0x14   ; 20
  8c:    90 e0           ldi    r25, 0x00   ; 0
  8e:    90 93 61 00     sts    0x0061, r25
  92:    80 93 60 00     sts    0x0060, r24
........................................
........................................
uint8_t temp;
init();
  98:    e8 df           rcall    .-48        ; 0x6a <init>
sei ();
  9a:    78 94           sei
  9c:    20 91 60 00     lds    r18, 0x0060
  a0:    30 91 61 00     lds    r19, 0x0061
  a4:    c9 01           movw    r24, r18
while (1)
    {
    if (RF.bOne==0)
  a6:    10 fd           sbrc    r17, 0
  a8:    fe cf           rjmp    .-4         ; 0xa6 <__stack+0x7>
        { RF.bOne=1;
  aa:    11 60           ori    r17, 0x01   ; 1
}
int main (void)
{
  ac:    ac 01           movw    r20, r24
  ae:    4f 5f           subi    r20, 0xFF   ; 255
  b0:    5f 4f           sbci    r21, 0xFF   ; 255
    {
    if (RF.bOne==0)
        { RF.bOne=1;
            temp=pgm_read_byte(p++);
  b2:    fc 01           movw    r30, r24
  b4:    84 91           lpm    r24, Z+
            if (temp!=0)
  b6:    88 23           and    r24, r24
  b8:    11 f4           brne    .+4         ; 0xbe <__stack+0x1f>
  ba:    ca 01           movw    r24, r20
  bc:    f4 cf           rjmp    .-24        ; 0xa6 <__stack+0x7>
            {
            Cnt1=pgm_read_byte(p++);
  be:    9a 01           movw    r18, r20
  c0:    2f 5f           subi    r18, 0xFF   ; 255
  c2:    3f 4f           sbci    r19, 0xFF   ; 255
  c4:    fa 01           movw    r30, r20
  c6:    84 91           lpm    r24, Z+
  c8:    80 93 62 00     sts    0x0062, r24
  cc:    eb cf           rjmp    .-42        ; 0xa4 <__stack+0x5>

Долго ломал голову зачем по несколько раз копировалась ссылка на массив! Создалось такое впечатление что, происходит дублирование пары регистров с адресом массива...
Непомерно раздутый код , который можно заменить как минимум вдвое меньшим числом команд. Я понимаю что возможно ктото скажет, что если не нравиться, пользуйся асемблерными вставками. Программа работает правильно. Может я слишком требователен к компилятору, но может есть менее громоздкий способ обращению к массиву ?
AHTOXA
bb-offtopic.gif И кто будет после этого утверждать, что изучать ассемблер полезно? Вот изучил человек ассемблер, и что? Теперь он вместо написания программы, реализации своего алгоритма, создания своего устройства - занят анализом работы компилятора... laughing.gif
MaxiMuz, вы сначала просто программу напишите, а оптимизировать её можно потом. Если возникнет нужда (а она скорее всего и не возникнет). Кроме того, со временем вы набьёте руку в Си, и будете автоматом писать наиболее оптимальным способом.
MaxiMuz
Проблема решилась выносом p++ за "скобки" функции pgm_read_byte:
Код
while (1)
    {
    if (RF.bOne==0)
        { RF.bOne=1;
            temp=pgm_read_byte(p);
            p++;
            if (temp!=0)
            {
            Cnt1=pgm_read_byte(p);
            p++;
            }
        }
    }
ARV
какая проблема решилась? по-моему, что в скобках, что за скобками - все одно и то же...
demiurg_spb
Цитата(ARV @ Jan 21 2012, 10:34) *
по-моему, что в скобках, что за скобками - все одно и то же...
Однозначно!
AHTOXA
Цитата(ARV @ Jan 21 2012, 13:34) *
какая проблема решилась? по-моему, что в скобках, что за скобками - все одно и то же...

Что-то там не так оптимизировалось:
Цитата(MaxiMuz @ Dec 14 2011, 22:14) *
Долго ломал голову зачем по несколько раз копировалась ссылка на массив!

MaxiMuz
Цитата(ARV @ Jan 21 2012, 10:34) *
какая проблема решилась? по-моему, что в скобках, что за скобками - все одно и то же...

решилась проблема лишних сохранений и тусований ссылочного регистра Z.
при использовании:
Цитата
pgm_read_byte(p); p++;
вместо
Цитата
pgm_read_byte(p++);
код сокращается на 18 байт!
Для просмотра полной версии этой страницы, пожалуйста, пройдите по ссылке.
Invision Power Board © 2001-2025 Invision Power Services, Inc.