فرض کنید در حال طراحی یک دستگاه ثبت داده (دیتالاگر) یا ایستگاه پایش محیطی هستید که باید مقادیر آنالوگ حسگرها را در فواصل زمانی منظم اندازهگیری کند. اگر سیستم هر 1 ثانیه یک نمونه ثبت کند، در پایان یک شبانهروز بیش از 86000 رکورد تولید میشود. حافظهٔ موقت SRAM داخلی میکروکنترلر ما در تراشهٔ STM32F303CBT6 در مجموع 40 KiB (شامل 32 KiB حافظهٔ اصلی SRAM و 8 KiB حافظهٔ CCM) است و با قطع تغذیه به کلی پاک میشود. حافظهٔ Flash داخلی نیز با ظرفیت کلی 128 KiB بخش زیادی از فضای خود را به کدهای برنامه اختصاص میدهد و واحد پاکسازی آن به صورت صفحههای (Page) 2 KiB است که برای ثبت مکرر دادههای متغیر در نظر گرفته نشده است. در چنین کاربردهایی، استفاده از کارتهای حافظهٔ MicroSD امکان ذخیرهسازی چند گیگابایتی و خواندن مستقیم فایلها در رایانه را فراهم میکند. در این درس، پیادهسازی سیستم فایل ChaN FatFs را روی رابط ارتباطی SPI در غیاب کنترلر سختافزاری SDIO بررسی میکنیم و یک لاگر کاربردی با قالب متنی CSV میسازیم.
وضعیت اعتبارسنجی: کامپایلشده؛ پروژهٔ همراه با گزینههای -Wall -Werror بدون خطا و هشدار ساخته شده است. اجرای روی برد، آزمون رابط گرافیکی و اندازهگیری فیزیکی انجام نشدهاند. شکلهای مفهومی، مدلهای محاسباتی و مقادیر مورد انتظار، نتیجهٔ اندازهگیری نیستند.
پیشنیاز: درس 27؛ برای انتقال به RTOS، درسهای بخش FreeRTOS نیز لازماند.. سختافزار مبنا: STM32F303CBT6 در بستهٔ LQFP48 روی GB100ST V2.0. قطعات جانبی، تغذیه و روش اتصال در متن همین درس توضیح داده شدهاند.
چرا ذخیرهسازی روی کارت حافظه؟ و محدودیت سختافزاری STM32F303
پیش از ورود به جزئیات کدنویسی، باید تفاوت ماهوی میان حافظههای داخلی میکروکنترلر و حافظههای جانبی ذخیرهسازی را درک کنیم. حافظهٔ Flash داخلی STM32 برای نگهداری دستورالعملهای برنامه بهینهسازی شده است. پاککردن اطلاعات در حافظهٔ فلش داخلی STM32F303CBT6 تنها در قالب صفحههای (Page) 2 KiB امکانپذیر است و اعمال چرخههای مداوم نوشتن، فرسایش سریع این صفحهها را به همراه دارد. در مقابل، کارتهای حافظهٔ SD بر پایهٔ حافظههای فلش NAND چند گیگابایتی ساخته شدهاند و درون خود به یک پردازنده یا کنترلر FTL (Flash Translation Layer) مجهز هستند که عملیات توازن فرسایش (Wear Leveling) و مدیریت سکتورها را انجام میدهد.
نبود کنترلر سختافزاری SDIO در STM32F303 و لزوم بهرهگیری از مد SPI
در برخی میکروکنترلرهای دیگر (نظیر نمونههای انتخابی از خانوادهٔ STM32F4)، یک کنترلر سختافزاری اختصاصی برای رابط SDIO تعبیه شده است که تبادل اطلاعات را از طریق گذرگاه 4 بیتی با سرعت بالا فراهم میکند. اما هنگامی که مستندات مرجع فنی RM0316 میکروکنترلر STM32F303CBT6 را بررسی میکنیم، متوجه میشویم که این تراشه فاقد کنترلر سختافزاری SDIO است. استاندارد کارت حافظهٔ SD برای پوشش سیستمهای فاقد SDIO، یک حالت سازگاری به نام مد ارتباطی SPI (Serial Peripheral Interface) تعریف کرده است تا میکروکنترلر بتواند با 4 سیم استاندارد با کارت تبادل داده داشته باشد. هرچند مد SPI در بسیاری از کارتهای متداول SD و بهویژه کارتهای SDHC پشتیبانی میشود، اما سرعت آن نسبت به گذرگاه بومی SD پایینتر است.
سیمبندی ماژول کارت حافظه و الزامات الکتریکی در STM32F303
برای برقراری ارتباط در برد آموزشی GebraBit GB100ST V2.0، از واحد سختافزاری SPI1 استفاده میکنیم. پایههای تخصیصیافته به صورت زیر پیکربندی شدهاند:
- PA5: سیگنال کلاک (SPI1_SCK) با نگاشت تابع جایگزین AF5
- PA6: خروجی دادهٔ کارت به ورودی میکروکنترلر (SPI1_MISO) با نگاشت AF5
- PA7: خروجی دادهٔ میکروکنترلر به ورودی کارت (SPI1_MOSI) با نگاشت AF5
- PA4: پایهٔ انتخاب تراشه (Chip Select – CS) که به صورت خروجی عمومی GPIO با منطق فعال-پایین عمل میکند (با استفاده از برچسب SENSOR_CS پروژهٔ پایه).
نکتهٔ مهمی که باید در بستن مدار رعایت شود، سطح ولتاژ تغذیه و منطق است. خود کارت حافظهٔ SD در بازهٔ ولتاژی 2.7 V تا 3.6 V کار میکند و هرگز نباید ولتاژ 5 V مستقیماً به پایههای کارت متصل شود؛ وجود رگولاتور 5 V به 3.3 V روی ماژول نیز لزوماً به معنای سازگاری کامل سطح منطقی پایهها با 5 V نیست. مصرف جریان و پایداری ولتاژ به نوع کارت و ساختار ماژول وابسته است؛ کارت حافظه بهویژه هنگام عملیات نوشتن داخلی ممکن است جریانهای پالسی لحظهای (Write Bursts) بکشد. به همین دلیل، قرار دادن یک خازن دکوپلینگ موضعی (برای نمونه یک خازن 10 µF با کیفیت در نزدیکی سوکت) توصیه میشود تا از افت ولتاژ خط 3.3 V و ریست شدن کارت جلوگیری کند، اگرچه مقدار خازن بسته به مدار متغیر است و یک تضمین قطعی برای همهٔ شرایط به شمار نمیرود. همچنین قرار دادن یک مقاومت بالاکش (Pull-up) 10 kΩ روی خط CS تضمین میکند که در زمان بالا آمدن میکروکنترلر یا در وضعیت ریست، کارت ناخواسته فعال نشود. خطوط استفادهنشدهٔ DAT1 و DAT2 نیز مطابق طراحی سوکت باید در وضعیت مناسب قرار گیرند.


پروتکل راهاندازی کارت SD در حالت SPI و قیدهای فرکانسی
کارتهای حافظه پس از اتصال تغذیه، به طور خودکار در مد بومی SD Bus شروع به کار میکنند. برای هدایت کارت به مد SPI، یک دنبالهٔ دقیق از فرمانها بر اساس مشخصات پروتکل لایهٔ فیزیکی SD الزامی است. اگر این دنباله به درستی رعایت نشود، کارت به حالت SPI وارد نمیشود و راهاندازی شکست خواهد خورد.
دنبالهٔ همگامسازی اولیه و ارسال کلاکهای اولیه
در پروژهٔ ما، پس از اتصال تغذیه یک تأخیر اولیهٔ 10 ms با تابع HAL_Delay(10) در نظر گرفته شده است تا ولتاژ خط تغذیه کاملاً پایدار شود. سپس میکروکنترلر باید پایهٔ CS را در وضعیت منطقی بالا (غیرفعال) نگه دارد و با ارسال بایت 0xFF روی خط خروجی، حداقل 74 پالس کلاک بر روی خط SCK تولید کند (در کد ما 10 بایت برابر 80 پالس کلاک با خطوط CS و MOSI در وضعیت بالا ارسال میشود). این عمل مدار منطقی کارت را برای دریافت نخستین فرمان آماده میکند.
قید فرکانسی پروتکل در مرحلهٔ راهاندازی اولیه
بر اساس الزامات پروتکل استاندارد SD، تمامی مراحل شناسایی و راهاندازی اولیهٔ کارت حافظه باید در فرکانس کلاک حداکثر 400 kHz انجام پذیرد تا سازگاری الکتریکی در شروع به کار تضمین شود. کلاک مبنای واحد SPI1 در این پروژه 72 MHz است؛ بنابراین در تابع SPI1_Init مقدار تقسیمکنندهٔ کلاک (Prescaler) را روی 256 تنظیم میکنیم تا فرکانس کاری به 281.25 kHz برسد که به خوبی زیر سقف 400 kHz قرار دارد:
/* Authored override of the real lesson27 hardware baseline; external CS stays high. */
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
if (HAL_SPI_Init(&hspi1) != HAL_OK) Error_Handler();
مراحل تبادل فرمان در درایور اختصاصی sd_diskio.c به ترتیب زیر دنبال میشود:
- فرمان ریست (CMD0): پایهٔ CS پایین کشیده شده و فرمان CMD0 با آرگومان 0 و کد معتبر CRC (مقدار 0x95) ارسال میشود. دریافت پاسخ 0x01 (وضعیت In Idle State) نشان میدهد که کارت وارد مد SPI شده است.
- بررسی ولتاژ کاری و نسخه (CMD8): آرگومان 0x1AA با CRC معادل 0x87 فرستاده میشود. اگر پاسخ 0x01 باشد، کارت نسخهٔ SD v2.0 یا بالاتر است و 4 بایت دادهٔ پاسخ R7 خوانده میشود تا تطابق الگوی 0xAA و محدودهٔ ولتاژ تأیید گردد. در کارتهای قدیمی نسخه 1، پاسخ 0x05 (فرمان غیرمجاز) دریافت میشود.
- حلقهٔ مقداردهی (CMD55 + ACMD41): فرمان مقدماتی CMD55 به همراه فرمان اختصاصی ACMD41 (با تنظیم بیت HCS برای کارتهای نسخه 2 با مقدار 0x40000000) در یک حلقه تکرار میشود تا کارت آماده به کار شده و پاسخ آن 0x00 شود. در کد یک سقف زمانی 1500 ms با تأخیر 1 ms در هر دور برای جلوگیری از توقف بیپایان برنامه در نظر گرفته شده است.
- بررسی رجیستر شرایط عملیاتی OCR (فرمان CMD58): با ارسال CMD58، پاسخ رجیستر OCR خوانده میشود. بیت 31 نشاندهندهٔ پایان تغذیه و آماده بودن کارت است. همچنین بیت 30 (بیت CCS) نشان میدهد که آیا کارت ظرفیت بالا بوده و از آدرسدهی بلوکی استفاده میکند یا خیر.
- تنظیم اندازهٔ بلوک برای SDSC (فرمان CMD16): در صورتی که کارت از نوع SDSC باشد (آدرسدهی بایتی)، پیش از بالا بردن کلاک، فرمان CMD16 با آرگومان 512 ارسال میشود تا اندازهٔ بلوک دادهها صریحاً روی 512 بایت قفل گردد.
تفاوت آدرسدهی بایتی (SDSC) و بلوکی (SDHC/SDXC)
کارتهای قدیمی با ظرفیت استاندارد (SDSC با ظرفیت 2 GB و کمتر) از آدرسدهی بر مبنای بایت استفاده میکنند. در این کارتها برای خواندن سکتور شمارهٔ 2، باید نشانی بایتی 1024 (حاصل 2 ضربدر 512) ارسال شود. اما در کارتهای متداول SDHC و SDXC (ظرفیتهای 4 GB تا 32 GB و بالاتر)، آدرسدهی به صورت شمارهٔ مستقیم بلوک 512 بایتی (LBA) تعریف شده است؛ یعنی برای اشاره به سکتور 2، دقیقاً خود عدد 2 ارسال میشود. درایور ما پس از ارزیابی بیت CCS رجیستر OCR، متغیر block_addressed را مقداردهی کرده و در تابع کمکی Address محاسبات نشانی را تطبیق میدهد.
تنظیم کلاک کاری بالا پس از راهاندازی
پس از آماده شدن موفقیتآمیز کارت و پایان مقداردهی اولیه، کلاک واحد SPI دیگر به محدودهٔ 400 kHz محدود نیست. درایور مقدار پیشتقسیمکنندهٔ کلاک SPI را به 16 تغییر میدهد که فرکانس اسمی 4.5 MHz (حاصل 72 MHz تقسیم بر 16) را بر روی خط ارتباطی برقرار میکند. پیادهسازی کامل این مراحل در تابع disk_initialize آمده است:
DSTATUS disk_initialize(BYTE drive)
{
if (drive != 0) return STA_NOINIT;
status = STA_NOINIT; io_failed = 0; block_addressed = 0; g_sd_error = 0;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /* 281.25 kHz */
if (HAL_SPI_Init(&hspi1) != HAL_OK) return status;
Deselect(); HAL_Delay(10);
for (unsigned i = 0; i < 10; ++i) (void)Xfer(0xFF); /* >=74 clocks, CS high */
if (Command(0, 0) != 1) { Deselect(); g_sd_error = 4; return status; }
uint8_t v2 = 0; uint8_t response = Command(8, 0x1AA);
if (response == 1) {
uint8_t r7[4]; for (unsigned i = 0; i < 4; ++i) r7[i] = Xfer(0xFF);
if (r7[2] != 1 || r7[3] != 0xAA) { Deselect(); g_sd_error = 5; return status; }
v2 = 1;
} else if (response != 5) { Deselect(); g_sd_error = 6; return status; }
uint32_t start = HAL_GetTick();
do {
if (Command(55, 0) > 1) break;
response = Command(41, v2 != 0U ? 0x40000000UL : 0);
if (response == 0) break;
if (response != 1 || io_failed != 0U) break;
HAL_Delay(1);
} while ((uint32_t)(HAL_GetTick() - start) < 1500U);
if (response != 0 || io_failed != 0U) { Deselect(); g_sd_error = 7; return status; }
if (Command(58, 0) != 0) { Deselect(); g_sd_error = 8; return status; }
uint8_t ocr[4]; for (unsigned i = 0; i < 4; ++i) ocr[i] = Xfer(0xFF);
if ((ocr[0] & 0x80U) == 0U) { Deselect(); return status; }
block_addressed = (uint8_t)(v2 != 0U && (ocr[0] & 0x40U) != 0U);
if (block_addressed == 0U && Command(16, 512) != 0) { Deselect(); return status; }
Deselect();
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16; /* 4.5 MHz */
if (HAL_SPI_Init(&hspi1) != HAL_OK || io_failed != 0U) return status;
status = 0; return status;
}

معماری میانافزار FatFs و لایهٔ تطبیق درایور دیسک (diskio.c)
ثبت مستقیم دادهها بر روی سکتورهای خام کارت حافظه بدون سیستم فایل امکانپذیر است، اما در عمل دادهها باید در قالب فایلهای استاندارد ذخیره شوند تا در سیستمعاملهایی مانند ویندوز و لینوکس بدون نیاز به نرمافزار اختصاصی قابل خواندن باشند. برای ایجاد این ساختار، از میانافزار ChaN FatFs استفاده میکنیم.
معماری لایهای کتابخانهٔ ChaN FatFs و درایور diskio
کتابخانهٔ FatFs ساختاری مستقل از سختافزار دارد و از لایههای زیر تشکیل شده است:
- لایهٔ کاربرد (Application): توابع سطح بالای مدیریت فایل نظیر
f_mount،f_open،f_write،f_syncوf_closeکه در کد برنامه فراخوانی میشوند. - هستهٔ پردازش سیستم فایل (ff.c و ff.h): منطق محاسباتی سیستم فایل برای پردازش جدول تخصیص فایل (FAT)، شاخهها، کلاسترها و بافرهای سکتور.
- لایهٔ رابط سختافزار (diskio.c و diskio.h): یک قرارداد واسط که شامل 5 تابع اساسی است و اتصال هستهٔ FatFs به رسانهٔ فیزیکی را تعریف میکند.
- درایور فیزیکی سختافزار: توابع HAL SPI میکروکنترلر که ارسال و دریافت الکتریکی بایتها روی پینها را انجام میدهند.
قرارداد توابع پنجگانهٔ diskio در درایور sd_diskio.c
هستهٔ FatFs ارتباط مستقیمی با رجیسترهای STM32 ندارد، بلکه از طریق 5 تابع رابط درایور با دیسک تبادل میکند که در فایل sd_diskio.c پروژه پیادهسازی شدهاند:
disk_initialize: درایو مشخصشده (درایو 0) را با ارسال دنبالهٔ فرمانهای SPI راهاندازی میکند و وضعیت درایو (DSTATUS) را بازمیگرداند.disk_status: وضعیت ذخیرهشدهٔ درایو را گزارش میدهد (آیا دیسک مقداردهی شده یا بیتSTA_NOINITفعال است). توجه داشته باشید که این وضعیت صرفاً یک متغیر نرمافزاری کششده است و درایور فاقد پایههای سختافزاری تشخیص حضور کارت یا حفاظت در برابر نوشتن است؛ همچنین قابلیت تعویض گرم (Hot-plug) وجود ندارد و در صورت خروج کارت یا بروز خطای رسانه، سیستم باید ریست یا مجدداً راهاندازی شود.disk_read: سکتورهای 512 بایتی را با استفاده از فرمان تکبلوکی CMD17 در یک حلقه از کارت میخواند (از فرمان چندبلوکی بهینهشدهٔ CMD18 استفاده نشده است). در مد پیشفرض SPI کارتها، اعتبارسنجی خطای CRC دادهها خاموش است و دو بایت CRC انتهای بلوک خوانده اما صرفنظر میشوند.disk_write: سکتورهای 512 بایتی را با استفاده از فرمان تکبلوکی CMD24 در یک حلقه روی کارت مینویسد، توکن پاسخ داده (0x05) را اعتبارسنجی میکند و با تابعReady(500)منتظر رفع مشغولی کارت میماند.disk_ioctl: دستورات کنترلی متفرقه را پردازش میکند:CTRL_SYNC: پایهٔ CS را فعال کرده و منتظر اتمام مشغولی کارت میماند (حداکثر 500 ms)؛ این دستور پایان نوشتن جاری را بررسی میکند اما تضمین اتمیک ذخیرهسازی دائمی در برابر قطع برق نیست.GET_SECTOR_SIZE: اندازهٔ ثابت سکتور را برابر 512 بایت گزارش میدهد.GET_SECTOR_COUNT: رجیستر CSD را با فرمان CMD9 میخواند و تعداد کل سکتورهای کارت را بر اساس نسخهٔ CSD محاسبه میکند.- دستورات
GET_BLOCK_SIZEوCTRL_TRIMبه دلیل غیرفعال بودن قابلیت قالببندی و عدم نیاز به TRIM مقدارRES_PARERRبازمیگردانند.
مدیریت میانافزار و ادغام دستی ChaN FatFs R0.16
در این پروژه، به جای تکیه بر قالبهای خودکار، از نسخهٔ رسمی ChaN FatFs R0.16 (منتشرشده در تاریخ 2025-07-22) استفاده شده است. فایلهای هستهٔ FatFs به صورت دستی درون ساختار ساخت CMake قرار گرفته و درایور sd_diskio.c بر پایهٔ مجوز MIT توسعه یافته است؛ متن مجوز اصلی BSD یکبندی ChaN در سرآیند فایلهای رسمی و فایل LICENSE.txt پروژه حفظ شده است. ادغام تمیز و کامپایل بدون خطای پروژه، اثبات ساختار نرمافزاری صحیح است اما به معنای گواهینامهٔ کیفیت صنعتی یا تضمین کارکرد در محیطهای بحرانی نیست.

