این یک پروژهی بسیار چالشبرانگیز و در عین حال کاربردی است، زیرا میکروکنترلر 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();
}
توضیحات کد
- پینهای مشترک: در این کد، از پینهای PB0 و PB1 به صورت مشترک برای I2C (ADS1110) و پروتکل TM1637 استفاده شده است. این کار به دلیل محدودیت پینها در ATTiny45 است. این اشتراک پینها به این دلیل کار میکند که پروتکلهای این دو ماژول با هم متفاوت هستند و میتوان آنها را به صورت دستی مدیریت کرد.
- تابع
readADS1110(): این تابع مقدار ۱۶ بیتی را از ADS1110 میخواند.- ابتدا پیکربندی ماژول را با آدرس
0x8Cمینویسد. این پیکربندی، ADS1110 را برای دقت ۱۶ بیتی در حالت تکشات (Single-Shot) و با نرخ نمونهبرداری ۱۵ SPS تنظیم میکند. - سپس، برای اتمام تبدیل، یک تأخیر ۱۰۰ میلیثانیهای در نظر گرفته میشود.
- در نهایت، دو بایت (High Byte و Low Byte) از ماژول خوانده و به یک مقدار
int16_tتبدیل میشوند.
- ابتدا پیکربندی ماژول را با آدرس
- محاسبه ولتاژ: مقدار خام ADC به ولتاژ بر حسب میلیولت (mV) تبدیل میشود.
mV = (int16_t)((float)adcValue * (2048.0 / 32768.0) * 1000.0);2048.0ولتاژ مرجع داخلی ADS1110 (2.048V) است.32768.0نصف دامنه ۱۶ بیتی است که برای تبدیل به مقدار ولتاژ استفاده میشود.1000.0برای تبدیل ولت به میلیولت استفاده میشود.
- توابع 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 نمایش داده میشود.
سایت آموزشی الکترونیک و کامپیوتر اوپن مقاله های آموزشی الکترونیک و کامپیوتر و فن آوری