1.

Lập trình LCD 16×2 ở chế độ 4-bits với STM32
Chương trình chạy trên kít OPENCMX-STM3210D; hiển thị dòng chữ “Sharing
Tech in ARM Viet Nam”. Ví dụ mẫu này sẽ nêu lên vài điểm cần chú ý khi giao
tiếp LCD 16×2 ở chế độ 4-bits.

Các chú ý quan trọng:
- Các chân D0-D3 được để trống hoặc kết nối xuống GND.
- Vì mặc định LCD hoạt động mode 8 bit, do đó khi mới khởi động LCD sẽ vào
chế độ 8 bit nên nếu ta thiết lập cho LCD mode 4-bit ngay từ đầu thì LCD sẽ
không hiểu. Do đó để LCD hiểu ta muốn giao tiếp ở mode 4-bit thì cần một lệnh
giả lập cho LCD, bằng cách gửi lệnh (như bên dưới) trước khi gọi
hàm LCD_Function_Set():
/* Set 4-bits interface */
lcd_Control_Write(0×33);
Delay(10);
lcd_Control_Write(0×32);
Lưu ý là điều này còn tùy vào loại LCD( một số LCD cũng không cần tới lệnh giả
này mà chỉ cần 1 delay() nhỏ thì cũng ok)
- Cần chú ý đến quá trình thao tác đến 4 đường Data trong
hàm lcd_data_line_write(u8 data) , có thể sẽ tác động đến các chân điều khiển nếu
dùng chung 1 Port ( Cần phải dùng các phép toán AND, OR thích hợp).
Tải chương trình nguồn ở đây. Ngoài ra có thể tham khảo thêm demo giao tiếp
LCD 16×2 ở chế độ 8-bits với STM32 ở đây.
2.Lập trình ADC
Ở bài này chúng ta sẽ tiếp cận cách sử dụng bộ ADC của STM32F103RC thông
qua các ứng dụng chuyển đổi giá trị tín hiệu tương tự sang số.
1 Môi trường phát triển phần cứng
1.1 Hỗ trợ ADC của GEM3v0.1
Ở board GEM3v0.1, có nhiều chân được thiết kế dành riêng cho các tác vụ tổng
hợp như: GPIOs, PWM, ADC, DAC, TIMER, Temperature sensor,…

Ở hình trên ta thấy chân số 14 của STM32F103x được thiết kế đa chức năng. Ở ví
dụ này, chúng ta sẽ sử dụng nó như là đầu vào dữ liệu cho bộ ADC.
Một số đặc tính cơ bản của bộ ADC của STM32F103x:
+ Độ phân giải 12-bit và tần suất lấy mẫu là 56MHz(khoảng 1us một mẫu).
+ 18 kênh chuyển đổi trong đó: 16 kênh dành cho tín hiệu ngoại được đánh số lần
lượt từ: AIN0,AIN1,…AIN15; 2 kênh còn lại dành cho cảm biến nhiệt nội và vôn
kế nội.
+ Bộ ADC được cấp nguồn riêng từ 2.4V đến 3.6V.
+ Hỗ trợ 2 loại chuyển đổi: regular, injected.
Ở board GEM31v0.1, bộ ADC được cấp nguồn trực tiếp 3.3V. Điện áp tham chiếu
Vref+ bằng 3.3V.
Độ phân giải 12bit cho phép mã hóa các tín hiệu tương tự từ 0->3.3 sang giá trị số
từ 0->4095. Giá trị lượng tử được tính bằng:

Quantizer = 3.3 / 2^12 = 3.3/4096 = 0.8mV

1.2 Cảm biến nhiệt LM35DZ
Cảm biến nhiệt LM35DZ gồm 3 chân:

Nguồn cung cấp cho chân +Vs vào khoảng từ 4V đến 30V. Ở chân xuất dữ liệu,
khi nhiệt độ bằng 0 điện áp ra sẽ là 0V. Cảm biến này đo nhiệt độ theo thang
Celcius. Cứ mỗi một đơn vị nhiệt độ sẽ tương ứng với 10mV. Và mức thay đổi là
tuyến tính. Ví dụ nếu nhiệt độ hiện giờ là 25 độ Celcius, thì điện áp xuất sẽ là
10mVx25 = 250mV. Như vậy, với giá trị lượng tử là 0.8mV, chúng ta hoàn toàn
có khả năng lấy mẫu chính xác giá trị từ cảm biến nhiệt.
Trong ứng dụng tích hợp với board GEM3v0.1, nguồn cấp cho cảm biến là 5V.
Sơ đồ mạch kết nối với STM32 được thiết kế như sau:

2 Cấu trúc chương trình
2.1 Sơ đồ khối

2.2 Cấu hình hoạt động ADC

