26 Нояб., 2020, 06:31

В семейной ссоре побеждает тот, у кого рога больше.


Вольтамперметр, 7 сегментные индикаторы, SPI

Автор zenon, 29 Сен., 2020, 22:18

« предыдущая - следующая »

Slabovik

О, да. Почитал аппнот, посмотрел видео - результат приятен. Однако шум подмешивать однозначно надо. На рисунке, что я выше кинул, наверное плохой способ подмешивания шума (была мысль о подмешивании псевдослучайной последовательности 0 и 1, которую можно легко получить на сдвиговом регистре даже чисто программно).

В аппноте же, насколько я понял (они это не уточняют, говорят только про PWM с заполнением 50%), используют треугольный сигнал для подмешивания в опору. Это имеет смысл т.к. всё становится ещё проще. Думаю, стоит сделать закладку на подмешивание такого сигнала (всего несколько деталей и нога порта), а подмешивать или нет - это уже можно решить в программе.

Сейчас смотрю твой последний код и плохо понимаю, как ты синхронизируешь считывание АЦП с пересчётом результатов. АЦП работает по FreeRunning режиму, но пересчёт результатов и вывод на индикаторы разве сами по себе? (повторю, я плох в Си и может просто не вижу - пожалуйста, ткните меня в нужное место)

Я бы попробовал такой вариант алгоритма.
Основа синхронизации - прерывания по таймеру. Пусть те же 1000 Гц (250 Гц мерцания на индикаторе - 4 цифры)

1. вывод показаний на индикатор (очередная цифра)

2. запуск однократного преобразования АЦП - (режим Sleep с ожиданием прерывания. В прерывании вообще не нужно ничего делать - оно лишь служит сигналом к продолжению работы программы). Последовательно запускаем так для каждого из каналов.

3. Пересчёт сумм в кольцевом буфере (если буфер накопительный, то пересчёт нужно сделать только 1 раз за n циклов, т.е. когда значения накопятся)

4. Подготовка показаний для индикатора (тоже 1 из 4 раз - только когда на индикатор выведена последняя цифра, если буфер кольцевой, или 1 раз за n циклов, если буфер накопительный)

5. сваливание в Sleep с ожиданием прерывания от таймера.

По времени я прикинул - должно укладываться со значительным запасом.


Что меня смущает... использование аппаратного вывода на индикатор безусловно круто, но... блин, есть две причины, чтобы он мне не нравился. Первая - всё-равно программа "тупо ждёт", когда из буфера выйдет всё, чтобы вывалить туда следующий байт, вторая - всё-равно вручную дёргается PB2 (SS). Ну, программа короче, да...
ps. а вот ещё. У процессоров в 32-выводных QFP корпусах есть два "лишних" входа ADC 6 и ADC7, выведенные на собственные ноги и питающиеся исключительно от AVcc. Возможно, что стоит задействовать? Выводы PC0~PC5 при этом совершенно свободны, собственно, как и PD0~PD7 (UART, я понимаю, исключительно для отладки)
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

В последнем моем варианте никакой синхронизации нет, я тут ещё сам для себя все эти моменты только открываю, опоздал лет так на надцать. :)
Во Free runnig режиме, насколько я понял прерывание работает по окончанию преобразования, далее с результатом я ничего не делал, чистый его вывод с задержкой. Надо сглаживать, накопитель и среднее.
Тот код который с оверсэмплингом не мой, я просто склеил куски и запустил для посмотреть, опять-таки выхлоп чистый, не сглаженный, потому как вот этот кусок кода для меня не понятен от слова совсем.
Отсюда:
// -------------------------------------------------------------
    // smoothing: IIR Filter
    //
    #define IIR_SHIFT 2
    V = ((V << IIR_SHIFT) - V + adcValue(Settings::ChannelVoltage, AdcReadLinearized)) >> IIR_SHIFT;
    C = ((C << IIR_SHIFT) - C + adcValue(Settings::ChannelCurrent, AdcReadLinearized)) >> IIR_SHIFT;

    // -------------------------------------------------------------
    // smoothing: exponential moving average with window
    //
    // weighting factor W = 0 .. 1
    // current average = (current sensor value * W) + (last average * (1 - W))
    // avg = val * 0.1 + avg * (1 - 0.1);
    //
    #define EXP_SHIFT   5
    #define EXP_WEIGHT 15
    #define EXP_SCALE   4
    #define EXP_WINDOW (2 ^ EXP_SHIFT)
    #define EXP_RC     (2 ^ EXP_SHIFT / 2) // Rounding correction: add 0,5 == 2 ^ SHIFT / 2

    smoothV = (V << (EXP_SHIFT - EXP_SCALE)) + ((smoothV * EXP_WEIGHT) >> EXP_SCALE);
    smoothC = (C << (EXP_SHIFT - EXP_SCALE)) + ((smoothC * EXP_WEIGHT) >> EXP_SCALE);

    uint32_t sv = (smoothV + EXP_RC) >> EXP_SHIFT;
    if (abs(sv - V) < EXP_WINDOW) {
      V = sv;
    }

    uint32_t sc = (smoothC + EXP_RC) >> EXP_SHIFT;
    if (abs(sc - C) < EXP_WINDOW) {
      C = sc;
    }
Вот код, на котором работает оверсэмплинг, каналы ADC0 и ADC1, выдернул оттуда же, забор показаний сделал из переменной adcOversampledValue. Там в коде есть ещё компенсации, с ними не разобрался.
Всё сырое!
#include <stdint.h>
#include <avr/io.h>
#include <avr/pgmspace.h>

#define adcChannels    2 // read ADC0 and ADC1
typedef enum AdcValueType_e {
  AdcReadLinearized = (1<<0),
  AdcReadRaw        = (1<<1)
} AdcValueType_t;

typedef struct adcCalibration_s {
  uint32_t hi;
  uint32_t lo;
} AdcCalibration_t;

// calibration points at runtime
extern AdcCalibration_t adcCalibration[adcChannels];

// measured AVCC at startup in mV
extern uint16_t adcAVcc;

extern void     adcInit(void);
extern void     adcLoadCalibrationData(void);
extern void     adcSaveCalibrationData(void);
extern void     adcCalculateReferenceCompensationFactor(void);
extern uint32_t adcValue(uint8_t channel, AdcValueType_t valuetype);

#define adcSelectChannel(channel) \
  (ADMUX = (ADMUX & (~((1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0)))) | (channel))

#define adcEnableInternalVref()       (ADMUX  |=  ((1<<REFS1) | (1<<REFS0))) // 1.1V internal Bandgap Reference
#define adcEnableExternalVref()       (ADMUX  &= ~((1<<REFS1) | (1<<REFS0))) // VRef at Pin AREF
#define adcEnableVccVref()            (ADMUX  &= ~(1<<REFS1), ADMUX |= (1<<REFS0)) // AVCC with external capacitor at AREF pin

#define adcStartConversion()          (ADCSRA |=  (1<<ADSC))
#define adcIsConversionFinished()     ((ADCSRA &  (1<<ADIF)) ? TRUE : FALSE)
#define adcIsConversionRunning()      ((ADCSRA | ~(1<<ADIF)) ? TRUE : FALSE)
#define adcWaitForConversion()        while (ADCSRA & (1<<ADSC))

#define adcEnable()                   (ADCSRA |=  (1<<ADEN))
#define adcDisable()                  (ADCSRA &= ~(1<<ADEN))

#define adcEnableInt()                (ADCSRA |=  (1<<ADIE))
#define adcDisableInt()               (ADCSRA &= ~(1<<ADIE))

#define adcClearFlag()                (ADCSRA &=  (1<<ADIF))
//
//
//
//
#include <avr/interrupt.h>
#include <avr/eeprom.h>
#include <util/delay.h>

// -----------------------------------------------------------------------------

#define adcOversampleCount        128
#define adcOversampleShift          4
#define adcCalibrationMarkHi    30000UL  // 1/10mV, upper calibration point
#define adcCalibrationMarkLo     3000UL  // 1/10mV, lower calibration point

#define adcScale                   16UL  // 2^4 fixed point scaling (shift by this value)
#define adcRoundingCorrection   32768UL  // add 0.5, which is: (2 ^ adcScale / 2)

#define adcRangeScaled  ((adcCalibrationMarkHi - adcCalibrationMarkLo) << adcScale)
#define adcOffsetScaled (adcCalibrationMarkLo << adcScale)

#define adcFactor(ch)       (adcRangeScaled / (adcCalibration[ch].hi - adcCalibration[ch].lo))
#define adcCorrection(ch)   (adcOffsetScaled - (adcCalibration[ch].lo * adcFactor(ch)))

// -----------------------------------------------------------------------------
// default values (Data Precision 8200 +-10ppm at ATMega Vcc = 5.0V)
// NB: these will vary per ATMega device
//
AdcCalibration_t eeAdcCalibration[adcChannels] EEMEM = {
  {
    4908UL, // 3.000V calibration point
     459UL  // 0.300V calibration point
  },
  {
    4908UL, // 3.000V calibration point
     459UL  // 0.300V calibration point
  }
};