مدیریت فایل و ایمنی دادهها در برابر قطع ناگهانی تغذیه
برای ثبت دادهها روی کارت حافظه، مدیریت چرخهٔ حیات فایل در کد برنامه الزامی است. این چرخه شامل مراحل مشخصی است:
- اتصال سیستم فایل (Mounting): با تابع
f_mount(&filesystem, "0:", 1)انجام میشود. آرگومان سوم با مقدار 1 بررسی فوری فایلسیستم را فعال میکند. برنامهٔ ما به صورت صریح شرطfilesystem.fs_type == FS_FAT32را بررسی میکند؛ در صورتی که دیسک با ساختار دیگری (مانند FAT12 یا FAT16) فرمت شده باشد، برنامه کدFR_NO_FILESYSTEMرا برمیگرداند. - ایجاد یا باز کردن فایل: با تابع
f_openو پرچمهای مربوطه صورت میپذیرد. - نوشتن رکوردها: دادهها از طریق تابع
f_writeبه بافرهای درایور تحویل میشوند. - تخلیه و همگامسازی (Sync): تابع
f_syncدادههای بافرشده در رم را در کارت ثبت کرده و اطلاعات دایرکتوری را بهروز میکند. - بستن نهایی فایل: با تابع
f_closeفایل بسته شده و منابع آزاد میگردند.
تحلیل ریسک قطعی ناگهانی برق و محدودیت تابع f_sync
یکی از مفاهیم کلیدی در کار با حافظههای فلش، درک نقش واقعی تابع f_sync است. این تابع بافرهای سکتور نگهداریشده در حافظهٔ RAM میکروکنترلر را به رسانهٔ کارت منتقل میکند و اندازهٔ فایل و موقعیت کلاسترها را در جدول دایرکتوری بهروزرسانی مینماید. در نتیجه، پنجرهٔ زمانی نگهداری دادههای ثبتنشده در رم به حداقل میرسد.
با این حال، درون کارت حافظه، کنترلر داخلی (FTL) مسئولیت مدیریت تراشههای فیزیکی NAND را بر عهده دارد. اندازهٔ بلوک پاکسازی فیزیکی حافظهٔ فلش NAND عموماً بسیار بزرگتر از یک سکتور 512 بایتی است. کنترلر داخلی کارت برای نگارش دادهها ممکن است بافرها را کش کرده، بلوکهای ناند را بازنگاشت کند یا جداول متادیتای داخلی خود را تغییر دهد. در صورتی که ولتاژ تغذیه دقیقاً در لحظهٔ بهروزرسانی فیزیکی بلوکهای ناند یا متادیتای داخلی کارت قطع شود، خطر خرابی فایل، ناقص ماندن سکتورها یا آسیب دیدن ساختار فایلسیستم وجود خواهد داشت. بنابراین، فراخوانی f_sync پنجرهٔ خطر را کوچکتر میکند، اما یک راهکار اتمیک برای مقاومت کامل در برابر قطع ناگهانی تغذیه نیست. طراحی مهندسی مناسب نیازمند بستن منظم فایل پیش از قطع تغذیه و فراهم کردن منبع تغذیهٔ پایدار و بدون افت ناگهانی است.
قالببندی دیسک در رایانه و سیاست عدم قالببندی در سورس کد
در پیکربندی ffconf.h این پروژه، قابلیت قالببندی خودکار از درون سورس کد غیرفعال شده است (FF_USE_MKFS = 0). فعال بودن فرمت در فرمور سیستم نهفته ریسک پاک شدن اشتباهی اطلاعات کارت کاربر را به دنبال دارد و بخشی از حافظهٔ محدود Flash و RAM را به کدهای حجیم ساخت جداول پارتیشن اختصاص میدهد. رویکرد صحیح این است که کاربر از یک کارت حافظهٔ استاندارد SDHC (با ظرفیت 4 GB تا 32 GB) استفاده کند که از قبل در رایانه با قالب استاندارد FAT32 قالببندی شده باشد. همچنین توصیه میشود همیشه پیش از آزمایش، از اطلاعات موجود روی کارت نسخهٔ پشتیبان تهیه شود.
آزمایش عملی: پیادهسازی دیتالاگر ADC و ثبت در فایل CSV
در این بخش، یک دیتالاگر با ساختار بدون سیستمعامل (Bare-metal) پیادهسازی میکنیم که مقادیر آنالوگ روی پایهٔ PA1 (کانال ورودی ADC1_IN2) را به صورت دورهای نمونهبرداری کرده و با فرمت متنی مقادیر جداشده با کاما (CSV) در کارتی با نامهای ترتیبی 0:ADC000.CSV تا 0:ADC999.CSV ذخیره میکند.
ساختار دیتالاگر در برنامهٔ app.c و نمونهبرداری از PA1
اجرای برنامه در حلقهٔ اصلی به صورت مسدودکننده (Blocking) و بر مبنای بهترین تلاش (Best-effort) صورت میپذیرد. در زمان راهاندازی، تابع App_Init تنها کالیبراسیون واحد ADC را با دستور HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED) اجرا میکند. ولتاژ ورودی آنالوگ باید همواره در محدودهٔ 0 تا VDDA (3.3 V) باشد و هرگز نباید ولتاژ 5 V به این پایه متصل گردد. برای حفظ ایمنی، تا زمانی که کاربر متغیر کنترلی g_start_request = 1 را درون دیباگر تنظیم نکند، هیچگونه دسترسی یا نوشتنی روی کارت حافظه آغاز نمیشود.
پیادهسازی تابع StartLog را بررسی کنید:
static void StartLog(void)
{
g_rows = 0; g_partial_write = 0; g_stop_request = 0;
g_fs_result = f_mount(&filesystem, "0:", 1);
if (g_fs_result != FR_OK) return;
if (filesystem.fs_type != FS_FAT32) { g_fs_result = FR_NO_FILESYSTEM; return; }
for (unsigned file = 0; file < 1000; ++file) {
(void)snprintf(g_log_name, sizeof(g_log_name), "0:ADC%03u.CSV", file);
g_fs_result = f_open(&logfile, g_log_name, FA_WRITE | FA_CREATE_NEW);
if (g_fs_result == FR_EXIST) continue;
break;
}
if (g_fs_result != FR_OK) return;
g_log_active = 1;
static const char header[] = "uptime_ms,adc_raw,estimated_mv\r\n";
if (!WriteChecked(header, sizeof(header) - 1U)) {
g_close_result = f_close(&logfile); g_log_active = 0; return;
}
g_fs_result = f_sync(&logfile);
if (g_fs_result != FR_OK) { g_close_result = f_close(&logfile); g_log_active = 0; return; }
last_sample = HAL_GetTick();
}
در این تابع، پرچم FA_CREATE_NEW مانع از بازنویسی عمدی روی فایلهای همنام موجود میشود؛ حلقه از شمارهٔ 0 تا 999 جستجو میکند تا نخستین نام استفادهنشده را پیدا کند. اگر فایلی با همان نام وجود داشته باشد، با دریافت FR_EXIST شمارهٔ بعدی بررسی میشود. توجه داشته باشید که این پرچم تضمینی برای خالی بودن فایل شمارهٔ 000 ایجاد نمیکند و در صورتی که هر 1000 فایل روی کارت وجود داشته باشند، حلقه با خطای FR_EXIST متوقف خواهد شد. پس از باز شدن موفق فایل، سطر عنوان uptime_ms,adc_raw,estimated_mv در ابتدای فایل نوشته و با f_sync ثبت میگردد.
چرخهٔ باز کردن، نوشتن دورهای دادههای CSV و بستن فایل
تابع App_Poll در حلقهٔ اصلی فراخوانی میشود. فاصلهٔ زمانی نمونهبرداری با استفاده از آپتایم پردازنده (تابع HAL_GetTick()) سنجیده میشود. این زمانبندی بر مبنای بهترین تلاش است؛ در صورت بروز تاخیر در عملیات I/O دیسک یا عدم پاسخگویی فوری کارت یا خطای نمونهبرداری ADC، فاصلهٔ زمانی بین دو ردیف ممکن است از 1000 ms فراتر رود. مقدار تخمینی ولتاژ با فرض خط تغذیهٔ اسمی 3300 mV و رابطهٔ عدد صحیح زیر محاسبه میشود که کالیبراسیون دقیق ولتاژ مرجع محسوب نمیشود و پلهٔ کوانتیزاسیون آن معادل VDDA بر 4096 است:
g_sample_mv = (g_sample_raw * 3300U + 2047U) / 4095U;
تابع WriteChecked صحت عملیات نوشتن را ارزیابی میکند؛ این تابع هم کد بازگشتی g_fs_result و هم تطابق تعداد بایتهای نوشتهشده با طول درخواستی (written == size) را بررسی مینماید تا نوشتارهای ناقص (مانند پر شدن حجم دیسک) را با فعال کردن g_partial_write = 1 آشکار کند. پایهٔ PB6 متصل به LED کاربر به ازای هر ردیف معتبر تغییر وضعیت میدهد (Toggle)، بنابراین یک دورهٔ کامل خاموش و روشن شدن الایدی اسماً حدود 2 ثانیه زمان میبرد. برای برقراری توازن میان سرعت و ماندگاری داده، f_sync هر 10 ردیف یک بار فراخوانی میشود و برنامه پس از ثبت 100 ردیف معتبر یا در صورت تنظیم پرچم g_stop_request، تابع Finish را صدا میزند:
static int WriteChecked(const char *text, UINT size)
{
UINT written = 0;
g_fs_result = f_write(&logfile, text, size, &written);
if (g_fs_result != FR_OK || written != size) {
if (written != size) g_partial_write = 1;
return 0;
}
return 1;
}
قطعهٔ بعدی از تابع حلقهٔ برنامه در همان فایل انتخاب شده است. توابع بین این دو قطعه در پروژهٔ کامل قرار دارند.
void App_Poll(void)
{
if (g_start_request != 0U && g_log_active == 0U) { g_start_request = 0; StartLog(); }
if (g_log_active == 0U) return;
if (g_stop_request != 0U || g_rows >= 100U) { Finish(); return; }
uint32_t now = HAL_GetTick();
if ((uint32_t)(now - last_sample) < 1000U) return;
last_sample = now;
HAL_StatusTypeDef result = HAL_ADC_Start(&hadc1);
if (result == HAL_OK) result = HAL_ADC_PollForConversion(&hadc1, 10);
if (result == HAL_OK) g_sample_raw = HAL_ADC_GetValue(&hadc1);
HAL_StatusTypeDef stopped = HAL_ADC_Stop(&hadc1);
if (result != HAL_OK || stopped != HAL_OK) { ++g_adc_failures; return; }
g_sample_mv = (g_sample_raw * 3300U + 2047U) / 4095U;
char line[64];
int size = snprintf(line, sizeof(line), "%lu,%lu,%lu\r\n",
(unsigned long)now, (unsigned long)g_sample_raw, (unsigned long)g_sample_mv);
if (size < 0 || (unsigned)size >= sizeof(line)) { Finish(); return; }
if (!WriteChecked(line, (UINT)size)) {
g_close_result = f_close(&logfile); g_log_active = 0; return;
}
++g_rows; HAL_GPIO_TogglePin(USER_LED_GPIO_Port, USER_LED_Pin);
if ((g_rows % 10U) == 0U) {
g_fs_result = f_sync(&logfile);
if (g_fs_result != FR_OK) { g_close_result = f_close(&logfile); g_log_active = 0; }
}
}
مراحل گامبهگام آزمایش با دیباگر و رفتار مورد انتظار
مراحل اجرای آزمایش با دیباگر و رفتار مورد انتظار برنامه به شرح زیر است:
- اتصال سختافزار: پایههای SPI1 و خط تغذیهٔ 3.3V ماژول MicroSD را با سیمهای کوتاه به برد GebraBit متصل کنید و اتصال زمین مشترک (GND) را بررسی نمایید. خروجی یک پتانسیومتر آنالوگ متصل به 3.3V را به پایهٔ PA1 متصل کنید.
- آمادهسازی کارت: یک کارت SDHC (ظرفیت 4 GB تا 32 GB) را که از قبل در رایانه با فرمت FAT32 قالببندی شده و اطلاعات مهم آن پشتیبانگیری شده است، درون ماژول قرار دهید.
- کامپایل و بارگذاری: پروژه را تحت کامپایلر Arm GNU Toolchain بیلد کرده و فایل باینری را روی میکروکنترلر پروگرام کنید.
- تنظیم متغیرها در دیباگر: محیط دیباگ را آغاز کنید. در پنجرهٔ Live Expressions متغیرهای
g_start_request،g_stop_request،g_log_active،g_rows،g_sample_mv،g_fs_result،g_close_resultوg_partial_writeرا اضافه نمایید. مقدارg_start_requestرا برابر 1 قرار داده و برنامه را ادامه (Resume) دهید. - رفتار مورد انتظار حین ثبت: انتظار میرود متغیر
g_log_activeبرابر 1 شود، نام اولین فایل درg_log_nameتخصیص یابد و با ثبت هر ردیف معتبر، پایهٔ PB6 تغییر وضعیت دهد. در صورت تغییر وضعیت پتانسیومتر، مقدارg_sample_mvدر فواصل زمانی حدود 1000 ms بهروزرسانی میشود. پس از ثبت 100 ردیف، تابعFinishبه صورت خودکار فراخوانی شده و مقدارg_log_activeبه 0 تغییر مییابد. - خروج ایمن کارت حافظه: پیش از خارج ساختن فیزیکی کارت از سوکت، حتماً در دیباگر بررسی کنید که
g_log_active == 0باشد و کدهایg_fs_resultوg_close_resultهر دو مقدارFR_OKرا نشان دهند تا اطمینان حاصل شود که هیچ عملیات تبادل دیسکی در جریان نیست. سپس تغذیه را قطع کرده یا کارت را با احتیاط خارج نمایید و فایل ثبتشده را در رایانه مشاهده کنید.