Giải thích:
(1): Sử dụng hàm chuẩn thư viện CMSIS RCC_APB2PeriphClockCmd để kích
hoạt các ngoại vi trên APB2, ở đây ta kích hoạt ADC1:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
(2): Để nhận được dữ liệu dạng tương tự, ta phải cấu hình chân dữ liệu hoạt động
ở chế độ Analog Input. Ở demo này, ta sử dụng chân số 14 , tương ứng là chân
0(GPIO_Pin_0) của Port A(GPIOA) , ký hiệu là PA0
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
(3): Cấu hình chế độ hoạt động của ADC1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStructure);
(4): Cấu hình ADC Channel, ở ví dụ này, ta cấu hình ADC Channel 0
(ADC_Channel_0) hoạt đông như là Regular Channel với thời gian lấy mẫu là 55
chu kỳ
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1,
ADC_SampleTime_55Cycles5);
(5): Kích hoạt bộ ADC1
ADC_Cmd(ADC1, ENABLE);
(6): Kích hoạt Reset Calibration, khởi động lại bộ lấy mẫu chuẩn và chờ cho quá
trình tái khởi động hoàn tất
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
(7): Kích hoạt chế độ lấy mẫu và cũng chờ cho nó hoàn tất
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
(8): Kích hoạt chế độ chuyển đổi
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
2.3 Đọc dữ liệu từ LM35DZ
Chân dữ liệu của LM35DZ được nối trực tiếp tới chân PA0. Để đọc dữ liệu đã
được chuyển đổi bởi khối ADC1 ta sử dụng hàm
uint16_t adc = 0;
adc = ADC_GetConversionValue(ADC1);
Giá trị này đã được lượng tử hóa , do đó chúng ta phải tính toán lại giá trị thực
theo công thức:

Điện áp thực = Vref * (Giá trị lượng tử) / 2^n
= 3.3 * (giá trị lượng tử)/ 4096.

Giả sử giá trị chuyển đổi đọc từ ADC1 là 251, khi đó điện áp thực nhận từ
LM35DZ là:

Điện áp thực = 3.3 * 251 / 4096 = 202mV
Khi đó nhiệt độ thực sẽ là:
Nhiệt độ thực = Điện áp thực / 10 = 20.2 độ C
3. Tài nguyên chương trình
Download dự án ở đây.
Các bạn dùng cáp cổng COM nối board với máy tính để coi giá trị đọc từ cảm
biến.
3. Lập trình điều khiển MMC/SD card với board STM32-GEM3M trên nền
TOPPERS/ASP
MMC là viết tắt của MultiMedia Card. Đây là loại thẻ nhớ sử dụng bộ nhớ NAND
flash để lưu trữ dữ liệu được giới thiệu lần đầu vào năm 1997 bởi Siemens AG và
SanDisk. Đối với các ứng dụng nhúng ở mức vi điều khiển, MMC/SD card là sự
lựa chọn thích hợp cho các ứng dụng cần lưu trữ dữ liệu vì kết nối phần cứng với
thẻ MMC/SD
đơn giản, hỗ trợ giao tiếp SPI, đồng thời dung lượng bộ nhớ lớn(có thể lên tới
32GBs). Bộ nhớ của thẻ MMC/SD được tổ chức dạng block, tương tự như ổ
cứng(hardisk) trong máy tính. Do vậy cũng như ổ cứng, MMC/SD sử dụng tập
lệnh ATA để giao tiếp với phần mềm điều khiển.
Một số đặc điểm khi thẻ MMC/SD hoạt động ở chế độ SPI
+ Truyền dữ liệu đồng bộ trên 3 tín hiệu: CLK, DI, DO.
+ Kích hoạt thẻ thông qua tín hiệu : Chip Select(CS).
+ Tần số hoạt động từ 0-20MHz.
+ Hỗ trợ truy cập Single Block và Multi Block.
+ Sơ đồ kết nối
Thứ tự chân Chân kết nối Loại Mô tả
1 CS Input Chip Select
2 DI Input/Push Pull Data Input
3 VSS -
4 VDD -
5 BCLK Input Clock Source
6 VSS2 -
7 DO Output/Push Pull Data Output
8 -
9 -
Khi hoạt động ở chế độ SPI, các lệnh điều khiển và dữ liệu được truyển chung trên
2 tín hiệu DI và DO
Khi đọc dữ liệu từ thẻ




Khi ghi dữ liệu xuống thẻ


FATFS
FatFS là bộ mã nguồn miễn phí hỗ trợ định dạng FAT(File Allocation Table) được
sử dụng rộng rãi trong hệ điều hành Windows. Tổ chức của FatFS được mô tả như
sau