AdcCalibration_t adcCalibration[adcChannels];

// -----------------------------------------------------------------------------

uint16_t adcAVcc;
uint8_t adcChannel;



// Места знаков и точек
uint8_t const DigiMass [] PROGMEM = {
    0b01111111, //  0  // 1-й знак на первом
    0b10111111, //  1  // 2-й знак на первом
    0b11011111, //  2  // 3-й знак на первом
    0b11101111, //  3  // 4-й знак на первом
    0b11110111, //  4  // 1-й знак на втором
    0b11111011, //  5  // 2-й знак на втором
    0b11111101, //  6  // 3-й знак на втором
    0b11111110, //  7  // 4-й знак на втором

    0b11011111, //  8  // место точки на 1-ом
    0b11111101  //  9  // место точки на 2-ом
};
// ABCEDFGH
uint8_t const SegmMass [] PROGMEM = {
    0b00111111, // 0
    0b00000110,
    0b01011011,
    0b01001111,
    0b01100110,
    0b01101101,
    0b01111101,
    0b00000111,
    0b01111111,
    0b01101111,
    0b10000000,
    0b00000000
};

uint8_t Counters[10] = { 0,  0,  0,  0,  // 1-й дисплей 4-знака
                         0,  0,  0,  0,  // 2-й дисплей 4-знака
                        10, 10 };        // точки
unsigned char n_count=0;
typedef enum {false, true} bool;
volatile bool trueValue = false;

void bin_bcd(uint16_t a);
struct {
    uint8_t tens,hundreds,thousands;
    uint16_t units;
}bcd;
//struct { uint8_t tens,hundreds,thousands; uint16_t units; }bcd;


volatile uint32_t adcOversampledValue[adcChannels];



//zz 
#define F_CPU 16000000UL // 16 MHz
#define UBRR 57600L
#define UBRR_div (F_CPU/(16UL*UBRR)-1)
#define HI(x) ((x)>>8)
#define LO(x) ((x)& 0xFF)

void UART_putc(unsigned char data);
void UART_puts(char* s);
void UART_puthex8(uint8_t val);
void UART_puthex16(uint16_t val);
void UART_putU8(uint8_t val);
void UART_putS8(int8_t val);
void UART_putU16(uint16_t val);
void UART_putS16(int16_t val);



uint32_t V0 = 0;
uint32_t V1 = 0;

uint32_t smoothV0 = 0;
uint32_t smoothV1 = 0;
int main (void) {
   
    // инициализвция UART =================================================
    UBRR0H = HI(UBRR_div );
    UBRR0L = LO(UBRR_div );
    UCSR0B = (1 << RXEN0 );             // Бит RXEN0 (4) регистра UCSR0B - разрешение приема если установлен в 1.
    UCSR0B = (1 << TXEN0 );             // Бит TXEN0 (3) регистра UCSR0B - разрешение передачи если установлен в 1.
    UCSR0C = (1 << USBS0 )|             // Бит USBS0 (3) регистра UCSR0C устанавливает количество стоп битов (1 стоп-бит если сброшен в 0 / 2 стоп-бита если установлен в 1).
             (1 << UCSZ00)|(1<<UCSZ01); // биты UCSZ01 и UCSZ00 (2, 1) регистра UCSR0C - устанавливают длину передаваемых посылок 011 - 8 бит
   

// инициализация портов сдвигового регистра  SPI  ======================
    DDRB  |=  (1<<PB5);      // линия тактирования   clock
    DDRB  |=  (1<<PB3);      // линия данных         data
    DDRB  |=  (1<<PB2);      // линия стробирования  latch                   // или краткая запись  DDRB  |=  ((1<<PB2)|(1<<PB3)|(1<<PB5)); //ножки SPI на выход
    PORTB &= ~(1<<PB2);      // подать низ на .. линия стробирования  latch
    PORTB &= ~(1<<PB3);      // подать низ на .. линия данных         data
    PORTB &= ~(1<<PB5);      // подать низ на .. линия тактирования   clock  // или краткая запись  PORTB &= ~((1<<PB2)|(1<<PB3)|(1<<PB5)); // низкий уровень
    SPCR  =   ((1<<SPE)|(1<<MSTR)); // включим шину, объявим ведущим
// ====================================================================
// настройка прерываний по таймеру 1000 Гц =============================
    TCCR1A  = 0; TCCR1B = 0;                       // установить регистры в 0
    OCR1A   = 250 - 1;                             // установка регистра совпадения  250 - 1;
    TCCR1B |= (1 << WGM12);                        // включить CTC режим
    TCCR1B |= (1 << CS11); TCCR1B |= (1 << CS10);  // включить делитель /64
    TIMSK1 |= (1 << OCIE1A);                       // включить прерывание по совпадению таймера
   
// ====================================================================


  adcInit();
  sei();
   
  while(1) {
      //V = ((V << IIR_SHIFT) - V + adcValue(1, AdcReadLinearized)) >> IIR_SHIFT;
      //V = adcValue(1, AdcReadLinearized);
      V0 = adcOversampledValue[0];
      V1 = adcOversampledValue[1];
     
      bin_bcd(V0); // закидываем прочитанное значение АЦП в функцию bin_bcd, результат отправляется в структуру bcd
      Counters[7] = bcd.thousands;
      Counters[6] = bcd.hundreds;
      Counters[5] = bcd.tens;
      Counters[4] = bcd.units;
     
      bin_bcd(V1);

      Counters[3] = bcd.thousands;
      Counters[2] = bcd.hundreds;
      Counters[1] = bcd.tens;
      Counters[0] = bcd.units;
     
     
      UART_puts("ADC1 :  "); UART_putU16(V0); UART_puts(" ");
      UART_puts("ADC2 :  "); UART_putU16(V1);
      UART_puts("\n");
      _delay_ms(50);
   
  }
 return 0;
}

// Прерывание по таймеру 1000Hz для 7 seg =============================
ISR(TIMER1_COMPA_vect) {
    SPDR = pgm_read_byte(&DigiMass[n_count]);           while(!(SPSR & (1<<SPIF)));
    SPDR = pgm_read_byte(&SegmMass[Counters[n_count]]); while(!(SPSR & (1<<SPIF)));
    PORTB |= (1<<PB2); asm ("nop"); PORTB &= ~(1<<PB2);
    n_count++;
    if (n_count>9) n_count=0;
}


// Раскладываем 9999 на цифры
void bin_bcd(uint16_t a) {
    bcd.tens=0;
    bcd.hundreds=0;
    bcd.thousands=0;
    bcd.units=a;
    if(bcd.units>=1000) {
      while (bcd.units>=1000) {
        bcd.units-=1000;
        bcd.thousands++;
      }
    }
    if(bcd.units>=100) {
      while (bcd.units>=100) {
        bcd.units-=100;
        bcd.hundreds++;
      }
    }
    if(bcd.units>=10) {
      while (bcd.units>=10) {
        bcd.units-=10;
        bcd.tens++;
      }
    }
}


// -----------------------------------------------------------------------------
//
ISR(ADC_vect)  {
  static uint32_t adcOversampleSum[adcChannels];
  static uint8_t adcOversampleCounter[adcChannels];
  adcOversampleSum[adcChannel] += ADC;

  if (++adcOversampleCounter[adcChannel] > adcOversampleCount - 1) {
    adcOversampledValue[adcChannel] = adcOversampleSum[adcChannel] >> adcOversampleShift;
    adcOversampleSum[adcChannel] = 0;
    adcOversampleCounter[adcChannel] = 0;
  }
  // next channel
  adcChannel = (adcChannel + 1) % adcChannels;
  adcSelectChannel(adcChannel);
  // no dummy read after switch results is the necessary noise for oversampling
}

// -----------------------------------------------------------------------------
// if we were to wait long engough after power on
// for the voltage to stabilize we can measure
// the internal 1.1V reference against Vcc == Vref
// and calculate a compensation factor 'adcAVcc'.
//
void adcCalculateReferenceCompensationFactor() {
  cli();
  adcEnableExternalVref();
  adcSelectChannel(0xe);
  adcEnable();
  _delay_ms(500);
  // dump intial dummy conversion
  adcStartConversion();
  adcWaitForConversion();
  adcAVcc = ADC; // ADC must be read, says datasheet
  adcAVcc = 0;   // TODO: check if the compiler swallows something here
  int i = 0;
  for (; i < 5; i++) {
    adcStartConversion();
    adcWaitForConversion();
    adcAVcc += ADC;
  }
  sei();
  adcAVcc = 1126400L / (adcAVcc / 5); // mV compared to Vref
}

// -----------------------------------------------------------------------------
void adcInit(void) {
  adcChannel = 0;

  adcLoadCalibrationData();
  adcEnableExternalVref();
  adcSelectChannel(adcChannel); 
 
  // ADCSRA |= (1<<ADPS2) | (1<<ADPS1); // prescale clk/64 = 125kHz@8Mhz
  ADCSRA |= (1<<ADPS2) | (1<<ADPS1) | (1<<ADPS0); // prescale clk/128 = 62.5kHz@8Mhz
  DIDR0  |= (1<<ADC1D) | (1<<ADC0D); // disable digital inputs on ADC pins
  adcEnable();
  adcEnableInt();
  ADCSRA |= (1<<ADATE); // auto trigger enable
  ADCSRB = 0x00;        // free running mode
  adcStartConversion();
}
// -----------------------------------------------------------------------------
uint32_t adcValue(uint8_t channel, AdcValueType_t valuetype) {
  cli();
  uint32_t v = adcOversampledValue[channel];
  sei();
  if (valuetype == AdcReadLinearized) {
    v = (v * adcFactor(channel) + adcCorrection(channel) + adcRoundingCorrection) >> adcScale;
  }
  return v;
}
// -----------------------------------------------------------------------------
// result in m°C: 21500 / 1000 = 21.5°C
//
uint16_t adcInternalTemperature() {
  uint16_t result = 0;
  cli();
  adcEnableInternalVref();
  adcSelectChannel(8); // thermometer
  _delay_ms(5);
  // dummy conversion
  adcStartConversion();
  adcWaitForConversion();
  result = ADC;
  adcStartConversion();
  adcWaitForConversion();
  result = ADC;
  sei();
  return (result - 125) * 1075;
}
// -----------------------------------------------------------------------------
void adcLoadCalibrationData(void) {
  do {} while (!(eeprom_is_ready()));
  eeprom_read_block(&adcCalibration, &eeAdcCalibration, sizeof(adcCalibration));
}

// -----------------------------------------------------------------------------
void adcSaveCalibrationData(void) {
  do {} while (!(eeprom_is_ready()));
  eeprom_write_block(&adcCalibration, &eeAdcCalibration, sizeof(adcCalibration));
}
// -----------------------------------------------------------------------------




///zzzz
// UART ===============================================================
// ====================================================================
// Все преобразования надёргал из http://www.rjhcoding.com/avrc-uart.php
void UART_putc (unsigned char data) {
        while (!( UCSR0A & (1<<UDRE0)));  // Wait for empty transmit buffer
        UDR0 = data;                      // Put data into buffer, sends the data
}
// ====================================================================
void UART_puts(char* s) {
    while(*s > 0) UART_putc(*s++);        // transmit character until NULL is reached
}
// ====================================================================
void UART_puthex8(uint8_t val) {
    // extract upper and lower nibbles from input value
    uint8_t upperNibble = (val & 0xF0) >> 4;
    uint8_t lowerNibble =  val & 0x0F;
    // convert nibble to its ASCII hex equivalent
    upperNibble += upperNibble > 9 ? 'A' - 10 : '0';
    lowerNibble += lowerNibble > 9 ? 'A' - 10 : '0';
    // print the characters
    UART_putc(upperNibble);
    UART_putc(lowerNibble);
}
// ====================================================================
void UART_puthex16(uint16_t val) {
    UART_puthex8((uint8_t)(val >> 8));        // transmit upper 8 bits
    UART_puthex8((uint8_t)(val & 0x00FF));    // transmit lower 8 bits
}
// ====================================================================
void UART_putU8(uint8_t val) {                // Transmitting Decimal Values
    uint8_t dig1 = '0', dig2 = '0';
    // count value in 100s place
    while(val >= 100) {
        val -= 100;
        dig1++;
    }
    // count value in 10s place
    while(val >= 10) {
        val -= 10;
        dig2++;
    }
    // print first digit (or ignore leading zeros)
    if(dig1 != '0') UART_putc(dig1);
    // print second digit (or ignore leading zeros)
    if((dig1 != '0') || (dig2 != '0')) UART_putc(dig2);
    // print final digit
    UART_putc(val + '0');
}
// ====================================================================
void UART_putS8(int8_t val) {
    // check for negative number
    if(val & 0x80) {
        // print negative sign
        UART_putc('-');
        // get unsigned magnitude
        val = ~(val - 1);
    }
    // print magnitude
    UART_putU8((uint8_t)val);
}
// ====================================================================
void UART_putU16(uint16_t val) {
    uint8_t dig1 = '0', dig2 = '0', dig3 = '0', dig4 = '0';
    // count value in 10000s place
    while(val >= 10000) {
        val -= 10000;
        dig1++;
    }
    // count value in 1000s place
    while(val >= 1000) {
        val -= 1000;
        dig2++;
    }
    // count value in 100s place
    while(val >= 100) {
        val -= 100;
        dig3++;
    }
    // count value in 10s place
    while(val >= 10) {
        val -= 10;
        dig4++;
    }
    // was previous value printed?
    uint8_t prevPrinted = 0;
    // print first digit (or ignore leading zeros)
    if(dig1 != '0') {UART_putc(dig1); prevPrinted = 1;}
    // print second digit (or ignore leading zeros)
    if(prevPrinted || (dig2 != '0')) {UART_putc(dig2); prevPrinted = 1;}
    // print third digit (or ignore leading zeros)
    if(prevPrinted || (dig3 != '0')) {UART_putc(dig3); prevPrinted = 1;}
    // print third digit (or ignore leading zeros)
    if(prevPrinted || (dig4 != '0')) {UART_putc(dig4); prevPrinted = 1;}
    // print final digit
    UART_putc(val + '0');
}
// ====================================================================
void UART_putS16(int16_t val) {
    // check for negative number
    if(val & 0x8000) {
        // print minus sign
        UART_putc('-');
        // convert to unsigned magnitude
        val = ~(val - 1);
    }
    // print unsigned magnitude
    UART_putU16((uint16_t) val);
}
// end UART functions
// ====================================================================

zenon

UART пока только для отладки.
У меня есть китайский генератор (FYT3200S) могу попробовать подмешать с него что-нибудь на опору, какое напряжение и сигнал? Подключать также - 33 кОм + 0,1 мкФ?
Не знал про ADC6/7 в QFP, да, если от AVCC они питаются думаю результат лучше будет.

Цитата: Slabovik от 07 Окт., 2020, 11:40Первая - всё-равно программа "тупо ждёт", когда из буфера выйдет всё, чтобы вывалить туда следующий байт, вторая - всё-равно вручную дёргается PB2 (SS). Ну, программа короче, да...
Разговор про while(!(SPSR & (1<<SPIF)));? Из этого куска:
ISR(TIMER1_COMPA_vect) {
    SPDR = pgm_read_byte(&DigiMass[n_count]);           while(!(SPSR & (1<<SPIF)));
    SPDR = pgm_read_byte(&SegmMass[Counters[n_count]]); while(!(SPSR & (1<<SPIF)));
    PORTB |= (1<<PB2); asm ("nop"); PORTB &= ~(1<<PB2);
    n_count++;
    if (n_count>9) n_count=0;
}
Не знаю, делать софтовую реализацию SPI? Мне кажется под вопросом.

Slabovik

Да, этот "while" и ожидает появления бита "передача окончена". В принципе, можно и прерывание организовать по окончании передачи, но... будет ли это колхоз проще.
Вот и получается, что этот SPI не совсем аппаратный, а "полу". Байт на биты аппаратно раскладывает, но более не умеет.

Подмес шума наверное надо организовать по схеме из аппнота. В этом случае с генератора достаточно подать меандр. Амплитуда помехи на ARef должна быть где-то 2,5 вольта опоры / 1024 отсчёта = 2,5 мВ, можно немного больше. Помеха не должна быть высокочастотной, чтобы ARef за время преобразования (~15 циклов тактовой АЦП - она ниже тактовой ядра на коэффициент деления) ARef не сместился куда-то далеко. Полагаю, что-нибудь в районе килогерца будет нормально. Если смотреть осциллографом с закрытым входом, там на конденсаторе сигнал должен быть близким к треугольнику (при меандре на входе всей системы).