جدول کدهای خطای FatFs، عیبیابی و آزمایشهای اعتبارسنجی
هنگام تعامل با سیستم فایل و درایور دیسک، بررسی کدهای وضعیت FRESULT و متغیرهای کنترلی برنامه برای عیبیابی دقیق اشکالات الزامی است:
کدهای خطای FRESULT و ریشهیابی خطاهای دیسک
| کد خطا (FRESULT) | نام نمادین | علت فنی و محل وقوع | اقدام اصلاحی مهندسی |
|---|---|---|---|
| 0 | FR_OK | عملیات با موفقیت کامل به پایان رسید. | نیاز به اقدام اصلاحی ندارد. |
| 1 | FR_DISK_ERR | خطای سطح پایین سختافزاری در توابع disk_read یا disk_write رخ داده است. | بررسی اتصالات فیزیکی SPI، کوتاه کردن سیمها، بررسی پایداری تغذیهٔ 3.3V و خازن دکوپلینگ ماژول. |
| 3 | FR_NOT_READY | رسانهٔ ذخیرهسازی آماده به کار نیست و مقداردهی اولیه با شکست مواجه شده است. | بررسی فرکانس کلاک اولیه (حتماً زیر 400 kHz باشد)، مقاومت بالاکش پایهٔ CS و زمان تأخیر تثبیت تغذیه. |
| 8 | FR_EXIST | فایلی با همان نام از قبل روی دیسک وجود دارد. | رفتار طبیعی هنگام اسکن نامهای تکراری؛ حلقه به طور خودکار شمارهٔ فایل بعدی را بررسی میکند. |
| 9 | FR_INVALID_OBJECT | اشارهگر ساختار فایل نامعتبر است یا فایل قبلاً بسته شده است. | اطمینان از فراخوانی موفق f_open پیش از اقدام به نوشتن و عدم فراخوانی f_write پس از بستن فایل. |
| 13 | FR_NO_FILESYSTEM | پارتیشن معتبر FAT32 روی کارت حافظه یافت نشد. هستهٔ FatFs از FAT12 و FAT16 نیز پشتیبانی میکند، اما کد این پروژه صریحاً ساختار FS_FAT32 را الزامی میداند. | بررسی اتصالات فیزیکی و تغذیه؛ تهیهٔ نسخهٔ پشتیبان از دادهها و قالببندی استاندارد کارت به صورت FAT32 در رایانه. |
جدول عیبیابی مشکلات سختافزاری و نرمافزاری
متغیرهای کنترلی تعبیهشده در برنامه علتهای احتمالی وقوع اشکالات را مشخص میکنند:
g_sd_error = 4: کارت در پاسخ به فرمان CMD0 پاسخ 0x01 را بازنگردانده است. علتهای محتمل شامل جابهجایی یا قطعی سیمهای MOSI، MISO یا SCK، قطعی خط CS، نبود زمین مشترک (GND)، عدم پایداری تغذیهٔ 3.3V یا عدم پاسخگویی کارت است.g_sd_error = 7: حلقهٔ مقداردهی ACMD41 به وضعیت آماده نرسیده است. این حالت میتواند ناشی از به پایان رسیدن مهلت زمانی حلقهٔ بیرونی 1500 ms، دریافت پاسخ نامعتبر به CMD55/ACMD41 یا خطای تبادل I/O در سطح SPI باشد. توجه داشته باشید که مهلت زمانی 1500 ms صرفاً سقف حلقه است و توابع زیرین نیز دارای مهلت زمانی محدود هستند.g_partial_write = 1: تابعWriteCheckedمتوجه شده که تعداد بایتهای نوشتهشده با طول درخواستی مطابقت ندارد (مثلاً در صورت پر شدن ظرفیت کارت حافظه). این متغیر صرفاً وقوع نوشتن ناقص را آشکار میکند و نمیتواند بایتهای قبلی را بازگردانی کرده یا سلامت فایلسیستم را تضمین نماید. در زمان اشکالزدایی باید مقادیرg_fs_result،g_close_resultوg_partial_writeبه صورت مجزا ارزیابی شوند.
وضعیت اعتبارسنجی و محدودیتهای پیادهسازی
این پروژه در محیط Windows با کامپایلر Arm GNU Toolchain 13.2.rel1 تحت پروفایل Debug با پرچمهای سختگیرانهٔ -O0 -g3 -Wall -Werror به طور کامل و با صفر اخطار (Zero Warnings) کامپایل شده است. فایل باینری خروجی و میزان مصرف حافظه (30120 بایت حافظهٔ Flash و 3368 بایت حافظهٔ RAM تخصیصیافته شامل رزرو پشته و هیپ) بررسی گردیده است. با این حال، به منظور حفظ صداقت علمی اعلام میشود که این اعتبارسنجی صرفاً مربوط به کامپایل، بررسی باینری و ساختار کد است؛ آزمون فیزیکی اجرای کد روی برد واقعی، ثبت عملی فایل روی کارت حافظه، بررسی فایل CSV در رایانه یا تحلیل شکل موجها با اسیلوسکوپ در این مرحله انجام نگرفته است.
پرسشهای خودآزمایی
1. چرا پروتکل SD الزام میکند که کلاک راهاندازی اولیهٔ کارت حداکثر 400 kHz باشد و در این پروژه چه مقداری تنظیم شده است؟
طبق مشخصات پروتکل فیزیکی کارت حافظه (SD Physical Layer)، تمام مراحل اولیهٔ شناسایی و راهاندازی کارت باید در فرکانس کلاک حداکثر 400 kHz انجام شوند تا تمام کارتها در وضعیت پیشفرض بتوانند دستورها را دریافت و پردازش کنند. در این پروژه، با تقسیم کلاک 72 MHz واحد SPI1 بر ضریب 256، فرکانس 281.25 kHz برقرار میشود. پس از دریافت پاسخ آماده (0x00) از ACMD41 و بررسی رجیستر OCR، کلاک به 4.5 MHz افزایش مییابد.
2. تفاوت نشانیدهی بایتی در SDSC و نشانیدهی بلوکی در SDHC چیست و درایور چگونه آن را مدیریت میکند؟
در کارتهای استاندارد قدیمی (SDSC تا 2 GB)، نشانی در دستورهای خواندن (CMD17) و نوشتن (CMD24) بر مبنای بایت است؛ بنابراین برای سکتور شمارهٔ 2، نشانی عددی 1024 ارسال میشود. اما در کارتهای SDHC و SDXC (از 4 GB تا 32 GB و بالاتر)، نشانیدهی بر مبنای بلوکهای 512 بایتی است و خود شمارهٔ سکتور (2) به عنوان آرگومان فرستاده میشود. در درایور sd_diskio.c، با خواندن بیت 30 رجیستر OCR مقدار block_addressed تعیین شده و در تابع Address ضرب در 512 اعمال یا صرفنظر میشود. همچنین برای کارتهای SDSC پیش از افزایش کلاک، اندازهٔ بلوک با دستور CMD16 روی 512 تنظیم میگردد.
3. آیا فراخوانی مکرر تابع f_sync سلامت قطعی دادهها را در برابر قطع ناگهانی برق تضمین میکند؟
خیر. تابع f_sync بافرهای رم میکروکنترلر را در کارت تخلیه کرده و جداول FAT و اندازهٔ فایل را در دایرکتوری بهروز میکند؛ بنابراین پنجرهٔ زمانی نگهداری دادههای ثبتنشده در رم را کوچک میسازد. اما کنترلر داخلی کارت (FTL) وظیفهٔ نگاشت و نوشتن بر روی فلش NAND را بر عهده دارد که واحد پاکسازی آن بسیار بزرگتر از 512 بایت است. اگر قطع برق دقیقاً حین بهروزرسانی بلاکهای ناند یا متادیتای داخلی کارت رخ دهد، دادهها یا ساختار فایلسیستم ممکن است آسیب ببینند. بنابراین f_sync کاهشدهندهٔ پنجرهٔ ریسک است، نه یک تضمین تراکنشی یا اتمیک.
4. چرا متغیر پیکربندی FF_USE_MKFS در این پروژه برابر 0 تنظیم شده و میکروکنترلر کارت را قالببندی نمیکند؟
تنظیم FF_USE_MKFS = 0 در فایل ffconf.h تابع f_mkfs را از بیلد حذف میکند. این کار از حذف ناخواستهٔ اطلاعات کارت کاربر در اثر خطای نرمافزاری جلوگیری کرده و از مصرف بیهودهٔ حافظهٔ Flash و RAM برای کد فرمتکننده جلوگیری مینماید. در طراحی سیستمهای نهفته، روش استاندارد این است که کارت حافظهٔ SDHC (4 GB تا 32 GB) از پیش در رایانه با سیستم فایل FAT32 قالببندی شود و دادههای قبلی پشتیبانگیری شده باشند.
5. رفتار تابع StartLog در مواجهه با فایلهای موجود روی کارت چگونه است و آیا دسترسی به 0:ADC000.CSV قطعی است؟
تابع StartLog با استفاده از پرچم FA_CREATE_NEW نامهای 0:ADC000.CSV تا 0:ADC999.CSV را به ترتیب جستجو میکند. اگر نامی از قبل وجود داشته باشد، خطای FR_EXIST رخ داده و حلقه به سراغ شمارهٔ بعدی میرود؛ در نتیجه فایلهای قبلی بازنویسی عمدی نمیشوند. هیچ تضمینی وجود ندارد که 0:ADC000.CSV حتماً خالی باشد. اگر هر 1000 نام از پیش پر شده باشند، حلقه با خطای FR_EXIST متوقف شده و عملیات لاگ شروع نمیشود. همچنین این پرچم محافظتی در برابر خرابی فایلسیستم بر اثر قطع برق ایجاد نمیکند.
6. مفهوم متغیرهای g_partial_write و g_sd_error چیست و هنگام بروز خطا چگونه باید بررسی شوند؟
تابع WriteChecked هم کد بازگشتی FRESULT و هم تعداد بایت نوشتهشده (written == size) را بررسی میکند. اگر اندازهٔ نوشتهشده کمتر از درخواست باشد (مثلاً پر شدن کارت)، متغیر g_partial_write = 1 میشود تا وقوع نوشتن ناقص آشکار شود؛ هرچند بایتهای نوشتهشده قبلی بازگردانی نمیشوند. متغیر g_sd_error نیز علتهای احتمالی خطای راهاندازی را ثبت میکند (مثلاً 4 برای عدم دریافت پاسخ به CMD0 به دلیل سیمبندی یا تغذیه، و 7 برای عدم اتمام موفقیتآمیز حلقهٔ ACMD41 ناشی از مهلت زمانی، پاسخ نامعتبر یا خطای I/O). این متغیرها باید همراه با g_fs_result و g_close_result به صورت جداگانه در دیباگر ارزیابی شوند.
تمرینها
تمرینهای تغییر کد، پیشنهاد آموزشیاند؛ نسخهٔ تغییریافته باید دوباره کامپایل و جداگانه روی برد بررسی شود.
1. تمرین 1 (مقدماتی): افزودن شمارهٔ ردیف به سطر خروجی CSV
خط قالببندی رشتهٔ CSV در تابع App_Poll را به نحوی تغییر دهید که شمارهٔ ردیف فعلی (g_rows + 1) به عنوان ستون اول اضافه شود (فرمت row,uptime_ms,adc_raw,estimated_mv). همچنین سطر عنوان (header) در تابع StartLog را متناسب با ستون جدید بهروزرسانی کنید.
معیار بررسی: اندازهٔ آرایهٔ line[64] را بررسی کنید تا مطمئن شوید مقدار بازگشتی snprintf منفی نبوده و رشته به دلیل طولانیتر شدن بریده یا سرریز نمیشود ((unsigned)size < sizeof(line)). کد پروژه باید مجدداً کامپایل شود.
2. تمرین 2 (متوسط): خاتمهٔ منظم عملیات ثبت با کلید ورودی و پرچم g_stop_request
یک کلید ورودی به میکروکنترلر اضافه کنید که با اعمال فیلتر نرمافزاری ضدلرزش (Debounce)، هنگام فشردن کلید متغیر g_stop_request را برابر 1 قرار دهد. عملکرد بسته شدن منظم فایل توسط تابع Finish و ذخیرهٔ نتیجه در g_close_result را بررسی کنید.
معیار بررسی: دقت کنید که درخواست توقف تنها روند بستن فایل را آغاز میکند و مصونیت مطلق فیزیکی در برابر قطع ناگهانی تغذیه ایجاد نمیکند؛ پس از توقف، باید g_log_active == 0 و هر دو نتیجهٔ g_fs_result == FR_OK و g_close_result == FR_OK تأیید شوند. برنامه باید با موفقیت کامپایل گردد.
3. تمرین 3 (پیشرفته): تحلیل بار گذرگاه SPI با تغییر بسامد f_sync
در تابع App_Poll، بسامد فراخوانی f_sync را از هر 10 ردیف به هر 1 ردیف یا هر 50 ردیف تغییر دهید. محاسبه کنید که چه تعداد تراکنش اضافی SPI برای نوشتن سکتورهای جدول FAT و دایرکتوری ایجاد میشود و چگونه بافرینگ دادهها در رم میتواند سربار دستورهای SPI را تعدیل کند.
معیار بررسی: توجه داشته باشید که طول عمر واقعی فلش به رفتار داخلی FTL کارت بستگی دارد و نمیتوان فرسایش را صرفاً از روی بسامد f_sync محاسبه کرد. هرگونه تغییر در کد باید کامپایل شده و در گزارش آزمایش منعکس گردد.
منابع و مبنای بررسی
مبنای فنی این درس، اسناد رسمی و نسخهٔ مشخص کتابخانهها و پروژهٔ قابل دریافت آن است. نسخهها و محدودیتها در راهنمای فایلهای همراه ثبت شدهاند.
- Official FatFs archive and R0.16 source
- FatFs application note
- FatFs f_sync
- ChaN How to Use MMC/SDC
- SD Association simplified specification index
دانلود پروژه و فایلهای همراه
دانلود بستهٔ کامل پروژه، خروجی ساخت، اسکریپتها و 6 تصویر آموزشی · راهنمای اجرا · پوشهٔ همین نسخه در گیتهاب
حجم بسته: 2,061,170 بایت. شناسهٔ SHA256: 15ea006e44209d14f4bacabf94efac42a1142bf6aebed48f9ed96c2ea99b9fd0. گزارش ساخت و برگهٔ خالی ثبت آزمایش در بسته قرار دارد.