FatFS sẽ cung cấp giao diện các hàm cơ bản để thực thi các thao tác file trên thẻ
MMC/SD tương tự như lập trình file trên máy tính.
• FatFS hỗ trợ các kiểu format FAT12, FAT16, FAT32.
• Các hàm giao diện lập trình: f_mount, f_open, f_close, f_read,
f_write,f_lseek, f_sync, f_opendir, f_readdir, f_getfree, f_stat, f_mkdir,
f_untrnk, f_chmod, f_rename, f_mkfs.
FatFS gồm 2 phần chính là phần FAT bao gồm các hàm liên quan đến File
Allocation Table, và phần ATA liên quan đến xử lý các lệnh ATA để giao tiếp với
thẻ nhớ.
Tổ chức file chính của FatFs gồm:
• file ff.c: gồm các hàm hỗ trợ FAT.
• file ata.c: gồm các hàm giao tiếp ATA command.
Để giao tiếp với phần điều khiển (driver) của thẻ MMC/SD, FatFS yêu cầu 5 hàm
giao diện
disk_initialize: hàm khởi tạo kết nối tới thẻ MMC/SD, đồng thời kích hoạt cho thẻ
ở trạng thái sẵn sàng hoạt động.
• disk_write: ghi dữ liệu vào một vùng sector nhất định.
• disk_read: đọc dữ liệu từ một vùng sector cho trước.
• disk_status: lấy trạng thái của thẻ.
• disk_ioctl: các hàm xử lý các yêu cầu khác sẽ được bố trí xử lý ở đây.
Kiến trúc của FatFS đơn giản do đó rất tiện cho việc “port” sang một hệ nhúng
khác. Người dùng không cần có kiến thức sâu về FAT vẫn có thể sử dụng FatFS
vào hệ thống của mình.
Giao tiếp với thẻ MMC/SD
Thẻ MMC/SD có 4 chế độ hoạt động là: InActive, Card Identification, Data
Transfer, Interrupt. Thông thường thẻ sẽ hoạt động ở hai chế độ Card
Identification và Data Transfer.
Để giao tiếp trao đổi dữ liệu với thẻ MMC/SD, vi điều khiển phải phát lệnh điều
khiển xuống thẻ. Định dạng lệnh MMC được tổ chức gồm 48 bit như sau
Vị trí bit 47 46 [45-40] [39-8] [7-1] 0
Độ lớn 1 1 6 32 7 1
Giá trị 0 1 x x x 1
Mô tả Start bit Transmission bit Command index Argument CRC7 End bit
Ở chế độ SPI, checksum luôn có giá trị là 0xFE ngoại trừ lúc khởi động vì lúc này
tính năng tính checksum vẫn còn hoạt động. Khi nhận lệnh từ vi điều khiển, thẻ
MMC/SD luôn trả lời lại bằng 1 byte. Nếu giá trị trả về là 0xFF có nghĩa là thẻ
bận.
Các lệnh MMC/SD thường gặp

lệnh
Ký hiệu Mô tả
CMD0 GO_IDLE_STATE Reset thẻ về trạng thái idle
CMD1 SEND_OP_CODE
Yêu cầu thẻ gửi nội dung thông tin của
Operating Condition Regiters
CMD8 SEND_EXT_CSD
Yêu cầu thẻ gửi thông tin các thanh ghi
CSD(Card Specific Data) dưới dạng
block dữ liệu.

CMD9 SEND_CSD
Yêu cầu thẻ gửi thông tin cụ thể của
thanh ghi CSD.

CMD10 SEND_CID
Yêu cầu gửi các thông tin CID(Card
Information Data).

CMD12 STOP_TRANSMISSION Ngưng trao đổi dữ liệu
CMD16 SET_BLOCKLEN
Thiết lập độ lớn tính theo byte của một
block dữ liệu, giá trị mặc này được lưu
trong CSD

CMD17 READ_SINGLE_BLOCK Đọc một block dữ liệu
CMD18 READ_MULTIPLE_BLOCK
Đọc nhiều block dữ liệu. Số lượng block
được thiết lập bởi lệnh CMD23

CMD23 SET_BLOCK_COUNT
Thiết lập số lượng block dữ liệu để ghi
hoặc đọc.

CMD24 WRITE_BLOCK Ghi một block dữ liệu.
CMD25 WRITE_MULTIPLE_BLOCK
Ghi nhiều block dữ liệu. Số lượng block
được thiết lập bởi lệnh CMD23

CMD55 APP_CMD
Thông báo cho thẻ nhớ lệnh tiếp theo là
lệnh riêng của ứng dụng chứ không phải
là lệnh chuẩn của MMC.


Porting FatFS vào board STM32-GEM3M-2
Như phân tích ở trên, chúng ta chỉ cần chú ý vào 5 hàm giao diện với thẻ
MMC/SD của FatFS.
Các hàm này sẽ trực tiếp gọi đến các hàm điều khiển phần cứng ta thường hay gọi
là driver để trao đổi dữ liệu trực tiếp với thẻ.
(*): disk_status không cần thiết phải cài đặt.
Như vậy chúng ta chỉ cần cài đặt các hàm ở khối “Driver” phù hợp với phần cứng
của board STM32-GEM3M-2 là được.
+ hàm power_on(): nhiệm vụ hàm này là thiết lập các chân tín hiệu ra của STM32
cho phù hợp với kết nối SPI tới thẻ.
+ hàm power_off(): giải phóng các thiết lập trong hàm power_on().
+ hàm send_cmd(): gửi lệnh xuống thẻ theo đúng định dạng lệnh MMC/SD được
mô tả trong bảng 1.
/* Send command packet */
xmit_spi(cmd); /* Start + Command index */
xmit_spi((BYTE)(arg >> 24)); /* Argument[31..24] */
xmit_spi((BYTE)(arg >> 16)); /* Argument[23..16] */
xmit_spi((BYTE)(arg >> 8)); /* Argument[15..8] */
xmit_spi((BYTE)arg); /* Argument[7..0] */
n = 0×01; /* Dummy CRC + Stop */
if (cmd == CMD0) n = 0×95; /* Valid CRC for CMD0(0) */
if (cmd == CMD8) n = 0×87; /* Valid CRC for CMD8(0x1AA) */
xmit_spi(n);