Код оверсемлинга я тоже что-то сходу не пойму, что они там делают. Надо на бумажке попробовать его "пошагать". Я могу рассказать, как работает мой код, но у меня асм   :-[  хотя вычислительные приёмы - они языконезависимы.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Мой вариант опроса (без оверсэмплинга) ADC такой:
ISR(ADC_vect) {
    if (trueValue) {
      if (adc_channel==0) { adc_buffer[0] = ADCL | (ADCH << 8); ADMUX = 1; } //ADMUX = (0b00<<REFS0)|1;    // ADC1 pin 24
      if (adc_channel==1) { adc_buffer[1] = ADCL | (ADCH << 8); ADMUX = 3; } //ADMUX = (0b00<<REFS0)|3;    // ADC3 pin 26
      ++adc_channel;
      if (adc_channel>1) { adc_channel = 0; }
      trueValue = false; // Устанавливаем флаг смены входного пина
    }
    else { trueValue = true; } // Первый раз пропускаем считывание и устанавливаем флаг на чтение в следующий раз
}
Тут не обязательно было делать adc_buffer[0] = ADCL | (ADCH << 8); ADMUX = 1; можно проще adc_buffer[0] = ADC;.
Можно твой кусок на ассемблере попробовать сюда включить, заодно посмотрел бы как вставка asm(); работает

Slabovik

Страшно мне смотреть на выкрутасы с ADMUX. Знаю, что для данного применения нормально, один фиг там управляющие биты по нулям и от нулей отличаются только сами MUX, но... правильно было бы читать ADMUX, изменять MUX (сле-едующий! :) ) и пихать его обратно. Ну или применять здесь дефайны настройки АЦП. При этом завязать номер MUX на переменную-указатель adc_channel (хотя, вынимая номер из MUX всегда можно знать, какой АЦП сейчас был включен.

В реальности же это работоспособности не вредит, только "причёсывает" стиль написания.

А есть .lss этого кусочка? Хочется посмотреть, как оно в машинные команды оттранслировалось...
В asm у меня процедура чтения длинная, но я там все 8 каналов щёлкаю, раскладывая по кольцевым буферам, вряд ли для вставки в Си оно пригодно, разве что только сам кусочек, где непосредственно опрос идёт, но я его приводил ранее.

зы: что я ни делаю, у меня только танк и получается. Напильники наверное не той системы  :-\
Наваял-10-07.png
Две 595 обязательные, третью в общем-то вполне могут заменить либо свободные разряды PC, либо PD полностью отдать. Но третья 595 мне нравится тем, что с ней индикатор меняет своё состояние абсолютно синхронно (это конечно не критично в данном изделии, просто греет ЧСВ). Можно повесить ещё пару светиков. Зачем - не знаю, но имеющиеся два нахожу приятными. Их можно расположить рядом с индикаторами и гореть ими, отображая CC/CV или ещё что-нибудь. Программно, конечно.

А, да... Разъёмчик AntuBug справа - это для возможности нарастить длину индикатора. Иногда полезно в отладочных целях плюнуть туда что-нибудь для проверки.

зызы: "выпихивание" 8 байтов в индикатор чисто программным способом у меня занимает порядка 70 микросекунд при тактовой 14,3 МГц.
AsmCode_Burst_8_bytes.png
 Сможешь посмотреть, как быстро выплёлывает аппаратный SPI? Только для этого SS нужно опускать прямо перед процедурой, а поднимать после (а то ты его сразу же опускаешь после поднятия) - тогда осциллографом легко определить, сколько времени это всё занимает.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

07 Окт., 2020, 20:56 #31 Последнее редактирование: 08 Окт., 2020, 02:19 от zenon
Насмешил про танк! Думал 8 транзисторов в имеющуюся схему внедрить, а у тебя вон она как, может всё-таки 8 ключей?
Не знаю получится ли найти осциллографом что-то.
Это надо сократить код как можно сильнее и сделать плевки с паузой так?
Про lss, у меня Makefile каст сокращенный, но внедрил, сейчас при сборке генерирует lss... Только как и какой кусочек нужен/выдрать?
У меня ж среда - Geany, если что. :)

Цитата: Slabovik от 07 Окт., 2020, 19:02В asm у меня процедура чтения длинная, но я там все 8 каналов щёлкаю, раскладывая по кольцевым буферам, вряд ли для вставки в Си оно пригодно, разве что только сам кусочек, где непосредственно опрос идёт, но я его приводил ранее.
Ну так и нужен только опрос.
Хотя не думаю, что сильный профит будет.
ы. Что-то редко я осциллографом пользуюсь... Встал на data.

Slabovik

Я вот про этот кусочек кода
ISR(TIMER1_COMPA_vect) {
    SPDR = pgm_read_byte(&DigiMass[n_count]);           while(!(SPSR & (1<<SPIF)));
    SPDR = pgm_read_byte(&SegmMass[Counters[n_count]]); while(!(SPSR & (1<<SPIF)));
    PORTB |= (1<<PB2); asm ("nop"); PORTB &= ~(1<<PB2);
    n_count++;
    if (n_count>9) n_count=0;
}
Предлагаю его сделать таким
ISR(TIMER1_COMPA_vect) {
    PORTB &= ~(1<<PB2);                                 // опускаем строб RDY
    SPDR = pgm_read_byte(&DigiMass[n_count]);           // засылаем первый байт
    while(!(SPSR & (1<<SPIF)));                         // ожидаем освобождение буфера
    SPDR = pgm_read_byte(&SegmMass[Counters[n_count]]); // засылаем второй байт
    while(!(SPSR & (1<<SPIF)));                         // снова ждём, когда байт уйдёт из буфера
    PORTB |= (1<<PB2);                                  // поднимаем строб RDY, защёлкивая данные на выход 595
    n_count++;
    if (n_count>9) n_count=0;
}
По опусканию строба PB2 (RDY, Latch и т.п.) можно чисто визуально наблюдать при помощи осциллографа, что происходит именно эта процедура и, соответственно, измерить, сколько времени она занимает да и вообще как выглядит. Вторым лучом можно Clock смотреть или Data.

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

Не, код вот такой как есть - он самый короткий. Тут просто чисто удивление, что ресурс вроде аппаратный (т.е. работать умеет полностью параллельно), но программа крутит пустой цикл и ждёт, пока "железо" отработает. Второй минус этого решения - привязка к конкретным ножкам. Впрочем, UART тоже можно в этом же режиме использовать (для засылки данных в 595), но суть остаётся та же.

А покажи просто, чего там вообще в .lss. Он выглядит как листинг с кодами? .lss ассемблерного файла выглядит так

Вид-на-lss.png

Сишный должен быть похожим по структуре. Конечно, метки и имена будут нечеловеческие, но кое-что можно подсмотреть.

Восемь транзисторов - это если один регистр с данными делать на оба индикатора. При этом скважность 1:8 получается, соответственно, яркость падает. Думаю, 1:4 всё-таки лучше, а значит, четыре ключа.
Регулировку яркости, кстати, легко ввести. Входы OE нужно от земли оторвать, объединить и подать на них PWM с частотой, кратной частоте смены знака. При таком построении регулироваться будет всё, и светодиоды в том числе.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Выше отредактировал.
adc_oversamling.zip Всё вместе.
atmrea328p_spi_7seg_only_01.zip lss от кода который только шлёт по SPI.

Slabovik

07 Окт., 2020, 22:24 #34 Последнее редактирование: 07 Окт., 2020, 22:40 от Slabovik
Во, отличная инфа для размышления. Завтра помедитирую :)

зы: не совсем понятно, это один бит даты или два... лучше Clock в таком ракусре глянуть, ну, или чуть переделать процедуру и посмотреть RAY (он же Latch, он же PB2)
зызы: фронты малёха звенят. От звона хорошо помогают резисторы небольшого сопротивления (22~220 Ом), включенные недалеко от выходов (на моей схеме наверное видел). На крутизну они особо не влияют, но со звоном (суть - паразитные колебания, как в искровом передатчике) борются хорошо.

:-\ гляжу на STM32 и... полная непонятка а) что у них в качестве основной среды. Многие кивают на IAR, но... там лицензия на раб. место 3к$. Есть небольшой зоопарк из свободного ПО, но не ясно, что с отладкой и т.д. б) полная ж - у них шаг выводов 0.5мм. Если 0.8 на ATмеге ещё терпимо и можно даже на самодельную плату (ЛУТ без маски) их лепить, то что делать с 0.5 - вопрос больной... в) есть всякие платки, но...  вот глядел сегодня на витрины. Нашёл такие, относительно недорого. С чем их едят?
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

07 Окт., 2020, 22:50 #35 Последнее редактирование: 08 Окт., 2020, 14:18 от zenon
А зачем такую плату, не, надо народную BluePill, зоопарк с IDE у них это прям беда и сплошные холивары, пытаюсь Makefile прикрутить, руки не доходят.
В своё время взял по 5 штук BluePiill и BlackPill, ну и десяток таких же камней для них.
Вот собирал Hldi, там GL850 (USB хаб) ещё торчит, не до конца собрал, потому что нет ещё транзисторов на IRF24N15 (мотор на 42в), но уже прошил и проверил работу энкодера, лазера.
Индуктивности - то что было. :)
ы. От звона в разрез линий PB2, PB3, PB5 резисторы 22-220 Ом, ближе к МК, так?
ыы. uart_04_atmega328p_adc_spi.zip - без оверсэмплинга, с твоим вариантом SPI, все файлы.
ыыы. 0,5 мм фоторезистом с принтером у меня нормально получалось, тут основные претензии к шаблону, лазерный принтер, который тонер не жалеет и пары ацетона (или аэрозоль) + сноровка, если нет ламинатора то лучше жидкий фоторезист, если есть Ordyl A340, с нашим НПВЩ - или как его - одно мучение...
4ы. Добавил осциллограмку двумя щупами.

Slabovik

08 Окт., 2020, 15:51 #36 Последнее редактирование: 09 Окт., 2020, 11:28 от Slabovik
Проведя немного времени за чтением документации, я правильно понимаю, что если я желаю выбраться по таймеру из режима Idle то нужно использовать второй таймер, т.к. системный клок останавливается, а второй таймер может работать от своего собственного клока (настроить надо). Т.е. нулевой и первый таймеры в режиме (команда SLEEP) останавливаются вместе с ядром?

Если это так, то получается, что для введения шума в ARef, также годится только второй таймер, а единственный его выход приходится на PD3, потому как PB3 желает занять аппаратный MOSI (Data)
скрытый текст
поэтому я ностальгирую по Z80, у которого только ядро, а нужное довешивать надо, но зато оно (довешенное) может работать независимо (в меру возможности и зависимости от CPU) друг от друга. Здесь же идеология наоборот - внутри есть много чего, но выбрать можно лишь что-то одно...
[свернуть]
Я правильно догадываюсь, или нет?

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

В общем, вот такая дилемма. Задействовав хардверный SPI (MOSI и SCK), лишаемся возможности повесить на эти выводы кнопки (знаю, у тебя их не было, но в моём варианте, они именно там по соображениям, что они уживаются с разъёмом PGM), и если они будут нужны, придётся "распечатывать" PD.

Гы: генератор "шума" можно и на микросхемке сделать. Однако учитывая современную корпусобоязнь (ни одним корпусом больше!), решение не станет популярным. Или сделать из принципа "потому что я могу"?

Цитата: zenon от 07 Окт., 2020, 22:50Добавил осциллограмку двумя щупами.
Во, теперь понятно. Весьма шустро получается, и картинка как по учебнику. В 2 с небольшим раза быстрее, чем софтверный.
скрытый текст
; === ShowDigits выводит 7-сегментный код, находящийся в буфере, на индикатор
; === регистр X (R26, R27) портится - используется как указатель на буфер
ShowDigits: LDI XL,low(Digits2Led + Total_Digits)
LDI XH,high(Digits2Led + Total_Digits)
ShowDigits01: ;PUSH R2 ; пусть в R2 располагается маска-счётчик для проверки
LDI R16,Total_Digits
CBI PORTB,RDY ; бита, ответственного за засветку сегмента
ShowDigits02: CLR R2 ; Задвинем туда '1' и будем сдвигать, пока она не
SEC ; вылезет с другой стороны байта
ROR R2 ; вот, установили 1 в старшем разряде маски (R2)
LD R1,-X ; в R1 помещаем код, отсылаемый в сегменты знакоместа
ShowDigits03: CBI PORTB,Clock
ROL R1
BRCS ShowDigits04 ; переход - это два цикла, нет перехода - один
NOP ; поэтому вставляем компенсирующий дрожание NOP
CBI PORTB,Data
RJMP ShowDigits05
ShowDigits04: SBI PORTB,Data
NOP ; два NOP в этой ветке компенсируют задержку
NOP ;  от одного RJMP в соседней ветке
ShowDigits05: SBI PORTB,Clock
ROR R2 ; можно было бы вставить LSR чтобы в R2 слева вдвигался 0, но что там бывают 1 никак не влияет на счёт битов
BRCC ShowDigits03 ; между битами Data 12 циклов
DEC R16
BRNE ShowDigits02
SBI PORTB,RDY ; поднимем строб-защёлку, чтобы отобразить загруженные данные
RET
Если маленько переработать, можно ускорить процентов на 20, но это будет предел. От счётчика в R2 можно полностью избавиться, задвигая в R1 "1" через флаг переноса при прочтении (-X), а в конце цикла проверяя R1 на нуль. Пара-тройка команд из цикла точно уберётся.
[свернуть]
после переработки
После убирания R2, стало короче. Эта процедура теперь выполняется 820 циклов против 953 в варианте, показанном выше. Но зато чуть длиннее - это способ избавиться от одного джампа, отъедающего время. Увы, за всё приходится платить. Можно ещё сократить время выполнения на 64 цикла, убрав NOP'ы перед поднятием Clock
; === ShowDigits выводит 7-сегментный код, находящийся в буфере, на индикатор
; === регистры R1, R16, X (R26,R27) портятся
; на выходе X показывает на начало буфера

ShowDigits: LDI XL,low(Digits2Led + Total_Digits)
LDI XH,high(Digits2Led + Total_Digits)
ShowDigits01: LDI R16,Total_Digits ; в R16 cчётчик выводимых знакомест
CBI PORTB,RDY ; опустим RDY
ShowDigits02: LD R1,-X ; прочитаем знакоместо и задвинем туда '1'
SEC ; сдвигать, пока она не вылезет с другой стороны байта
ROL R1 ; вот, установили 1 в младшем разряде.
ShowDigits03: CBI PORTB,Clock ; в "CF" - значение для подачи в линию "Data"
BRCS ShowDigits04 ; переход - это два цикла, нет перехода - один
NOP ; поэтому вставляем компенсирующий дрожание NOP
CBI PORTB,Data
NOP ; пауза перед подъёмом "Clock"
SBI PORTB,Clock
LSL R1
BRNE ShowDigits03 ; если задвинутая вначале "1" вылезла
DEC R16 ; переходим к следующему знаку
BRNE ShowDigits02
SBI PORTB,RDY ; поднимем строб-защёлку, чтобы отобразить загруженные данные
RET

ShowDigits04: SBI PORTB,Data
NOP ; пауза перед подъёмом "Clock"
SBI PORTB,Clock
LSL R1
BRNE ShowDigits03 ; если задвинутая вначале "1" вылезла
DEC R16 ; переходим к следующему знаку
BRNE ShowDigits02
SBI PORTB,RDY ; поднимем строб-защёлку, чтобы отобразить загруженные данные
RET
убрав NOP'ы получаем 756 циклов на весь 8-разрядный индикатор
; === ShowDigits выводит 7-сегментный код, находящийся в буфере, на индикатор
; === регистры R1, R16, X (R26,R27) портятся
; на выходе X показывает на начало буфера

ShowDigits: LDI XL,low(Digits2Led + Total_Digits)
LDI XH,high(Digits2Led + Total_Digits)
ShowDigits01: LDI R16,Total_Digits ; в R16 cчётчик выводимых знакомест
CBI PORTB,RDY ; опустим RDY
SEC ; прочитаем знакоместо, задвинем в мл.разряд '1' и будем
ShowDigits02: LD R1,-X ; сдвигать, пока она не вылезет с слева
ROL R1 ; вот, 1 в младшем разряде, а в CF бит для Data
ShowDigits03: CBI PORTB,Clock ; опустим Clock
BRCS ShowDigits04 ; переход - это два цикла, нет перехода - один
NOP ; поэтому вставляем компенсирующий дрожание NOP
CBI PORTB,Data
LSL R1
SBI PORTB,Clock ; поднимем Clock
BRNE ShowDigits03 ; если задвинутая вначале "1" вылезла (ZF=1,CF=1)
DEC R16 ; переходим к следующему знаку
BRNE ShowDigits02
SBI PORTB,RDY ; поднимем строб-защёлку, чтобы отобразить загруженные данные
RET

ShowDigits04: SBI PORTB,Data
LSL R1
SBI PORTB,Clock ; поднимем Clock
BRNE ShowDigits03 ; если задвинутая вначале "1" вылезла (ZF=1,CF=1)
DEC R16 ; переходим к следующему знаку
BRNE ShowDigits02
SBI PORTB,RDY ; поднимем строб-защёлку, чтобы отобразить загруженные данные
RET
[свернуть]
Итого: было 953, стало 749, быстрее на 21% :)
[свернуть]

о технологиях плат
с фоторезистом я баловался, но не смог получить стабильный результат. И от принтера нужной плотности не смог добиться (не от одного, их было много, лазерники, струйники). Минусом послужила ещё необходимость держать "химию", да и и фоторезист портится со временем (пара рулонов лежит, выбросить рука не поднимается). Т.е. нужна массовость, которой у меня нет и не предвидится. Поэтому редкие единичные платы ЛУТ'ом, остальное на завод - там маску сделают, металлизацию...
[свернуть]
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

