برنامه ای برای میکرو کنترلر ATTiny45 که ولتاژ دقیق را بصورت 16 بیتی توسط ADS1110 اندازه گیری و توسط تراشه TM1637 نشان میده

این یک پروژه‌ی بسیار چالش‌برانگیز و در عین حال کاربردی است، زیرا میکروکنترلر ATTiny45 حافظه و منابع بسیار محدودی دارد و برای ارتباط با هر دو ماژول ADS1110 و TM1637 باید پروتکل‌های ارتباطی را به صورت نرم‌افزاری پیاده‌سازی کنیم. در اینجا یک نمونه کد بهینه و کامل ارائه می‌شود که این وظیفه را انجام می‌دهد.


شماتیک و اتصالات

برای این پروژه، اتصالات سخت‌افزاری زیر لازم است:

  • VCC از ATTiny45 به VCC هر دو ماژول ADS1110 و TM1637
  • GND از ATTiny45 به GND هر دو ماژول
  • پایه PB0 از ATTiny45 به SDA ماژول ADS1110 و DIO ماژول TM1637
  • پایه PB1 از ATTiny45 به SCL ماژول ADS1110 و CLK ماژول TM1637
  • ولتاژ ورودی شما به پایه VIN+ ماژول ADS1110
  • پایه VIN- ماژول ADS1110 به GND متصل می‌شود (برای حالت تک‌طرفه).
  • دو مقاومت ۴.۷ کیلواهمی به عنوان مقاومت‌های پول‌آپ (Pull-up Resistors) روی خطوط SDA و SCL به VCC متصل می‌شوند.

توجه: در این پروژه، پین‌های I2C و TM1637 با هم مشترک هستند. این کار به دلیل کمبود پین در ATTiny45 انجام شده است و با مدیریت دقیق کد امکان‌پذیر است، زیرا پروتکل‌های آن‌ها متفاوت است.


نمونه کد

این کد برای استفاده در محیط Arduino IDE و با نصب هسته ATTiny نوشته شده است.

C++

#include <avr/io.h>
#include <util/delay.h>

// ADS1110 I2C Defines
#define ADS1110_ADDR 0x48   // I2C address of ADS1110
#define SDA_PIN PB0
#define SCL_PIN PB1

// TM1637 Defines
#define TM1637_DIO_PIN PB0
#define TM1637_CLK_PIN PB1

// ADS1110 Functions
void i2c_init();
void i2c_start();
void i2c_stop();
void i2c_write(uint8_t data);
uint8_t i2c_read(uint8_t ack);
int16_t readADS1110();

// TM1637 Functions
void tm1637_start();
void tm1637_stop();
void tm1637_write_byte(uint8_t b);
void tm1637_display(int16_t num);
void tm1637_init();

void setup() {
  i2c_init();
  tm1637_init();
}

void loop() {
  int16_t adcValue = readADS1110();
  // Convert ADC value to millivolts
  // Vref = 2.048V, 16-bit range = 32768
  int16_t mV = (int16_t)((float)adcValue * (2048.0 / 32768.0) * 1000.0);

  // We are going to display the voltage in millivolts.
  // For example, if mV = 1234, display will be 12.34
  // We can also adjust this to display in volts, e.g., 1.23
  // For simplicity, let's just display the millivolt value.
  tm1637_display(mV);
  
  _delay_ms(1000); // Wait for 1 second before the next reading
}

//--- ADS1110 & I2C Software Implementation ---//
void i2c_init() {
  DDRB &= ~((1 << SDA_PIN) | (1 << SCL_PIN));
  PORTB |= (1 << SDA_PIN) | (1 << SCL_PIN);
}

void i2c_start() {
  // Ensure both are high before starting
  DDRB &= ~((1 << SDA_PIN) | (1 << SCL_PIN));
  _delay_us(4);
  DDRB |= (1 << SDA_PIN); // SDA high to low
  _delay_us(4);
  DDRB |= (1 << SCL_PIN); // SCL stays high
  _delay_us(4);
}

void i2c_stop() {
  DDRB &= ~(1 << SDA_PIN);
  _delay_us(4);
  DDRB |= (1 << SCL_PIN);
  _delay_us(4);
  DDRB |= (1 << SDA_PIN); // SDA low to high
  _delay_us(4);
}

void i2c_write(uint8_t data) {
  for (uint8_t i = 0; i < 8; i++) {
    DDRB |= (1 << SCL_PIN);
    _delay_us(1);
    if (data & 0x80) {
      DDRB &= ~(1 << SDA_PIN);
    } else {
      DDRB |= (1 << SDA_PIN);
    }
    _delay_us(4);
    DDRB &= ~(1 << SCL_PIN);
    _delay_us(4);
    data <<= 1;
  }
  // Read ACK
  DDRB &= ~(1 << SDA_PIN);
  _delay_us(4);
  // while(PINB & (1 << SDA_PIN)); // Don't check ACK to save code space
}