+ hàm xmit_datablock(): gửi một khối dữ liệu xuống

xmit_spi(token); /* Xmit data token */
if (token != 0xFD) { /* Is data token */
wc = 0;
do { /* Xmit the 512 byte data block to MMC */
xmit_spi(*buff++);
xmit_spi(*buff++);
} while (–wc);
xmit_spi(0xFF); /* CRC (Dummy) */
xmit_spi(0xFF);
resp = rcvr_spi(); /* Reveive data response */
if ((resp & 0x1F) != 0×05) /* If not accepted, return with error */
return 0;
}

+ hàm rcvr_datablock():
do { /* Wait for data packet in timeout of 200ms */
token = rcvr_spi();
#ifdef SUPPORT_TIMEOUT
} while ((token == 0xFF) && Timer1);
#else
} while ((token == 0xFF));
#endif
if(token != 0xFE)
return 0; /* If not valid data token, retutn with error */
do { /* Receive the data block into buffer */
rcvr_spi_m(buff++);
rcvr_spi_m(buff++);
rcvr_spi_m(buff++);
rcvr_spi_m(buff++);
} while (btr -= 4);
rcvr_spi(); /* Discard CRC */
rcvr_spi();

Trước khi nhận dữ liệu, kiểm tra xem thẻ MMC/SD có đang bận hay không
+ hàm xmit_spi():
#define SPI_SD SPI1
#define xmit_spi(dat) sd_raw_rw_spi(dat)
BYTE sd_raw_rw_spi(BYTE b_data)
{
BYTE Data = 0;
/* Wait until the transmit buffer is empty */
//while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
/* Send the byte */
SPI_I2S_SendData(SPI_SD, b_data);
/* Wait until a data is received */
while (SPI_I2S_GetFlagStatus(SPI_SD, SPI_I2S_FLAG_RXNE) == RESET);
/* Get the received data */
Data = SPI_I2S_ReceiveData(SPI_SD);
/* Return the shifted data */
return Data;
}
+ hàm rcvr_spi():
static
BYTE rcvr_spi (void)
{
return sd_raw_rw_spi(0xFF);
}
Kết luận
FatFS với mã nguồn nhỏ gọn, rõ ràng đã cung cấp khả năng tuyệt vời truy cập file
dưới định dạng FAT. Kiến trúc của FatFS rất phù hợp với các ứng dụng nhúng,
tính khả chuyển(port) cho phép người phát triển sử dụng lại mã nguồn trên nhiều
nền tảng phần cứng khác nhau. Người lập trình không cần phải hiểu rõ về định
dạng FAT vẫn có thể sử dụng thành thạo bộ mã nguồn này trong các ứng dụng có
điều khiển thẻ MMC/SD của mình. Đây chính là lợi ích lớn nhất của các
middleware như FatFS
4. Lập trình I/O trên thẻ nhớ
Hiện nay, hệ thống file FAT (DOS) được lấy làm chuẩn định dạng cho thẻ nhớ
flash. Bạn có thể dùng một con vi điều khiển để đọc và ghi file trên một thẻ nhớ
SD hay MMC với hệ thống FAT.



Hình 01
Để đọc hay ghi file trên thẻ, đầu tiên chúng ta phải khởi tạo thẻ nhớ. Đoạn code
sau đây thực hiện việc này.
boolean initializeMemCard(void)
{
MEM_CS = HIGH;
for(i= 0; i < 10; i++)
{
spi_put(0xFF);
}
MEM_CS = LOW;
send_command(0×40,0,0,0,0×95);
if(card_response(0×01))
{
i = 255;
do
{
send_command(0×41,0,0,0,0,0xFF);
i–;
} while (!card_response(0×00) && i > 0);
MEM_CS = HIGH;
spi_put(0xFF);
if(i == 0)
{
return false;
}
return true;
}
return false;
}
Code 01

Chú ý: MEM_CS chính là tín hiệu chip select cho thẻ nhớ.
Tại hàm khởi tạo iniializeMemCard trong Code 1, dòng đầu tiên sẽ disable thẻ nhớ
bằng cách đặt biến MEM_CS về giá trị HIGH. Tiếp theo, một vòng lặp sẽ đảm bảo
rằng thẻ nhớ có đủ thời gian để hoàn thành quá trình khởi động. Lệnh spi_put
(byte x) sẽ truyền tham số vào chân SDI của thẻ nhớ. Trong vòng lặp này, tham số
x phải có giá trị 0xFF để thẻ nhớ khởi động thành công. Sau đó biến MEM_CS
được thiết lập thành LOW và lệnh 0×40 (đưa thẻ nhớ về trạng thái rảnh rỗi) được
gọi với các tham số lần lượt là 0, 0, 0, 0 và 0×95 (giá trị CRC). Đây là lúc duy
nhất giá trị CRC quan trọng, vì sau khi card vào chế độ SPI, CRC sẽ không cần
thiết nữa.
Lệnh send_command (byte cmd, byte arg1, byte arg2, byte arg3, byte arg4, byte
CRC) sẽ truyền 6 tham số của nó bằng cách gọi lệnh spi_put (byte x) 6 lần.
Dòng code tiếp theo gọi hàm card_reponse (byte x) để đợi tín hiệu trả lời thích
hợp từ thẻ nhớ. Hàm này sẽ gọi liên tục lệnh spi_get () và đợi token trả về. Nếu
token = 0×01 thì hàm trả về true, nếu sau một số lần lặp lại token vẫn khác 0×01
thì hàm sẽ trả về false. Để đưa card ra khỏi trạng thái rảnh rỗi, lệnh 0×41 sẽ được
gửi liên tục cho đến khi nhận được tín hiệu trả lời thích hợp. Nếu sau 255 lần lặp
lại mà vẫn không thành công, hàm initializeMemCard sẽ trả về false.
Sau khi có tín hiệu trả lời thích hợp, biến MEM_CS lại được thiêt lập bằng HIGH
và một byte sẽ được gửi đi, tạo ra thêm 8 chu kỳ đồng hồ để thẻ nhớ hoàn tất việc
khởi động. Chú ý rằng các chu kỳ đồng hồ này cần thiết đối với tất cả các lệnh.
Đến đây, chúng ta đã tóm tắt được các bước của tiến trình khởi động của thẻ nhớ
(các tiến trình khác cũng tương tự):
1. Đặt biến MEM_CS=LOW.
2. Gọi lệnh (1 byte lệnh, 4 byte thamsố, 1 byte CRC).
3. Đợi một byte trả lời từ thẻ nhớ.
4. Chờ nhận một data token (thao tác đọc) hay gửi data token (thao tác ghi).
5. Gửi hay nhận một khối dữ liệu.
6. Nhận 2 byte thông tin kiểm tra lỗi.
7. Đặt biến MEM_CS=HIGH.
8. Tạo thêm 8 chu kỳ đồng hồ để thẻ nhớ hoàn tất thao tác.
Bước 5 và 6 không xuất hiện trong tiến trình khởi động, nhưng là bước chính
trong các thao tác đọc và ghi. Chú ý là thẻ nhớ nên tự động khởi tạo kích thước
khối dữ liệu 512 byte như kích thước sector của FAT.
Lệnh Byte
code
Response
byte
Data token
nhận
Data token
chuyển
Khởi tạo và đưa thẻ vào
trạng thái rảnh rỗi
0×40 0×01 – –
Đưa thẻ ra khỏi trạng thái
rảnh rỗi
0×41 0×00 – –
Đọc dữ liệu 0×51 0×00 0xFE –
Ghi dữ liệu 0×58 0×00 – 8 chu kỳ đồng
hồ, 0xFE
Đặt độ dài khối dữ liệu 0×50 0×00 – –
Bảng 01
Bảng trên liệt kê tất cả các giá trị lệnh cần thiết để thao tác với hệ thống FAT. Mặc
dù thẻ nhớ hỗ trợ chế độ đọc một khối một lần và cả nhiều khối một lần nhưng
chúng ta hãy cùng tìm hiểu việc đọc một khối 512 byte (gửi lệnh đọc 0×51).
Phải đảm bảo rằng tham số địa chỉ được định dạng thích hợp cho thao tác đọc, ghi.
Ví dụ, nếu ta muốn đọc sector thứ 2005, 4 byte địa chỉ tham số phải là 0×00, 0x0F,
0xA8, 0×00 vì sector được đánh số khởi đầu bằng 0. Sector 2004 có địa chỉ
0x000FA800, được xác định như sau:
(2,005-1)x512= 1,024,048 = 0x000FA800.
Chú ý rằng định dạng địa chỉ của SD/MMC là kiểu big endian. Đoạn code trong
Code 2 sẽ đọc sector 2004 trong thẻ nhớ. Đoạn này gần tương tự với đoạn khởi tạo
thẻ. Khác biệt đáng chú ý là việc gọi thêm hàm card_response (byte x) lần thứ 2
với tham số có giá trị 0xFE chính là data token. Tất cả dữ liệu được trao đổi qua
card đều bắt đầu với data token này. 512 lần gọi hàm spi_get() trong vòng lặp sẽ
điền đầy giá trị của sector 2004 vào buffer. Hai lần gọi thêm hàm spi_get() sẽ đọc
các byte CRC không được dùng tới được đính kèm vào các khối dữ liệu gửi bởi
thẻ nhớ.
unsigned char buf[512];
boolean read_sector(void)
{
unsigned short i;
boolean retval = false;
MEM_CS = LOW;
send_comman(0×51,0,0x0F,xA8,0,0xFF);
if(card_response(0×00))
{
if(card_response(0xFE))
{
for(i = 0; i < 512; i++)
{
buf[i] = spi_get();
}
spi_get();
spi_get();
retval = true;
}
MEM_CS = HIGH;
spi_put(0xFF);
}
return retval;
}
Code 02

Việc ghi dữ liệu cũng tương tự như việc đọc, điểm khác nhau duy nhất chỉ là
hướng đi của dữ liệu. Trong thao tác đọc, thẻ nhớ cung cấp data token và dữ liệu
còn trong thao tác ghi, bộ vi điều khiển chịu trách nhiệm này. Để ghi một khối dữ
liệu, bạn cần gửi lệnh ghi (0×58) cùng với địa chỉ đúng định dạng, chờ byte trả lời,
tạo ra 8 chu kỳ đồng hồ, gửi data token, và bắt đầu gửi dữ liệu. Chú ý đừng quên 2
byte CRC cuối cùng đính vào khối dữ liệu. Không giồng thao tác đọc, ở thao tác
ghi bạn phải đảm bảo thẻ nhớ đã hoàn tất việc ghi sau khi bạn đã gửi dữ liệu. Để
thực hiện việc này, ta cần kiểm tra byte trả lời xuất ra từ thẻ nhớ. Hàm
checkWriteState() gọi lặp lại nhiều lần hàm spi_get() trong khi chờ data token
0×05. Khi đã có data token này rồi, byte khác 0 đầu tiên có được từ hàm spi_get()
sẽ báo hiệu hoàn tất.

VÍ DỤ VỀ FAT



Hình 02
Hãy xem xét 2 ví dụ về đọc và ghi file trên một thẻ nhớ. Giả sử chúng ta đang làm
việc với thẻ được định dạng FAT 16. Trên một đĩa được định dạng theo FAT 16 sẽ
có khoảng 65,000 vị trí nhớ. Một thẻ nhớ được định dạng như một đĩa cứng với
một phân vùng (phân vùng DOS chính). Như hình 2, sector đầu tiên (512 byte) của
thẻ nhớ chứa MBR (master boot record). Phân vùng FAT (phân vùng DOS chính)
tiếp theo ngay phía sau. Phân vùng này bắt đầu bằng một vài sector để dành với
sector đầu tiên là boot record (không được nhầm với MBR), 1 hoặc 2 bảng FAT
và một bảng thư mục gốc (root directory table). Vùng dữ liệu thật sự nằm ngay
phía sau bảng thư mục gốc, được bố trí thành từng nhóm các sector gọi là cluster.
Không gian lưu trữ được gán cho các file theo từng cluster. MBR chứa một trình
nạp nhỏ (boot strap loader) và bảng thư mục. Trong một đĩa cứng thật sự, trình nạp
sẽ tìm và chạy trình nạp thứ 2 trong boot record của phân vùng chính (primary
partition). Trong một thẻ nhớ, MBR vẫn chứa chương trình mồi chính, nhưng máy
tính chỉ dùng thông tin từ bảng thư mục.
ĐỌC FILE
Byte thứ 0 – 7 8 – 10 11 – 25 26 – 27 28 – 31
File 1 Tên file Phần mở rộng Thuộc tính, ngày Cluster đầu Kích thước
File 2 Tên file Phần mở rộng Thuộc tính, ngày Cluster đầu Kích thước
… … … … … …
File n Tên file Phần mở rộng Thuộc tính, ngày Cluster đầu Kích thước
Bảng 02
Chúng ta sẽ xem xét việc đọc một file hello.txt chứa trong thư mục gốc và đưa nội
dung của nó (Hello World) vào một mảng ký tự. Bạn có thể thử nghiệm ví dụ này
bằng cách tạo file có nội dung “Hello World”, lưu file vào thư mục gốc của thẻ
nhớ. Để đọc file, bạn phải xác định mục nhập của file trong bảng thư mục gốc
ngay phía sau 2 bảng FAT (xem hình 2). Bảng thư mục gốc có cấu trúc dạng bảng
(xem bảng 2), mỗi một dòng 32 byte tương ứng với một mục nhập file. Một vài
dòng sẽ trống hay trỏ về các file đã bị xóa. Các cột trong bảng sẽ cung cấp các
thông tin vê file. Cột đầu tiên là tên file, cột thứ 2 chỉ phần mở rộng, không bao
gồm dấu chấm (FAT 16 có tên file dạng 8.3). Cột kế cuối chỉ ra cluster đầu tiên
chứa dữ liệu file và cột cuối cùng xác định kích thước file theo byte (định dạng
little endian). Các cột khác (byte 11 – 25) chứa các thông tin khác như ngày giờ
tạo, thay đổi file.
Để xác định file hello.txt, tìm tên file và phần mở rộng trong bảng thư mục gốc. Vì
bảng thư mục gốc cũng chỉ là dữ liệu chứa trong bộ nhớ, bạn có thể đọc thông tin
từ bảng thư mục theo từng sector dùng hàm read_sector(). Sector đầu tiên trong
phân vùng FAT là boot record, chứa các tham số cho ta biết vị tri của sector đầu
tiên của bảng thư mục gốc. Ngoài ra ta cũng biết chiều dài của mỗi dòng trong
bảng thư mục, như vậy sẽ dễ dàng tìm ra file hello.txt. Hàm này sẽ trả về dòng
chứa mục nhập của file. Nếu không tìm ra, có nghĩa là file không tồn tại trong thẻ
nhớ. Trong bảng thư mục gốc, ký tự đầu tiên trong tên một file bị xóa được thay
thế bằng một ký tự đặc biệt. Dòng đầu tiên bắt đầu bằng byte có giá trị 0×00 chỉ ra
rằng sẽ không có dòng tiếp theo nào được dùng. Vì thế, trừ khi thư mục gốc chứa
tối đa số file nó có thể, công việc tìm kiếm có thể dừng lại mà không cần phải đọc
đến dòng cuối cùng của bảng. Giả sử chúng ta đã biết được dòng tương ứng với
mục nhập file hello.txt. Bạn có thể đọc 2 byte từ cột chứa số cluster đầu tiên theo
định dạng litle endian để xác định xem dữ liệu bắt đầu từ đâu. Bạn cũng có thể đọc
cột kích thước file để biết kích thước file. Để bắt đầu đọc nội dung file, bạn phải
xác định được sector đầu tiên của cluster đầu tiên. Boot record có chứa một tham
số cho chúng ta biết có bao nhiêu sector trong một cluster. Đây là thông tin được
xác định khi đĩa được dịnh dạng. Cluster càng lớn thì bảng FAT càng nhỏ nhưng
sẽ dẫn đến việc lãng phí vùng nhớ đối với các file không dùng hết dữ không gian
nhớ ở cluster cuối cùng của file. Vùng dữ liệu nằm ngay sau bảng thư mục gốc, vì
thế bạn có thể xác định sector đầu tiên của cluster đầu tiên theo cách sau:
Nếu cột cluster đầu tiên trong bảng thư mục của file hello.txt chứa số 107 thì
cluster 107 sẽ là nơi bắt đầu chứa dữ liệu cho file (Cluster đầu tiên của vùng dữ
liệu là cluster 2 vì mục nhập số 0 và 1 đã dành cho thông tin bảng FAT như hình
2). Giả sử rằng mỗi cluster có 4 sector. Có nghĩa là file bắt đầu tại sector số:
(Sector đầu của vùng dữ liệu) + [(cluster đầu - 2) x (số sector trên một cluster)]
Trong đó, sector đầu của vùng dữ liệu được tính toán với tham số trong MBR để
xác định vị trí bắt đầu của phân vùng FAT, sau đó dùng tham số của boot record
để xác định vị trí bắt đầu của vùng dữ liệu.
Trở lại với file hello.txt, giả sử rằng file dài 12 byte. Có nghĩa là toàn bộ dữ liệu
của file chứa đủ trong một cluster đầu tiên (cụ thể là trong sector đầu tiên). Bây
giờ hàm read_sector() có thể đọc dữ liệu của file vào bộ vi điều khiển. Tất cả các
byte sau byte thứ 12 được bỏ qua.
Nếu bạn thay đổi file hello.txt để nó chứa 1000 dòng “Hello World!”. Nội dung
của file lúc này sẽ không chứa đủ trong 1 cluster. Làm thế nào bạn xác định được
phần còn lại của file nằm ở đâu? Câu trả lời nằm trong bảng FAT. Sau khi đọc dữ
liệu từ cluster 107, bạn sẽ vào mục nhập 107 trong bảng FAT để đọc xem cluster
tiếp theo là số bao nhiêu. Có thể có 2 bảng FAT giống hệt như nhau trên thẻ nhớ
(xem hình 2). Boot record chứa các tham số cho bạn biết có bao nhiêu bảng FAT,
độ dài mỗi bảng FAT và vị trí bắt đầu của bảng FAT đầu tiên so với boot record.
Mỗi mục nhập trong FAT 16 là một số 16 bit (định dạng little endian), nên mục
nhập 107 sẽ nằm tại byte 214 và 215 của sector đầu tiên của mỗi bảng FAT. Nhiều
bảng FAT cùng hiện diện trên thẻ nhớ để có thể phục hồi nếu hệ thống file gặp sự
cố. Cấu trúc bảng FAT đơn giản, nhưng bù lại không tốt lắm trong việc chịu lỗi.
Nếu máy tính gặp sự cố mất điện khi bạn đang cập nhật bảng FAT thì bạn có thể
sẽ mất vị trí dữ liệu tiếp theo của một số lớn file. Mỗi bảng FAT được cập nhật
riêng rẽ, vì thế chỉ có một bảng FAT bị hỏng khi gặp sự cố. Các dịch vụ CHKDSK
hay SCANDSK của hệ điều hành cố gắng tối thiểu hóa chuyện mất mát dữ liệu
bằng cách phục hồi tối đa thông tin từ bảng FAT còn lại không bị hư. Để tìm
cluster tiếp theo của file, đọc sector tương ứng (thường từ bảng FAT đầu tiên)
bằng hàm read_sector() và lấy dữ liệu cần thiết. Giả sử mục nhập 107 trong bảng
FAT chứa giá trị 489. Có nghĩa là khối dữ liệu tiếp theo của file nằm tại cluster
489. Cứ như vậy ta sẽ tìm ra dần dần các khối dữ liệu tiếp theo của file. Ta sẽ biết
khi nào đã hết dữ liệu của file vì ta đã có thông tin về độ dài của file, hơn nữa khi
một mục nhập của file trong bảng FAT chứa giá trị từ 0xFFF8 đến 0xFFFF thì có
nghĩa là file không còn cluster nào theo sau nữa.
GHI FILE
Ở ví dụ thứ 2 chúng ta sẽ tạo file goodbye.txt trong thư mục gốc với nội dung
“Goodbye World!”. Đầu tiên, bạn cần tìm một dòng còn dùng được trong bảng thư
mục gốc để tạo mục nhập mới. Bạn có thể làm điều này bằng cách tìm một mục
nhập có tên file bắt đầu với ký tự 0xE5 hay 0×00, tương ứng với file đã bị xóa
hoặc là mục nhập chưa dùng. Sau khi tìm được mục nhập ta sẽ ghi vào mục nhập
tên file, phần mở rộng, thời gian tạo file và các thuộc tính của file. Trường thuộc
tính tại byte thứ 11 chứa một số cờ bit, trong đó có một cờ chỉ ra rằng đây là file
hay là một thư mục con. Khi tạo một file bình thường, bạn phải đặt giá trị cho
trường thuộc tính là 0×20. Một điều rắc rối là bạn cần phải ghi vào bảng thư mục
này vị trí cluster đầu tiên của file. Có 2 trường hợp phải xem xét. Trường hợp đầu
tiên là ta đang dùng lại mục nhập của một file bị xóa. Vị trí cluster đầu tiên và toàn
bộ chuỗi giá trị trong FAT (của file đã bị xóa) vẫn không bị thay đổi. Bạn có thể
ghi đè dữ liệu lên các cluster này. Nếu file mới tạo dài hơn file cũ bị xóa thì ta
thêm cluster vào. Nói thêm về trường hợp file bị xóa: Để giảm thiểu các tác vụ I/O
khi một file nào đó bị xóa, chỉ một ký tự đầu tiên của tên file đó bị chuyển thành
0xE5. Phần thông tin còn lại trong bảng thư mục không thay đổi. Đây là lý do tại
sao file có thể được phục hồi. Trường hợp thứ 2 là ta dùng một mục nhập mới
trong bảng thư mục gốc, như vậy ta cần phải tìm một cluster trống. Để tìm một
cluster trống, ta chỉ cần duyệt FAT bằng cách đọc từng sector một và tìm một mục
nhập có giá trị 0×0000. Sau khi tìm được, đổi giá trị của nó thành 0xFFFF để báo
hiệu đây là nơi kết thúc file, và ghi lại vị trí này vào trường vị trí cluster đầu tiên
trong bảng thư mục gốc. Với vị trí của cluster trống cho file goodbye.txt, bạn có
thể bắt đầu ghi nội dung của file (“Goodbye World!”). Bạn ít nhất phải ghi vào
sector đầu tiên của cluster. Đặt chuỗi cần ghi vào một mảng ký tự 512 byte rồi
dùng hàm write_sector() để ghi vào thẻ nhớ. Việc mảng ký tự chứa gì sau ký tự
cuối cùng cần ghi không quan trọng. Chúng ta biết rằng nội dung file goodbye.txt
có thể được chứa hoàn toàn trong sector đầu tiên của một cluster nên ta không cần
viết vào các sector tiếp theo nữa.
Sau khi thêm nội dung vào file, cập nhật trường kích thước file trong bảng thư
mục. Đồng thời ta cũng nên cập nhật thời gian truy cập/thay đổi file. Nếu một file
tăng kích thước và cần thêm cluster để chứa, một cluster trống mới cần được xác
định trong bảng FAT. Vị tri của cluster trống này sẽ được ghi vào mục nhập của
cluster trước đó của file trong bảng FAT, và giá trị của mục nhập cho cluster cuối
cùng của file sẽ là 0xFFFF để xác định đây là cluster kết thúc của file. Vì thế, một
chuỗi mục nhập trong FAT có thể kéo dài mãi cho đến khi còn cluster trống.
int main(void)
{
signed int stringSize;
signed char handle;
char stringBuf[100];
cpu_init(); //Initialize the CPU clocks, etc.
eint(); //Enable global interrupts (if required).
fat_initialize(); //Initialize the FAT library.
handle = fat_openRead(“hello.txt”);
if (handle >= 0)
{
stringSize = fat_read(handle, stringBuf, 99);
stringBuf[stringSize] = ‘’;
lcd_print(stringBuf);
fat_close(handle);
}
handle = fat_openWrite(“goodbye.txt”);
if (handle >= 0)
{
strcpy(stringBuf, “Goodbye World!”);
stringSize = strlen(stringBuf);
fat_write(handle,stringBuf, stringSize);
fat_flush(); //Optional.
fat_close(handle);
}
while (1)
{
//Stay here.
}
}

Code 03
Ví dụ:
Đoạn code trên sẽ đọc 12 ký tự đầu tiên từ file hello.txt trong thư mục gốc. Sau đó
chương trình in các ký tự đó ra màn hình LCD (dòng chữ Hello World!). Tiếp đến
chương trình sẽ tạo file goodbye.txt và ghi “Goodbye Worlds!” vào file. Khi
chương trình kết thúc, bạn có thể gắn thẻ nhớ của bạn vào bất kỳ đầu đọc thẻ nhớ
chuẩn nào và bạn sẽ thấy file goodbye.txt trong thẻ