08 Окт., 2020, 18:55 #37 Последнее редактирование: 08 Окт., 2020, 23:01 от zenon
Мне затея с засыпанием не очень нравиться.
Была где-то ветка со сравнением работы так и так, вывод был одинаково, что так, что так.
Но проверить надо.
+++
Trigger по SPI если в осциллографе поставить это тремя/двумя щупами же надо?
Путаюсь я в названиях, кто из них кто, в осциллографе SCL,SDA,CS, When по timeout`у или по CS.
+++
Вот еще что вспомнил, - даже пару слов писал на эту тему: из BluePill stm32 можно сделать отладчик Black Magic Probe, я его сделал, но дальше не продвинулся. :)
Вот на хабре есть.

Slabovik

09 Окт., 2020, 14:54 #38 Последнее редактирование: 09 Окт., 2020, 18:56 от Slabovik
Бумажка - вещь крайне необходимая. И не только в плане гигиены. Ещё она думать помогает и на ней можно рисовать :)

Введение шума в Ref имеет один крайне неприятный подводный камень в плане применения к AVR. Всё дело в её конструкции: Ref имеет только один вывод. Второй вывод Ref жёстко сидит на земле. И это перечёркивает всю задумку. Вот, полюбуйтесь

К-вопросу-о-тщетности-бытия.png

Суть проста. Если мы модулируем Ref только с одного конца (в нашем случае Ref+), то "сетка" из уровней - она как гармошка растягивается и сжимается. И получается, что Ref-, будучи привязанным к земле, не даёт смещаться нижним уровням. Если мы верхний уровень (Ref+) смещаем на 1 отсчёт (один младший разряд), то близкие к нижу уровни смещаются только на 1/1023 отсчёта. Т.е. вообще никак. Серединка смещается на 0.5 отсчёта...

Вывод: в данном применении не годится. Вот на PIC'ах было бы нормально - там выведены оба конца Ref (Ref+ и Ref-) и их можно двигать синхронно.
Цитата: zenon от 08 Окт., 2020, 18:55затея с засыпанием не очень нравиться.
Смотря как на неё смотреть. Есть разные варианты реалицазии. Один - это крутить цикл, отвлекаясь на прерывания (опрос АЦП, вывод). В цикле можно что-нибудь делать, если есть чего. Вся работа по прерываниям - там и делается всё полезное.
Второй вариант - не использовать прерывания вообще, разве что для синхронизации на вывод. Программа просто бегает по кругу опрос-расчёт-вывод. Засыпание не нужно (старые компьютеры как-то обходились без системы прерываний: Радио-86, Орион-128 и т.п.)

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

А вывод конечно одинаковый для глаза будет, тут вопросов нет.
Цитата: zenon от 08 Окт., 2020, 18:55Trigger по SPI если в осциллографе поставить это тремя/двумя щупами же надо?
Триггер удобно поставить по ~|_ на том канале, который на CS (Ready/ST/PB2) смотрит. Тогда легко увидеть весь бурст.

ps. В попытках разобраться, как правильно округлять, сподобился изобразить покрасивее.
Вот график, на котором я попытался изобразить дизер.
Дизер-графики.png
Цветные линии - напряжения на АЦП. Кружочки - отсчёты. Данные отсчётов в квадратиках выше и ниже - они в двоичном коде.
Если без дизера, то результаты измерений были бы 0, 1 и 2. Ориентироваться можно по широким ровным полосам на фоне.
А вот с дизером становится много интереснее, отсчёты перескакивают, но ведь именно это нам и надо  ;)

В общем, исходное разрешение было 1. Делаем 16 отсчётов с челью увеличить разрешение вчетверо. Вместо 1 цена деления станет 0.25. Дальше двоичный калькулятор в руки (ох, нравится мне эта система: палочка есть - палочки нет :) )

Расчёт первый. Учитываем флаг переноса, когда "теряем" младшие биты при сдвиге вправо.
Дизер-расчёт.png
Вроде неплохо.

Был вопрос о том, надо ли учитывать округление (тот самый CF). Пробуем без CF
Дизер-расчёт-без-CF.png
Вот и ответ. 2,63 должно однозначно округлиться к 2.75, но без учёта CF оно осталось на 2.50

Впрочем, вопрос всё ещё спорный.

Ну, а правило для вычисления, надеюсь, стало более понятным: считаем сумму, прибавляем к ней половину от количества слагаемых, делим, добавляя CF от последнего сдвига вправо. Запятую ставим где надо :)

зы... зызы... Продолжим  :) Я добавляю сюда т.к. ответов не было.
Залез в микрокап и промикрокапал мысль о вводе дизера в сигнал. Микрокап говорит, что вполне реальная вещь. Сигнал подмешиваем на выходе масштабирующего усилителя.
Дизер-схема.png
Вначале хотел "квадратный сигнал" по причине, что его получить проще. Не прокатило (ну... не мой день). Но генератор хорошего треугольника можно сделать на одном корпусе с двумя ОУ внутри - это то, что надо. И будет он абсолютно независим и некоррелирован с частотой выборки АЦП - это второе "то, что надо".

Вот что получается в итоге (по данным микрокапа, конечно)
Дизер-схема-графики.png
Сигнал на входе АЦП аккуратненько смещается "треугольником в обе стороны". Амплитуда dither'а при этом совершенно не зависит от величины измеряемого сигнала. Необходимо только выдержать ёмкости. С5 и С6 образуют RC фильтр с выходным сопротивлением мастабирующего усилителя, в то время, как С3 и С7 железно завязаны на подключенные к ним последовательно сопротивления.

Думаю, можно заморочиться сделать  ;)

А, да, ещё момент. Период дизера должен быть много меньше (в разы) времени накопления отсчётов. С другой стороны, пока АЦП работает (время, необходимое для выборки, 13 циклов тактовой частоты АЦП, которая может составлять от 50 до 200 кГц), дизер не должен изменить сигнал на заметную величину. По этой причине режим АЦП "Free Running" наверное неприемлем, нужно делать паузы.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Ну вот не хватает у меня понимания сдвигов, двоичных калькуляторов итп. И на ассемблер я смотрю и, к своему глубокому стыду ничего не понимаю  :o
Для меня функцию опроса АЦП написать на си уже праздник...
С указателями до сих пор не понимаю главного, зачем они нужны? Зачем ссылки на переменные, например типа char, они же ещё больше места занимают.
Суммируя, уходим от прерываний?
Вводим счетчики для выполнения отправки по SPI и для чтения значений АЦП?
ОУ добавить в схему я не против.
Сегодня глянул ещё опору ref192gs - она подешевле, но и чуть похуже параметры, может её вместо 431.

Slabovik

09 Окт., 2020, 22:04 #40 Последнее редактирование: 09 Окт., 2020, 22:31 от Slabovik
Ну, ассемблер - это просто. Можно вполне позаниматься. Только у него есть последствия. Как мне сказал один хороший товарищ, "твоё мышление бенадёжно испорчено ассемблером" :)  Собственно, так и есть  :)  Си мне даётся очень трудно  :-\  А когда пытался Perl осваивать, чуть совсем не свихнулся  :P  Бросил - здоровье дороже  ;D

Ссылки и указатели - это необходимые элементы языка высокого уровня. По факту, каждое объявление переменной является объявлением ссылки на неё, хотя и неявной. Явные ссылки используют для организации работ с высоким объёмом данных. Абстракция очень простая.

Переменная: я вот тебе архивчик (на пару помещений) принёс, тебе бумашка была нужна на  третьей полке. Как закончишь, я всё унесу на место.  :o

Ссылка: вот тебе адрес, гони туда (сам ножками), там третья полка, на полке бумашка, прочитай и дуй обратно к себе.

Я надеюсь, алгоритм рассчёта-то понятен?
Цитата: Slabovik от 09 Окт., 2020, 14:54считаем сумму, прибавляем к ней половину от количества слагаемых, делим, добавляя CF от последнего сдвига вправо. Запятую ставим где надо
:)

Цитата: zenon от 09 Окт., 2020, 20:38ОУ добавить в схему я не против.
Один кузовок MCP 6002 (я не ошибся? Два ОУ в одном кузове). И немного усложнить вход АЦП несколькими деталями для подмеса дизера.
Алгоритм (не на си, а так, чисто алгоритмически, на си сам будешь  ;) ) разрисую попозже, наверное уж на следующей неделе. А то завтра в поля подамся, очередную хуюмбулу точить :)

Да, REF'ы - они отличные в плане точности. Даже G вполне хватит - уже она на порядок лучше чем Tl431. Тут интересны не столько абсолютная точность, сколько температурный уход.

REF198? Она 4,096 вольта - как раз почти полный диапазон питания, саму её можно от этих же 5 вольт запитать (запаса хватает т.к. нагрузки на неё практически нет).

Есть более дешёвые LM4040 (.pdf) - они в SOT23, по принципу как TL431, только без возможности регулировки.

Вот, чуть перерисовал модельку дизер-генератора.
Жалко, но в свою плату я такое вставить уже не могу - они готовые. Впрочем, платы мастабирующих усилителей только в проекте - можно в них предусмотреть.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Тут я скорее не понимаю где и как указатели использовать, если переменная объявлена глобально в функциях она тоже доступна...
Генератор наверное можно и на народной 358, мы определились будет двухдиапазонное измерение или хватит 2-х каналов АЦП?
Можно и REF198.
LM4040 у своих нашёл тыц. спс.

Slabovik

09 Окт., 2020, 22:45 #42 Последнее редактирование: 10 Окт., 2020, 00:12 от Slabovik
Если делать дизер, то без двух диапазонов нормально выходит 4 десятичных разряда. Я у себя делаю два диапазона потому что не предусматривал дизер и у меня горят всегда только три цифры. А 4-разрядный индикатор использую только для того, чтобы не двигать точку. Это заметно облегчает восприятие показаний (тут уже эргономика вмешивается).

LM358 можно попробовать. Но у неё ограничение сверху до полутора вольт, можно среднюю точку чуть пониже повесить (точка Half), резисторы примерно с отношением 6:5 (т.е. 1,2 кОм верхний, 1,0 кОм нижний). Это даст среднюю точку на уровне 5*1.0/(1.0+1.2)=2.2 вольта. Если генерации не будет, пробовать понемногу уменьшать R4 (33к на последней схеме). От его отношения к R3 зависит амплитуда треугольных колебаний, если треугольник "наткнётся" на клиппинг, генерации не будет. Чем меньше R4, тем меньше амплитуда, но больше частота. А от R5 зависит скорость роста напряжения треугольника, что тоже прямым образом влияет на частоту.

Да, указатели... Явные указатели чисто идеологически используются для сложных структур данных. Когда есть какой-то "неопределённого объёма" набор данных (в памяти, в стеке или ещё где, причём его местонахождение - не константа а меняется по мере прихода-ухода данных), то указатели становятся очень удобным средством. Самые простейшие указатели применительно к микропроцессорам - указатель стека (регистр стека) и указатель исполняемой команды (регистр адреса). Регистр стека показывает (содержит адрес) на верхушку стека (область памяти). Регистр адреса, он же Program Counter (PC), показывает на ячейку памяти, из которой выполняется очередная команда процессора.

Ведь само понятие указатель - это чисто логическая штука, она в голове.  Физически указатель - это адрес. Вот здесь
ShowDigits: LDI XL,low(Digits2Led + Total_Digits)
LDI XH,high(Digits2Led + Total_Digits)
ShowDigits01: LDI R16,Total_Digits ; в R16 cчётчик выводимых знакомест
CBI PORTB,RDY ; опустим RDY
ShowDigits02: LD R1,-X ; прочитаем знакоместо и задвинем туда '1'
происходит загрузка указателя в регистры
LDI XL,low(Digits2Led + Total_Digits)
LDI XH,high(Digits2Led + Total_Digits)
Digits2Led - это адрес памяти, которая выделена под буфер "экрана". Фактически константа т.к. буфер экрана "закреплён" строго в одном месте памяти. А Ttal_Digits - это длина буфера. Таким образом указатель показывает на конец "экранной памяти", здесь это просто потому, что данные в регистры пересылать надо с конца - там так запаяно.

и затем "взятие" данных их ячейки, на которую показывает этот указатель
LD R1,-X ; прочитаем знакоместот.е. мы по указателю X (бмажка с адресом) сходили по адресу, прочитали то, что там записано и переписали себе в локальную переменную (регистр) R1. С R1 дальше и будем работать.

А вот здесь вот
LDI R16,Total_Digits ; в R16 cчётчик выводимых знакоместтоже произошло определение локальной переменной (выделили под неё регистр R16) и присвоение этой переменной начального значения - константы Total_Digits (которая в моём случае равна 8 - это определено в самом начале программы).
Локальная она потому, что я поработаю с этим регистром (у переменной-то и имени фактически нет), а по выходу из процедуры "забуду" про данные из него - они больше не нужны и регистр можно использовать для чего-нибудь ещё.

В Си-шных эквивалентах это выглядело бы примерно так.
-----------------------------
LDI XL,low(Digits2Led + Total_Digits)
LDI XH,high(Digits2Led + Total_Digits)
~~
unsigned shortint X = (&Digits2Led)+Total_Digits;
-----------------------------
LD R1,-X
~~
R1 = --*X;
-----------------------------
LDI R16,Total_Digits
~~
char i=Total_Digits;

Поправьте меня, если я где ошибся...
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Ух и разговорчик у нас получается, - пример на asm, и я неуч в си.
Перечитаю пару раз ещё пару раз, хотя вот вчера до ночи вникать пытался, что нашёл по указателям... туго.
Не я конечно же посмотрел что есть LDI, LD, CBI...
+++
Если 358 неустойчива может быть, то ладно, 6002 есть.
Мысль была на две платы разбить, дизер+масштаб_ус+шунт+опора на одной, остальное на другой, опора под вопросом.
В своем варианте вывод был двух светодиодов на всякий случай, тут я не совсем определился, LM35 можно ещё добавить, или просто терморезистор на 10k. Ну и переключение обмоток как опция, включение/отключение выхода?

Slabovik

Не, 358 устойчива, просто она не совсем R2R, что при низком напряжении питания надо учитывать.
Генератор спаять и на проводках можно, чтобы просто проверить - он не сложный, пять резисторов и два конденсатора (Half тоже надо организовать).
Убедиться, что он работает (а если не работает, то наладить), да помучить немного :)

Дизер лучше на платке проца разместить. А вот масштабирующие можно и наружу. Плюс такого решения - мастабирующие не будут греться от индикаторов, да и заменить их легче (например, немного переработав или ещё чего).

Управлять релюшками, коммутирующими напряжение, с индикатора я бы не решился, да и вентилятор лучше сделать самостоятельным узлом (тиньки даже хватит, а если не бояться "тёплолампового аналога, то я там где-то схемку прорабатывал - работает прекрасно).
А вот включение-отключение выхода - вещь полезная. Сюда же можно подумать, как замер тока ограничения сделать (есть два варианта: первый - устраивать к.з, второй - измерять задающее напряжение).
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Цитата: Slabovik от 10 Окт., 2020, 00:47то я там где-то схемку прорабатывал
Собирал, помню, это я только к кикаду подступаться начал, первый вариант в аттаче, был ещё один, но почему-то кикад плату не открывает, ругается на версию.

zenon

10 Окт., 2020, 13:23 #46 Последнее редактирование: 10 Окт., 2020, 20:47 от zenon
Цитата: Slabovik от 07 Окт., 2020, 19:02Страшно мне смотреть на выкрутасы с ADMUX. Знаю, что для данного применения нормально, один фиг там управляющие биты по нулям и от нулей отличаются только сами MUX, но... правильно было бы читать ADMUX, изменять MUX (сле-едующий! :) ) и пихать его обратно. Ну или применять здесь дефайны настройки АЦП. При этом завязать номер MUX на переменную-указатель adc_channel (хотя, вынимая номер из MUX всегда можно знать, какой АЦП сейчас был включен.

В реальности же это работоспособности не вредит, только "причёсывает" стиль написания.
Вот эту строку у немца я до конца не понял, её бы посидеть покрутить...
Но она мне нравится, и работает :)
#define adc_select_channel(channel) (ADMUX = (ADMUX & (~((1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0)))) | (channel))

Добавил её в свой код и изменил ADC_vect:
...
...
...
#define adc_channels    2 // read ADC0 and ADC1
#define adc_select_channel(channel) (ADMUX = (ADMUX & (~((1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0)))) | (channel))
...
...
...
ISR(ADC_vect) {
      adc_buffer[adc_channel] = ADC;
      adc_channel = (adc_channel + 1) % adc_channels;
      adc_select_channel(adc_channel);
}
...
...
...
Так лучше?
Вот такой lss получается:
// Прерывание опроса АЦП ==============================================
ISR(ADC_vect) {
 11e: 1f 92      push r1
 120: 0f 92      push r0
 122: 0f b6      in r0, 0x3f ; 63
 124: 0f 92      push r0
 126: 11 24      eor r1, r1
 128: 2f 93      push r18
 12a: 3f 93      push r19
 12c: 8f 93      push r24
 12e: 9f 93      push r25
 130: ef 93      push r30
 132: ff 93      push r31
      adc_buffer[adc_channel] = ADC;
 134: 80 91 1f 01 lds r24, 0x011F
 138: 90 e0      ldi r25, 0x00 ; 0
 13a: 20 91 78 00 lds r18, 0x0078
 13e: 30 91 79 00 lds r19, 0x0079
 142: fc 01      movw r30, r24
 144: ee 0f      add r30, r30
 146: ff 1f      adc r31, r31
 148: e0 5e      subi r30, 0xE0 ; 224
 14a: fe 4f      sbci r31, 0xFE ; 254
 14c: 31 83      std Z+1, r19 ; 0x01
 14e: 20 83      st Z, r18
      adc_channel = (adc_channel + 1) % adc_channels;
 150: 01 96      adiw r24, 0x01 ; 1
 152: 81 70      andi r24, 0x01 ; 1
 154: 90 70      andi r25, 0x00 ; 0
 156: 80 93 1f 01 sts 0x011F, r24
      adc_select_channel(adc_channel);
 15a: ec e7      ldi r30, 0x7C ; 124
 15c: f0 e0      ldi r31, 0x00 ; 0
 15e: 20 81      ld r18, Z
 160: 20 7f      andi r18, 0xF0 ; 240
 162: 28 2b      or r18, r24
 164: 20 83      st Z, r18
}
 166: ff 91      pop r31
 168: ef 91      pop r30
 16a: 9f 91      pop r25
 16c: 8f 91      pop r24
 16e: 3f 91      pop r19
 170: 2f 91      pop r18
 172: 0f 90      pop r0
 174: 0f be      out 0x3f, r0 ; 63
 176: 0f 90      pop r0
 178: 1f 90      pop r1
 17a: 18 95      reti
+++
Начал отрисовывать, правильно?
В опору ничего не надо?

Slabovik

11 Окт., 2020, 22:14 #47 Последнее редактирование: 11 Окт., 2020, 22:52 от Slabovik
Эту строку надо изнутри разбирать
Цитата: zenon от 10 Окт., 2020, 13:23#define adc_select_channel(channel) (ADMUX = (ADMUX & (~((1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0)))) | (channel))
Вот это '<<' побитовый сдвиг влево. Запись простая: [то, что двигаем] << [на сколько двигаем].
MUX0, MUX1, MUX2 - это имена битов в регистре управления выбором канала АЦП. В числовом выражении это 0-й бит, 1-й бит и 2-й бит соответственно.
Соответственно, запись, 1<<MUX2 означает, что берём единицу и сдвигаем слево побитово на два (потому что определено, что MUX2=2)
В бинарном виде получается
00000001 << 2 = 00000100. Т.е единичка "уехала" на две позиции левее.
С MUX1 и MUX0 то же самое. Только поскольку MUX0 равен нулю, это значит что единичка никуда и не уехала - осталась сама собой.
00000001 << 1 = 00000010
00000001 << 0 = 00000001

Далее вот это '|' - операция "побитовое логическое или". Суть простая: в результате соответствующая позиция бита равна 1, если хотя бы у одного операнда в этой позиции есть 1.
Очень просто:
0 | 1 = 1,
1 | 0 = 1,
1 | 1 = 1,
0 | 0 = 0

С восемью битами то же самое, только в расчёт идут не все биты сразу, а по позициям. Напишу результат также в столбик (нагляднее)
00000100 |
00000010 |
00000001 =
00000111  Очень прикольно :) Скобка закрылась с результатом '7' (00000111bin = 7)
Но на деле интереснее именно бинарный результат.

Далее операция "~" - побитовая инверсия. В общем, те биты, что были '1', становятся нулями и наоборот.
Было 00000111, стало 11111000  :o А зачем?
А затем, что это будет применено в качестве "маски", при помощи которой биты MUX будут обнулены с дальнейшей целью записать туда новое значение так, чтобы другие биты регистра управления АЦП остались нетронутыми.
Для этого используется следующая операция: "побитовое логическое и", суть которой в том, что результат (побитовый, точно также как и в других логических операциях) равен в соответствующей позиции байта только тогда, когда биты оба операнда равны 1, иначе в бите результата будет 0. Получается тоже интересно.

У нас слева в "маске" пять единиц. Первый операнд - содержимое порта ADMUX. Второй операнд - вот эта маска.
После того, как мы их '&', результатом будут не изменённые (взаправдашние) левые пять бит из порта ADMUX (либо нули, либо единицы - нам пофиг, главное, что именно они, а не нечто другое), а справа гарантированных три нулевых бита. И позиции этих битов тютя-в-тютю на позиции номера канала АЦП (это как раз выше определили).

Далее, со всем эти результатом - пятью битами из порта ADMUX и свободным местом для номера канала, снова делаем логическое "или" уже с новым номером канала. По факту он просто прописывается (бинарно) вместо трёх нулевых битов (он конечно сам может быть нулевым, но тут главное, что эти новые биты точно соответствуют нужному нам номеру из "channel").
Ну и последнее ADMUX= - это запись всего этого результата обратно в регистр. Пять "левых" битов остались неизменными, три бита справа содержат новый номер канала. Профит  :) Ты пишешь
adc_select_channel(channel);
а компилятор это видит как
ADMUX = (ADMUX & (~((1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (1<<MUX0)))) | (channel);

зы: я в объяснении использовал три MUX, 4-й добавить туда самостоятельно, думаю, не составит проблем.
Цитата: zenon от 10 Окт., 2020, 13:23Вот такой lss получается
Видно, что переменная adc_channel (да и наверное adc_channels) двубайтовые. Это излишне - ведь там никогда не бывает больше 16 в принципе. Надо бы им в определениях чётко прописать, char мол это, и даже беззнаковый.


Схема imho нормальная. Видятся лишними C1 и C3 потому что C7 и C8 - это они и есть.

А мне вот жужжит мысль "поставь-ка DD3.1 первой в цепочке, а не последней, как сейчас". зачем - не пойму, просто если как изображено по схеме, первыми в "сдвиг" должны уйти данные для светодиодов и транзисторов, а потом данные на сегменты индикатора, а если переставить - светодиоды и транзисторы будем вдвигать последними. Чисто технически это совершенно пофиг, в какой последовательности ставить, но мысль основывается на том, что DD3.1 может оказаться проще чисто по разводке поставить ближе к процессору. Можно поставить себе крестик, что в этом месте вдруг что-то поменяется и быть готовым к этому.

А опора остаётся как есть. Т.е. что есть - то и ставишь. LM4040 наверное в самый раз, а если REF19x засунуть - это вообще шикарно :) Абсолютное значение напряжения роли большой не играет (это для выходного ЦАП было бы важно) т.к. всё-равно всё приводится к нему мастабирующими усилителями, крутить которыми можно как угодно. Единственное - это то, что значение опорного напряжения я бы выбирал повыше - проще получить стабильный ноль. Как раз идеальное значение в районе 4-х вольт (и уже много, 4 мВ на степ, и до питания ещё не достаёт, что опору можно прямо от этих же +5 и питать). Но и 2,5 и 3 тоже будут нормальным вариантом.
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.

zenon

Тут получается как, - по отдельности вроде понятно, в кучу сгреблось - нет.
Вот упростим (предположим, что у нас только MUX1 и MUX0):
#define adc_select_channel(channel) (ADMUX = (ADMUX & (~((1<<MUX1) | (1<<MUX0)))) | (channel))теперь всего может быть 00, 01, 11, 10, так?
Значит подставляя в этом сокращённом варианте в channel от 0 до 3 получим эти четыре комбинации?


Цитата: Slabovik от 11 Окт., 2020, 22:14Соответственно, запись, 1<<MUX2 означает, что берём единицу и сдвигаем слево побитово на два (потому что определено, что MUX2=2)
Не понял откуда видно, что MUX2=2?

+++
Начал разводить, пока вот это получается, дороги крупновато взял, 0,44мм, надо на десятку меньше наверное.
Поверхностным монтажом выводные потенциометры сделал, не лучший вариант, но эту плату наверное оставлю без индикаторов, те это макетом будет. В окончательном варианте индикаторы будут на другой стороне.

Slabovik

Цитата: zenon от 12 Окт., 2020, 11:01теперь всего может быть 00, 01, 11, 10, так?
Да, но не совсем. Комбинации из MUX1 и  MUX0 могут быть именно такими. Но т.к. в выражении формируется именно маска (как бы "закрывающая" часть битов от изменения), то результатом получается, что channel можно выбирать только из этого получившегося ряда.
скрытый текст
на самом деле это выражение всё-таки маленько "дырявое" по той причине, что маска на считанное значение ADMUX накладывается, но за значением переменной channel следить нужно программисту, не допуская её увеличение свыше, чем есть каналов у АЦП.
[свернуть]
Цитата: zenon от 12 Окт., 2020, 11:01откуда видно, что MUX2=2?
MUX2 - это имя, определённое через #define в заголовочном файла с настройками процессора. Там где-то в начале программы у тебя <include xxxx.h> есть, называется обычно по названию использованного процессора. Там внутри куча сделана дефайнов. Можно найти этот файл и посмотреть на все дефайны - оно бывает полезно.

А то, почему MUX2 равен должен быть именно 2 - это уже от аппаратуры зависит, т.к. оно внутри так работает (у атмела хорошие даташиты с описанием всех узлов, что внутри проца есть). Вот, например, на Мегу 8-ю, про устройство регистра ADMUX там на 199 странице (по-английски, конечно). На другие там такие же.
Цитата: zenon от 12 Окт., 2020, 11:01дороги крупновато взял, 0,44мм, надо на десятку меньше наверное.
Если плата планируется быть пробной, то можно не мельчить. Удобнее будет резать (я так думаю)...

А в какой размер планируешь уложиться? Как индикаторы расположить? Какой  размер индикаторов (я закладываюсь на СС56-12, 20x50 мм)
Общением на форуме подпитываю свою эгоистичную, склонную к самолюбованию сущность.