uint8_t i2c_read(uint8_t ack) {
  uint8_t data = 0;
  DDRB &= ~(1 << SDA_PIN);
  for (uint8_t i = 0; i < 8; i++) {
    DDRB |= (1 << SCL_PIN);
    _delay_us(4);
    data <<= 1;
    if (PINB & (1 << SDA_PIN)) {
      data |= 1;
    }
    DDRB &= ~(1 << SCL_PIN);
    _delay_us(4);
  }
  if (ack) {
    DDRB |= (1 << SDA_PIN);
  } else {
    DDRB &= ~(1 << SDA_PIN);
  }
  return data;
}

int16_t readADS1110() {
  int16_t value = 0;
  uint8_t highByte, lowByte;

  i2c_start();
  i2c_write(ADS1110_ADDR << 1); 
  i2c_write(0x8C); // Config: 16bit, 15sps, single-shot mode
  i2c_stop();

  _delay_ms(100); 

  i2c_start();
  i2c_write((ADS1110_ADDR << 1) | 1);
  highByte = i2c_read(1);
  lowByte = i2c_read(0);
  i2c_stop();

  value = (highByte << 8) | lowByte;
  return value;
}

//--- TM1637 Software Implementation ---//
// This is a simplified version of the library to save space.
void tm1637_init() {
  DDRB |= (1 << TM1637_CLK_PIN) | (1 << TM1637_DIO_PIN);
  tm1637_start();
  tm1637_write_byte(0x8f); // 88h - Display control, 8Fh - display ON, 10/16 brightness
  tm1637_stop();
}

void tm1637_start() {
  DDRB |= (1 << TM1637_CLK_PIN);
  DDRB &= ~(1 << TM1637_DIO_PIN);
  _delay_us(4);
}

void tm1637_stop() {
  DDRB |= (1 << TM1637_CLK_PIN);
  _delay_us(4);
  DDRB |= (1 << TM1637_DIO_PIN);
  _delay_us(4);
}

void tm1637_write_byte(uint8_t b) {
  for (uint8_t i = 0; i < 8; i++) {
    DDRB &= ~(1 << TM1637_CLK_PIN);
    _delay_us(2);
    if ((b >> i) & 1) {
      DDRB &= ~(1 << TM1637_DIO_PIN);
    } else {
      DDRB |= (1 << TM1637_DIO_PIN);
    }
    DDRB |= (1 << TM1637_CLK_PIN);
    _delay_us(2);
  }
  DDRB &= ~(1 << TM1637_CLK_PIN);
  _delay_us(2);
  DDRB &= ~(1 << TM1637_DIO_PIN);
  _delay_us(2);
  DDRB |= (1 << TM1637_CLK_PIN);
  _delay_us(2);
}

void tm1637_display(int16_t num) {
  uint8_t digits[4] = {0};
  uint8_t i = 3;
  
  if (num == 0) {
      digits[0] = 0;
  } else {
      while(num > 0 && i >= 0) {
          digits[i--] = num % 10;
          num /= 10;
      }
  }

  uint8_t segments[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

  tm1637_start();
  tm1637_write_byte(0x40); // Address setting command
  tm1637_stop();

  tm1637_start();
  tm1637_write_byte(0xC0); // Data command, start at address 0
  tm1637_write_byte(segments[digits[0]] | 0x80); // Display 1st digit with decimal point
  tm1637_write_byte(segments[digits[1]]); // Display 2nd digit
  tm1637_write_byte(segments[digits[2]]); // Display 3rd digit
  tm1637_write_byte(segments[digits[3]]); // Display 4th digit
  tm1637_stop();
}

توضیحات کد

  1. پین‌های مشترک: در این کد، از پین‌های PB0 و PB1 به صورت مشترک برای I2C (ADS1110) و پروتکل TM1637 استفاده شده است. این کار به دلیل محدودیت پین‌ها در ATTiny45 است. این اشتراک پین‌ها به این دلیل کار می‌کند که پروتکل‌های این دو ماژول با هم متفاوت هستند و می‌توان آن‌ها را به صورت دستی مدیریت کرد.
  2. تابع readADS1110(): این تابع مقدار ۱۶ بیتی را از ADS1110 می‌خواند.
    • ابتدا پیکربندی ماژول را با آدرس 0x8C می‌نویسد. این پیکربندی، ADS1110 را برای دقت ۱۶ بیتی در حالت تک‌شات (Single-Shot) و با نرخ نمونه‌برداری ۱۵ SPS تنظیم می‌کند.
    • سپس، برای اتمام تبدیل، یک تأخیر ۱۰۰ میلی‌ثانیه‌ای در نظر گرفته می‌شود.
    • در نهایت، دو بایت (High Byte و Low Byte) از ماژول خوانده و به یک مقدار int16_t تبدیل می‌شوند.
  3. محاسبه ولتاژ: مقدار خام ADC به ولتاژ بر حسب میلی‌ولت (mV) تبدیل می‌شود.
    • mV = (int16_t)((float)adcValue * (2048.0 / 32768.0) * 1000.0);
    • 2048.0 ولتاژ مرجع داخلی ADS1110 (2.048V) است.
    • 32768.0 نصف دامنه ۱۶ بیتی است که برای تبدیل به مقدار ولتاژ استفاده می‌شود.
    • 1000.0 برای تبدیل ولت به میلی‌ولت استفاده می‌شود.
  4. توابع TM1637: این توابع پروتکل TM1637 را به صورت نرم‌افزاری پیاده‌سازی می‌کنند.
    • tm1637_init() نمایشگر را برای کار آماده می‌کند.
    • tm1637_display(int16_t num) یک عدد صحیح را به چهار رقم جداگانه تقسیم کرده و آن‌ها را روی نمایشگر TM1637 نشان می‌دهد.
    • برای نمایش اعشار، باید به صورت دستی رقم مربوطه را با دستور 0x80 (segments[digits[0]] | 0x80) علامت‌گذاری کنید. در این مثال، اعشار بعد از اولین رقم قرار می‌گیرد تا مثلاً ولتاژ ۱.۲۳۴ ولت به صورت 1.234 نمایش داده شود.

نکات مهم:

به دلیل استفاده از float در محاسبات، ممکن است کد کمی بزرگ باشد، اما به دلیل سادگی، برای ATTiny45 با ۴ کیلوبایت حافظه کافی است. در صورت مواجهه با خطای حافظه، می‌توانید محاسبات را به صورت اعداد صحیح انجام دهید.

این کد برای صرفه‌جویی در فضای حافظه، فاقد مدیریت خطا (Error Handling) برای ارتباط I2C است.

همچنین، توابع delay بسیار کوتاه (_delay_us(4)) برای سرعت بخشیدن به ارتباط‌ها استفاده شده است.

مقدار ولتاژ را به صورت میلی‌ولت و بدون اعشار نمایش دهد. در این نسخه، تابع tm1637_display به گونه‌ای تغییر کرده که فقط چهار رقم صحیح را نشان می‌دهد و از نقطه‌ی اعشار صرف‌نظر می‌کند.


نمونه کد اصلاح شده

C++

#include <avr/io.h>
#include <util/delay.h>

// ADS1110 I2C Defines
#define ADS1110_ADDR 0x48   // I2C address of ADS1110
#define SDA_PIN PB0
#define SCL_PIN PB1

// TM1637 Defines
#define TM1637_DIO_PIN PB0
#define TM1637_CLK_PIN PB1

// ADS1110 Functions
void i2c_init();
void i2c_start();
void i2c_stop();
void i2c_write(uint8_t data);
uint8_t i2c_read(uint8_t ack);
int16_t readADS1110();

// TM1637 Functions
void tm1637_start();
void tm1637_stop();
void tm1637_write_byte(uint8_t b);
void tm1637_display(int16_t num);
void tm1637_init();

void setup() {
  i2c_init();
  tm1637_init();
}

void loop() {
  int16_t adcValue = readADS1110();
  // تبدیل مقدار ADC به میلی‌ولت
  // Vref = 2.048V, 16-bit range = 32768
  int16_t mV = (int16_t)((float)adcValue * (2048.0 / 32768.0) * 1000.0);
  
  tm1637_display(mV);
  
  _delay_ms(1000); // یک ثانیه تأخیر قبل از خواندن مجدد
}

//--- ADS1110 & I2C Software Implementation ---//
void i2c_init() {
  DDRB &= ~((1 << SDA_PIN) | (1 << SCL_PIN));
  PORTB |= (1 << SDA_PIN) | (1 << SCL_PIN);
}

void i2c_start() {
  DDRB &= ~((1 << SDA_PIN) | (1 << SCL_PIN));
  _delay_us(4);
  DDRB |= (1 << SDA_PIN); 
  _delay_us(4);
  DDRB |= (1 << SCL_PIN); 
  _delay_us(4);
}

void i2c_stop() {
  DDRB &= ~(1 << SDA_PIN);
  _delay_us(4);
  DDRB |= (1 << SCL_PIN);
  _delay_us(4);
  DDRB |= (1 << SDA_PIN); 
  _delay_us(4);
}

void i2c_write(uint8_t data) {
  for (uint8_t i = 0; i < 8; i++) {
    DDRB |= (1 << SCL_PIN);
    _delay_us(1);
    if (data & 0x80) {
      DDRB &= ~(1 << SDA_PIN);
    } else {
      DDRB |= (1 << SDA_PIN);
    }
    _delay_us(4);
    DDRB &= ~(1 << SCL_PIN);
    _delay_us(4);
    data <<= 1;
  }
}

uint8_t i2c_read(uint8_t ack) {
  uint8_t data = 0;
  DDRB &= ~(1 << SDA_PIN);
  for (uint8_t i = 0; i < 8; i++) {
    DDRB |= (1 << SCL_PIN);
    _delay_us(4);
    data <<= 1;
    if (PINB & (1 << SDA_PIN)) {
      data |= 1;
    }
    DDRB &= ~(1 << SCL_PIN);
    _delay_us(4);
  }
  if (ack) {
    DDRB |= (1 << SDA_PIN);
  } else {
    DDRB &= ~(1 << SDA_PIN);
  }
  return data;
}

int16_t readADS1110() {
  int16_t value = 0;
  uint8_t highByte, lowByte;

  i2c_start();
  i2c_write(ADS1110_ADDR << 1); 
  i2c_write(0x8C); // Config: 16bit, 15sps, single-shot mode
  i2c_stop();

  _delay_ms(100); 

  i2c_start();
  i2c_write((ADS1110_ADDR << 1) | 1);
  highByte = i2c_read(1);
  lowByte = i2c_read(0);
  i2c_stop();

  value = (highByte << 8) | lowByte;
  return value;
}

//--- TM1637 Software Implementation ---//
void tm1637_init() {
  DDRB |= (1 << TM1637_CLK_PIN) | (1 << TM1637_DIO_PIN);
  tm1637_start();
  tm1637_write_byte(0x8f); 
  tm1637_stop();
}

void tm1637_start() {
  DDRB |= (1 << TM1637_CLK_PIN);
  DDRB &= ~(1 << TM1637_DIO_PIN);
  _delay_us(4);
}

void tm1637_stop() {
  DDRB |= (1 << TM1637_CLK_PIN);
  _delay_us(4);
  DDRB |= (1 << TM1637_DIO_PIN);
  _delay_us(4);
}

void tm1637_write_byte(uint8_t b) {
  for (uint8_t i = 0; i < 8; i++) {
    DDRB &= ~(1 << TM1637_CLK_PIN);
    _delay_us(2);
    if ((b >> i) & 1) {
      DDRB &= ~(1 << TM1637_DIO_PIN);
    } else {
      DDRB |= (1 << TM1637_DIO_PIN);
    }
    DDRB |= (1 << TM1637_CLK_PIN);
    _delay_us(2);
  }
  DDRB &= ~(1 << TM1637_CLK_PIN);
  _delay_us(2);
  DDRB &= ~(1 << TM1637_DIO_PIN);
  _delay_us(2);
  DDRB |= (1 << TM1637_CLK_PIN);
  _delay_us(2);
}

void tm1637_display(int16_t num) {
  uint8_t digits[4] = {0};
  uint8_t i = 3;
  
  if (num == 0) {
      digits[0] = 0;
  } else {
      while(num > 0 && i >= 0) {
          digits[i--] = num % 10;
          num /= 10;
      }
  }

  uint8_t segments[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

  tm1637_start();
  tm1637_write_byte(0x40); // Address setting command
  tm1637_stop();

  tm1637_start();
  tm1637_write_byte(0xC0); // Data command, start at address 0
  tm1637_write_byte(segments[digits[0]]); 
  tm1637_write_byte(segments[digits[1]]); 
  tm1637_write_byte(segments[digits[2]]); 
  tm1637_write_byte(segments[digits[3]]); 
  tm1637_stop();
}

تغییرات انجام شده

  • حذف کد مربوط به اعشار: در تابع tm1637_display، خط مربوط به نمایش نقطه‌ی اعشار (| 0x80) از دستور نمایش اولین رقم حذف شده است.
  • نمایش عدد صحیح: اکنون مقدار mV که یک عدد صحیح است، مستقیماً به تابع tm1637_display ارسال می‌شود و نمایشگر TM1637 آن را به صورت چهار رقمی و بدون اعشار نشان می‌دهد. برای مثال، اگر ولتاژ اندازه‌گیری شده 1.234 ولت باشد، مقدار mV به صورت 1234 نمایش داده می‌شود.

همچنین بررسی کنید

ساخت پروب اسیلوسکوپ برای اندازه گیری توان (قسمت دوم)

در قسمت یک، ما محدودیت های پهنای باند پروب های غیرفعال را پوشش دادیم و …

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *