субота, 11 травня 2019 р.

STM32: Бібліотека OneWire і Dallas Temperature HAL по UART з DMA

update 04.01.2021: повністю оновлена стаття, переписана бібліотека, приклади, додано відео.

update 10.12.2020: завдяки коментаторам додав корективи. Зазначив, що для UART на якому будуть сенсори, крім DMA, потрібно ще увімкнути глобальні переривання "USART global interrupt". Та в файлі OneWire.c поміняв місцями функції:
HAL_UART_Receive_DMA(&HUARTx, ow_buf, num_bits);
HAL_UART_Transmit_DMA(&HUARTx, ow_buf, num_bits);

Первіша має бути функція приймання. Сподіваюсь це допоможе. Бо в мене і так і так все нормально працювало.

Передмова

Макет

Бібліотека OneWire створена за мотивами статей "Stm32 + 1-wire + DMA (продолжение)" та "STM32 + 1-wire. Поиск устройств". А бібліотека Dallas Temperature клон бібліотеки "DallasTemperature" для Arduino. За винятком: не працює з "Alarm" - не розібрався, не реалізована робота з паразитним живленням датчиків, та без перевірки на всі нулі функцією "IsAllZeros". У файлі "dallastemperature.c" в функції DT_IsConnected_ScratchPad закоментована перевірка "IsAllZeros". В іншому все працює коректно.

Створюємо проект в CubeMX або CubeIDE

Запускаємо CubeIDE, обираємо свій мікроконтролер, або дошку. Даємо назву проекту, та налаштовуємо проект:
Налаштування проекту
  1. RCC - тактуємось від зовнішнього кварцу "Crystal/Ceramic resonator";
  2. SYS - відладка по "Serial Wire";
  3. USART1 або USART3 - обираємо режим "Single Wire (Half-Duplex)" та додаємо канали DMA як для прийому так і для передачі, а також вмикаємо глобальні переривання "USART global interrupt";
  4. USART2 - за замовчуванням, для передачі даних температури на послідовний порт комп'ютера.
Генеруємо проект, та додаємо файли бібліотеки до проекту. OneWire.h і DallasTemperature.h до теки "inc" проекту, а файли OneWire.с і DallasTemperature.с до теки "src" проекту.

В файлі DallasTemperature.h за замовчуванням, на лінії 1-Wire йде пошук, максимум 5 сенсорів. Змінити можна в рядку:

#define ONEWIRE_MAX_DEVICES	5

Схема підключення

Схема підключення стандартна:
Схема підключення

Живлення, земля, дата + підтяжка до живлення через резистор 4.7 - 5.6 кОм. Ніжка "DQ" до USART1 (PA9) або USART3 (PC10) .

Демо-код

Відкриваємо файл проекту main.c та додаємо заголовні файли бібліотек до коду:
/* USER CODE BEGIN Includes */
#include "OneWire.h"
#include "DallasTemperature.h"
/* USER CODE END Includes */

Додаємо функцію відправки тексту по USART2 та функцію друкування адреси сенсорів:
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len)
{
 HAL_UART_Transmit(&huart2, (uint8_t *) ptr, len, HAL_MAX_DELAY);
 return len;
}

// function to print a device address
void printAddress(CurrentDeviceAddress deviceAddress)
{
  for (uint8_t i = 0; i < 8; i++)
  {
	  printf("0x%02X ", deviceAddress[i]);
  }
}
/* USER CODE END 0 */

Приклад розрахований для двох DS18B20 на одній лінії USART3. Як у вас один сенсор, видаліть код для другого сенсора з індексом 1. Та якщо у вас лінія 1-wire не на USART3, то зробіть відповідні зміни в демо-коді.

Оголошуємо структури для OneWire та для DallasTemperature:
/* USER CODE BEGIN PV */
OneWire_HandleTypeDef ow;
DallasTemperature_HandleTypeDef dt;
/* USER CODE END PV */

В секцію 2 додаємо перевірку чи на лінії є якісь сенсори 1-Wire, взнаємо кількість підключених до лінії сенсорів, задаємо роздільну здатність сенсорів, та прочитаємо їхню адресу і поточну роздільну здатність:
/* USER CODE BEGIN 2 */
  printf("[%8lu] Debug UART2 is OK!\r\n", HAL_GetTick());

  OW_Begin(&ow, &huart3);

  if(OW_Reset(&ow) == OW_OK)
  {
	  printf("[%8lu] OneWire devices are present :)\r\n", HAL_GetTick());
  }
  else
  {
	  printf("[%8lu] OneWire no devices :(\r\n", HAL_GetTick());
  }

  DT_SetOneWire(&dt, &ow);

  // arrays to hold device address
  CurrentDeviceAddress insideThermometer;

  // locate devices on the bus
  printf("[%8lu] Locating devices...", HAL_GetTick());

  DT_Begin(&dt);

  printf("[%8lu] Found %d devices.\r\n", HAL_GetTick(), DT_GetDeviceCount(&dt));

  if (!DT_GetAddress(&dt, insideThermometer, 0))
	  printf("[%8lu] Unable to find address for Device 0\r\n", HAL_GetTick());

  printf("[%8lu] Device 0 Address: ", HAL_GetTick());
  printAddress(insideThermometer);
  printf("\r\n");

  // set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
  DT_SetResolution(&dt, insideThermometer, 12, true);

  printf("[%8lu] Device 0 Resolution: %d\r\n", HAL_GetTick(), DT_GetResolution(&dt, insideThermometer));

  if (!DT_GetAddress(&dt, insideThermometer, 1))
	  printf("[%8lu] Unable to find address for Device 1\r\n", HAL_GetTick());

  printf("[%8lu] Device 1 Address: ", HAL_GetTick());
  printAddress(insideThermometer);
  printf("\r\n");

  // set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
  DT_SetResolution(&dt, insideThermometer, 12, true);

  printf("[%8lu] Device 1 Resolution: %d\r\n", HAL_GetTick(), DT_GetResolution(&dt, insideThermometer));
  /* USER CODE END 2 */

Приклад розрахований для двох DS18B20 на одній лінії. Як у вас один сенсор, видаліть код для другого сенсора з індексом 1.

А в безкінечному циклі раз на 2 секунди будемо опитувати сенсори і виводити в послідовний порт отриману температуру:

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	// call DT_RequestTemperatures(&dt) to issue a global temperature
	// request to all devices on the bus
	printf("[%8lu] Requesting temperatures...", HAL_GetTick());
	DT_RequestTemperatures(&dt); // Send the command to get temperatures
	printf("\r\n[%8lu] DONE\r\n", HAL_GetTick());
	// After we got the temperatures, we can print them here.
	// We use the function ByIndex, and as an example get the temperature from the first sensor only.
	printf("[%8lu] Temperature for the device 1 (index 0) is: %.2f\r\n", HAL_GetTick(), DT_GetTempCByIndex(&dt, 0));
	printf("[%8lu] Temperature for the device 2 (index 1) is: %.2f\r\n", HAL_GetTick(), DT_GetTempCByIndex(&dt, 1));
	HAL_Delay(2000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Щоб функція "printf" могла виводити значення типу "float" потрібно відповідно налаштувати проект в CubeIDE, а саме додати ключ "-u _printf_float":

Налаштування друку float

І для Atolic TrueStudio обрати "NewLib standart":

Налаштування для друку float

Компілюємо, заливаємо. Запускаємо будь-яку термінальну програму і спостерігаємо:

Приклад роботи двох датчиків на лінії

Відео-посібник

Текст коду з відео:

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "OneWire.h"
#include "DallasTemperature.h"
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
OneWire_HandleTypeDef ow1;
DallasTemperature_HandleTypeDef dt1;
OneWire_HandleTypeDef ow2;
DallasTemperature_HandleTypeDef dt2;
/* USER CODE END PV */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len)
{
	HAL_UART_Transmit(&huart2, (uint8_t *) ptr, len, HAL_MAX_DELAY);
	return len;
}

// function to print a device address
void printAddress(CurrentDeviceAddress deviceAddress)
{
  for (uint8_t i = 0; i < 8; i++)
  {
	  printf("0x%02X ", deviceAddress[i]);
  }
}
/* USER CODE END 0 */

/* USER CODE BEGIN 2 */
printf("[%8lu] Debug UART2 is OK!\r\n", HAL_GetTick());

OW_Begin(&ow1, &huart1);
OW_Begin(&ow2, &huart3);

if(OW_Reset(&ow1) == OW_OK)
{
  printf("[%8lu] OneWire 1 line devices are present :)\r\n", HAL_GetTick());
}
else
{
  printf("[%8lu] OneWire 1 line no devices :(\r\n", HAL_GetTick());
}

if(OW_Reset(&ow2) == OW_OK)
{
  printf("[%8lu] OneWire 2 line devices are present :)\r\n", HAL_GetTick());
}
else
{
  printf("[%8lu] OneWire 2 line no devices :(\r\n", HAL_GetTick());
}

DT_SetOneWire(&dt1, &ow1);
DT_SetOneWire(&dt2, &ow2);

// arrays to hold device address
CurrentDeviceAddress insideThermometer;

// locate devices on the bus
printf("[%8lu] 1-line Locating devices...\r\n", HAL_GetTick());
DT_Begin(&dt1);
uint8_t numDevOneLine = DT_GetDeviceCount(&dt1);
printf("[%8lu] 1-line Found %d devices.\r\n", HAL_GetTick(), numDevOneLine);

printf("[%8lu] 2-line Locating devices...\r\n", HAL_GetTick());
DT_Begin(&dt2);
uint8_t numDevTwoLine = DT_GetDeviceCount(&dt2);
printf("[%8lu] 2-line Found %d devices.\r\n", HAL_GetTick(), numDevTwoLine);

for (int i = 0; i < numDevOneLine; ++i)
{
	if (!DT_GetAddress(&dt1, insideThermometer, i))
		  printf("[%8lu] 1-line: Unable to find address for Device %d\r\n", HAL_GetTick(), i);
	printf("[%8lu] 1-line: Device %d Address: ", HAL_GetTick(), i);
	printAddress(insideThermometer);
	printf("\r\n");
	// set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
	DT_SetResolution(&dt1, insideThermometer, 12, true);
	printf("[%8lu] 1-line: Device %d Resolution: %d\r\n", HAL_GetTick(), i, DT_GetResolution(&dt1, insideThermometer));
}

for (int i = 0; i < numDevTwoLine; ++i)
{
	if (!DT_GetAddress(&dt2, insideThermometer, i))
		printf("[%8lu] 2-line: Unable to find address for Device %d\r\n", HAL_GetTick(), i);
	printf("[%8lu] 2-line: Device %d Address: ", HAL_GetTick(), i);
	printAddress(insideThermometer);
	printf("\r\n");
	// set the resolution to 12 bit (Each Dallas/Maxim device is capable of several different resolutions)
	DT_SetResolution(&dt2, insideThermometer, 12, true);
	printf("[%8lu] 2-line: Device %d Resolution: %d\r\n", HAL_GetTick(), i, DT_GetResolution(&dt2, insideThermometer));
}
/* USER CODE END 2 */

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	// call sensors.requestTemperatures() to issue a global temperature
	// request to all devices on the bus
	printf("[%8lu] 1-line: Requesting temperatures...", HAL_GetTick());
	DT_RequestTemperatures(&dt1); // Send the command to get temperatures
	printf("\r\n[%8lu] 1-line: DONE\r\n", HAL_GetTick());

	printf("[%8lu] 2-line: Requesting temperatures...", HAL_GetTick());
	DT_RequestTemperatures(&dt2); // Send the command to get temperatures
	printf("\r\n[%8lu] 2-line: DONE\r\n", HAL_GetTick());

	for (int i = 0; i < numDevOneLine; ++i)
	{
		// After we got the temperatures, we can print them here.
		// We use the function ByIndex, and as an example get the temperature from the first sensor only.
		printf("[%8lu] 1-line: Temperature for the device %d (index %d) is: %.2f\r\n", HAL_GetTick(), i, i, DT_GetTempCByIndex(&dt1, i));
	}

	for (int i = 0; i < numDevTwoLine; ++i)
	{
		// After we got the temperatures, we can print them here.
		// We use the function ByIndex, and as an example get the temperature from the first sensor only.
		printf("[%8lu] 2-line: Temperature for the device %d (index %d) is: %.2f\r\n", HAL_GetTick(), i, i, DT_GetTempCByIndex(&dt2, i));
	}

	HAL_Delay(DT_MillisToWaitForConversion(DT_GetAllResolution(&dt1)));
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

понеділок, 25 березня 2019 р.

STM32: Бібліотека для роботи з LCD дисплеєм з контролером ST7735

update 10.03.2021: Замінив в файлі бібліотеки st7735.c дефайни з маніпулюванням регістрами GPIO на HAL функції. Для сумісності з будь яким чипом. Додав відео-підтримку до статті.

Передмова

Бібліотека призначена для роботи з LCD дисплеєм з контролером ST7735(S) по SPI шині. Для розмірів 128X160, 128X128 та 160X80. Дисплей можна придбати деінде, наприклад: ebay, aliexpress, тощо.

Я в проекті використовую:

Схема з'єднання

Схема з'єднання
Клацайте по зображенню, щоб збільшити. На деяких дисплеях, як на моєму, шпильки MOSI та SCK позначені як SDA та SCL. Та помилково на платі надруковано роздільна здатність як 128Х120, насправді роздільна здатність дисплею 128X160 точок.
Приклад дисплею ST7735S

Створюємо проект в CubeMX

Запускаємо CubeMX, обираємо свій мікроконтролер, або дошку. Та налаштовуємо проект:
Налаштування периферії
  1. Тиснемо RCC -> High Speed Clock (HSE) -> Crystal/Ceramic Resonator, та SYS -> Debug -> Serial Wire.
  2. Тиснемо SPI1.
  3. Обираємо Mode -> Transmit Only Master, та Hardware NSS Signal -> Disable, все інше без змін.
  4. Обираємо шпильки для сигналів BL (підсвітка), CS (вибір чипу), DC (дані/команда), RES (скидання). Шпильки називаємо так як на малюнку: ST7735_BL, ST7735_CS, ST7735_DC, ST7735_RES.
  5. Генеруємо проект: вказуємо шлях, даємо назву проекту "STM32F103C8T6_ST7735_SPI", обираємо свій засіб розробки. В мене це "Atolic Truestudio". Тиснемо "OK".

Демонстраційний код

Відкриваємо свій засіб розробки та експортуємо згенерований, CubeMX, проект. Файли st7735.h, st7735_cfg.h, fonts.h, testimg.h копіюємо до теки "inc" проекту, а файли st7735.c та fonts.c до теки "src" проекту.
Відкриваємо файл st7735_cfg.h та налаштовуємо бібліотеку.
Вказуємо до якого саме SPI під'єднано дисплей. В прикладі до SPI1, та якщо використовуєте DMA для передачі даних по SPI, то розкоментуйте рядок з "USE_SPI_DMA". Але хочу зауважити, що в режимі SPI з DMA екран дисплею промальовується повільніше ніж без DMA.
#define ST7735_SPI_PORT hspi1 //hspi1, hspi2, hspi3...
//#define USE_SPI_DMA   //if used DMA for SPI bus
Вказуємо тип дисплею. В прикладі це ST7735S.
//#define ST7735_1_8_DEFAULT_ORIENTATION // AliExpress/eBay 1.8" display, default orientation
#define ST7735S_1_8_DEFAULT_ORIENTATION  // WaveShare ST7735S-based 1.8" display, default orientation
//#define ST7735_1_44_DEFAULT_ORIENTATION  // 1.44" display, default orientation
//#define ST7735_MINI_DEFAULT_ORIENTATION  // mini 160x80 display (it's unlikely you want the default orientation)
Та якщо ви не дали назви шпилькам в CubeMX, то потрібно самому назначити порт і шпильку до відповідних назв:
//Port and pin connected signal 'RES' (reset) ST7735 display
#ifndef ST7735_RES_Pin
#define ST7735_RES_Pin   GPIO_PIN_12
#endif
#ifndef ST7735_RES_GPIO_Port
#define ST7735_RES_GPIO_Port  GPIOB
#endif
//Port and pin connected signal 'DC' (data or command) ST7735 display
#ifndef ST7735_DC_Pin
#define ST7735_DC_Pin   GPIO_PIN_13
#endif
#ifndef ST7735_DC_GPIO_Port
#define ST7735_DC_GPIO_Port  GPIOB
#endif
//Port and pin connected signal 'CS' (chip select) ST7735 display
#ifndef ST7735_CS_Pin
#define ST7735_CS_Pin   GPIO_PIN_14
#endif
#ifndef ST7735_CS_GPIO_Port
#define ST7735_CS_GPIO_Port  GPIOB
#endif
//Port and pin connected signal 'BL' (back light) ST7735 display
#ifndef ST7735_BL_Pin
#define ST7735_BL_Pin  GPIO_PIN_15
#endif
#ifndef ST7735_BL_GPIO_Port
#define ST7735_BL_GPIO_Port  GPIOB
#endif
Якщо ви вже дали назви шпилькам в програмі CubeMX, то про це вже не потрібно турбуватись. З налаштуванням бібліотеки це все.

Залишилось додати демонстраційний код до файлу main.c. Відкриваємо його і додаємо такі рядки в секцію "USER CODE BEGIN Includes":
/* USER CODE BEGIN Includes */
#include "st7735.h"
#include "fonts.h"
#include "testimg.h"
/* USER CODE END Includes */
В секцію "USER CODE BEGIN PV" оголошуємо глобальну змінну:
/* USER CODE BEGIN PV */
uint8_t r = 0;
/* USER CODE END PV */
В секцію "USER CODE BEGIN 0" додаємо функцію демонстрації можливостей бібліотеки:
/* USER CODE BEGIN 0 */
void demoTFT(void)
{
 ST7735_SetRotation(r);

 ST7735_FillScreen(ST7735_BLACK);

 for(int x = 0; x < ST7735_GetWidth(); x++)
 {
   ST7735_DrawPixel(x, 0, ST7735_WHITE);
   ST7735_DrawPixel(x, ST7735_GetHeight() - 1, ST7735_WHITE);
 }

 for(int y = 0; y < ST7735_GetHeight(); y++)
 {
   ST7735_DrawPixel(0, y, ST7735_WHITE);
   ST7735_DrawPixel(ST7735_GetWidth() - 1, y, ST7735_WHITE);
 }

 ST7735_DrawLine(0, 0, ST7735_GetWidth(), ST7735_GetHeight(), ST7735_WHITE);
 ST7735_DrawLine(ST7735_GetWidth(), 0, 0, ST7735_GetHeight(), ST7735_WHITE);

 HAL_Delay(2000);

 ST7735_FillScreen(ST7735_BLACK);

 for (int i = 0; i < ST7735_GetHeight(); i += 4)
 {
  ST7735_DrawFastHLine(0, i, ST7735_GetWidth() - 1, ST7735_WHITE);
 }

 for (int i = 0; i < ST7735_GetWidth(); i += 4)
 {
  ST7735_DrawFastVLine(i, 0, ST7735_GetHeight() - 1, ST7735_WHITE);
 }

 HAL_Delay(2000);

 // Check fonts
 ST7735_FillScreen(ST7735_BLACK);
 ST7735_DrawString(0, 0, "Font_7x10, red on black, lorem ipsum dolor sit amet", Font_7x10, ST7735_RED, ST7735_BLACK);
 ST7735_DrawString(0, 3*10, "Font_11x18, green, lorem ipsum", Font_11x18, ST7735_GREEN, ST7735_BLACK);
 ST7735_DrawString(0, 3*10+3*18, "Font_16x26", Font_16x26, ST7735_BLUE, ST7735_BLACK);
 HAL_Delay(2000);

 // Check colors
 ST7735_FillScreen(ST7735_BLACK);
 ST7735_DrawString(0, 0, "BLACK", Font_11x18, ST7735_WHITE, ST7735_BLACK);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_BLUE);
 ST7735_DrawString(0, 0, "BLUE", Font_11x18, ST7735_BLACK, ST7735_BLUE);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_RED);
 ST7735_DrawString(0, 0, "RED", Font_11x18, ST7735_BLACK, ST7735_RED);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_GREEN);
 ST7735_DrawString(0, 0, "GREEN", Font_11x18, ST7735_BLACK, ST7735_GREEN);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_CYAN);
 ST7735_DrawString(0, 0, "CYAN", Font_11x18, ST7735_BLACK, ST7735_CYAN);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_MAGENTA);
 ST7735_DrawString(0, 0, "MAGENTA", Font_11x18, ST7735_BLACK, ST7735_MAGENTA);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_YELLOW);
 ST7735_DrawString(0, 0, "YELLOW", Font_11x18, ST7735_BLACK, ST7735_YELLOW);
 HAL_Delay(500);

 ST7735_FillScreen(ST7735_WHITE);
 ST7735_DrawString(0, 0, "WHITE", Font_11x18, ST7735_BLACK, ST7735_WHITE);
 HAL_Delay(500);

 // Draw circles
 ST7735_FillScreen(ST7735_BLACK);
 for (int i = 0; i < ST7735_GetHeight() / 2; i += 2)
 {
  ST7735_DrawCircle(ST7735_GetWidth() / 2, ST7735_GetHeight() / 2, i, ST7735_YELLOW);
 }
 HAL_Delay(1000);

 ST7735_FillScreen(ST7735_BLACK);
 ST7735_FillTriangle(0, 0, ST7735_GetWidth() / 2, ST7735_GetHeight(), ST7735_GetWidth(), 0, ST7735_RED);
 HAL_Delay(1000);

 ST7735_FillScreen(ST7735_BLACK);
 ST7735_DrawImage(0, 0, 128, 128, (uint16_t*) test_img_128x128);
 HAL_Delay(3000);

 r++;
}
/* USER CODE END 0 */
Та додаємо код що ініціалізує дисплей, вмикає підсвітку, та по колу запускає демонстрацію:
/* USER CODE BEGIN 2 */
  ST7735_Init();
  ST7735_Backlight_On();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   demoTFT();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */

Файли для завантаження

Відео інструкція


Відео-демонстрація


пʼятниця, 15 березня 2019 р.

Mbed: Знайомство з Mbed Studio та створення тестової програми blinky

Передмова

Нещодавно вийшла "Mbed Studio IDE" для розробки додатків і бібліотек "Mbed OS 5", яка вміщає в собі всі необхідні залежності та інструменти в одному пакеті, що дозволяє створювати, компілювати та налагоджувати програми Mbed на вашому комп'ютері. Це і програмування в стилі Arduino IDE, що допоможе легко стартувати людям, які звикли до Arduino, та хочуть перейти на мікроконтролери STM32. Це і програмування за допомоги HAL бібліотек, для тих, хто вже знайомий з сімейством STM32, та віддає перевагу цим бібліотекам. В цій статті розглянемо встановлення "Mbed Studio IDE", та створення не простого мікроконтролерного "Hello, world!", яким не тільки поблимаємо вбудованим світлодіодом. А щоб переконатись в легкості і доступності розробки на всі смаки, зробімо так - ініціалізуємо і поблимаємо вбудованим світлодіодом на PA_5 за допомоги mbedOS, ініціалізуємо і поблимаємо зовнішнім світлодіодом на PA_9 (який треба приєднати до плати самотужки) за допомоги HAL драйверів. Та не просто поблимаємо ними по черзі з "delay" в основному циклі, а за допомоги переривань з різною частотою. Плюс в основному циклі будемо передавати по UART2 (цей UART2 також надсилає данні і на USB_COM_PORT) лічильник секунд. Піддослідною платою буде STM32F103 Nucleo.

Встановлення Mbed Studio IDE

Завантажте та запустіть інсталяційний файл з "Mbed Studio IDE". Встановлення стандартне без якихось особливостей. Слідуйте вказівкам інсталяційної програми. Після встановлення запустіть Mbed Studio IDE. Та підключіть свою плату Nucleo до USB роз'єму.

Створення демонстраційного проекту

  1. File -> New Program -> Select Example Code "empty Mbed OS program" -> Program name "my_test_blinky" 
    Створення проекту
  2. Відкрийте файл main.cpp, клацнувши по назві файлу в дереві проекту. Зітріть весь текст в полі для коду і вставте цей демонстраційний код (код добре прокоментований додаткових пояснень не потребує):
    #include "mbed.h"
    
    RawSerial pc(USBTX, USBRX);     // tx, rx ініціалізація UART2 для передачі/прийому даних по USB COM PORT
    
    DigitalOut  ledGreen(LED2);     // ініціалізація вбудованого на платі Nucleo світлодіода як цифровий віхід
    
    Ticker flipper1;                // створення ticker з назвою flipper1
    Ticker flipper2;                // створення ticker з назвою flipper2
    
    //--Оголошення прототипів функцій----
    static void flip1(void);        // функція перекидання стану світлодіода PA_5 за допомоги MbedOS
    static void flip2(void);        // функція перекидання стану світлодіода PA_9 за допомоги HAL
    static void ledWhiteInit(void); // функція ініціалізації ніжки PA_9 за допомоги HAL драйверу
    //-----------------------------------
    
    // main() runs in its own thread in the OS
    int main()
    {   
        ledWhiteInit();                // викликаємо функцію ініциалізації ніжки PA_9 за допомоги HAL драйверу
        int counter = 0;               // ініціалізуємо змінну для лічильника секунд
        flipper1.attach(&flip1, 0.5);  // закріплюємо за flipper1 виклик функції flip1 з частотою два рази на секунду
        flipper2.attach(&flip2, 1.5);  // закріплюємо за flipper2 виклик функції flip2 з частотою раз на півтори секунди
        pc.baud(115200);               // встановлюємо швидкість обміну даними по UART2 як 115200
        pc.printf("Hello UART!\n\r");  // надсилаємо привітання в usb_com_port
    
        while (true)
        {
            pc.printf("counter sec = %d\n\r", counter++);   // надсилаємо в usb_com_port значення лічильника
            wait(1.0);                                      // чекаємо 1 секунду
        }
    }
    
    //--функція ініціалізації ніжки PA_9 за допомоги HAL драйверу--
    static void ledWhiteInit(void)
    {
        __HAL_RCC_GPIOA_CLK_ENABLE();                           // викликаємо тактування порту А
        
        GPIO_InitTypeDef GPIO_InitStruct;                       // створюємо структуру для налаштування
    
        GPIO_InitStruct.Pin = GPIO_PIN_9;                       // ніжка 9
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;             // режим вихід
        GPIO_InitStruct.Pull = GPIO_NOPULL;                     
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;           
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);                 // ініціалізуємо
    
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_9, GPIO_PIN_RESET);   // встановлюємо початковий стан "вимкнено"
    }
    //-------------------------------------------------------------
    
    //--функція зміни стану світлодіода на PA_5--------------------
    static void flip1(void)
    {    
        ledGreen = !ledGreen;
    }
    //-------------------------------------------------------------
    
    //--функція зміни стану світлодіода на PA_9--------------------
    static void flip2(void)
    {    
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_9);
    }
    //-------------------------------------------------------------
    
  3. Натисніть кнопку "Build program". Перший раз будуватись програма буде довго (декілька хвилин). Будуть завантажуватись всі бібліотеки, залежності і драйвери, які входять до MbedOS. По завершенню компіляції натисніть кнопку "Run program". Ваша програма заллється до пам'яті мікроконтролеру і автоматично запуститься.
  4. З різною частотою заблимають світлодіоди. А запустивши якусь термінальну програму і налаштував її на COM port, до якого під'єднано ваш Nucleo, та швидкість 115200 побачите там лічильник секунд:
    Термінал

Короткий огляд інтерфейсу Mbed Studio IDE

Інтерфейс Mbed Studio IDE
  1. Яка зараз активна програма для розробки;
  2. Яка плата під'єднана для розробки;
  3. Який профіль розробки: debug, develop, release;
  4. Компілювання програми;
  5. Заливка і запуск програми на мікроконтролері;
  6. Поле для редагування коду;
  7. Дерево проекту.

пʼятниця, 1 березня 2019 р.

STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780 по I2C шині, або по 4-х бітній шині


Update 11.05.2019: Виправив невиличку помилку, та додав підтримку дисплею LCD2004. Для використання дисплею 2004 в файлі "stm32_device.h" розкоментувати рядок з "#define USE_LCD2004".

Update 25.04.2022: Додано до бібліотеки підтримку декількох мікроконтролерів: STM32 HAL, PSoC PDL, Arduino framework. 

Передмова

У попередній статті "STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780" надано бібліотеку для роботи з дисплеєм, виключно, по 4-х бітовій шині. На прохання моїх читачів, допрацював бібліотеку і для роботи з цими дисплеями по шині I2C. Підключення по шині I2C вигідно економить, як кількість з'єднань, так і ніжки мікроконтролеру. Ця бібліотека по своєму функціоналу повністю сумісна з попередньою. Додано і модифіковано тільки роботу з шиною I2C. В варіанті роботи з шиною I2C додається дві функції які вмикають і вимикають підсвітку дисплея. Обрати роботу по шині I2C чи 4-х бітний варіант можна в файлі налаштувань бібліотеки.

Компоненти проекту

  1. Дошка "Синя пігулка"
  2. ST-Link V2
  3. Дисплей LCD1602
  4. Адаптер I2C шини для дисплею PCF8574A

CubeMXforSTM32

В CubeMX створюємо новий проект. Обираємо свій мікроконтролер, або дошку розробника. 
Налаштування шини I2C
  1. Обираємо шину I2C до якої під'єднано дисплей - I2C1
  2. Режим шини - I2C
  3. Налаштування шини - за замовчуванням, нічого не міняв
  4. Дивимось розташування на шпильках сигналів SDA і SCL
Генеруємо проект. Називаємо його, наприклад "STM32F103C8T6_I2C_LCD1602". По завершенню генерації відкриваємо свій засіб розробки.

STM32CubeIDE

Додаємо до проекту файли бібліотеки. Файли "stm32_device.h" та "hd44780.h" до теки "inc" проекту, а файл "hd44780.c" до теки "src" проекту.
Відкриваємо файл проекту "main.c", та підключаємо до головного файлу заголовний файл бібліотеки "hd44780.h" та файл "stm32_device.h"
/* USER CODE BEGIN Includes */
#include "hd44780.h"
#include "stm32_device.h"
/* USER CODE END Includes */
Далі переходимо до файлу "stm32_device.h":
#include "main.h"

#define USE_I2C_BUS
//#define USE_LCD2004

//-------------------------------
/* SET LCD<->MCU CONNECTIONS */
//-------------------------------
#ifdef USE_I2C_BUS

#define LCD_I2C_PORT  hi2c1
#define LCD_I2C_ADDRESS  0x3F

#define LCD_I2C_ADDRESS_8B (LCD_I2C_ADDRESS << 1)
#define PIN_RS      (1 << 0)
#define PIN_EN      (1 << 2)
#define BACKLIGHT   3

extern I2C_HandleTypeDef LCD_I2C_PORT;

#else
Де визначаємо що будемо використовувати шину I2C "#define USE_I2C_BUS". Як потрібна робота з 4-х бітною шиною, то цей рядок потрібно за коментувати і діяти по інструкції з попередньої статті "STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780".
Далі налаштовуємо до якого саме порту під'єднано дисплей і яку адресу має дисплей на шині I2C - це рядки "#define LCD_I2C_PORT hi2c1" та "#define LCD_I2C_ADDRESS 0x3F".
Цього достатньо.
Якщо використовуєте дисплей LCD2004, то розкоментуйте рядок "#define USE_LCD2004" в файлі "stm32_device.h"

Демонстраційна програма

Демонстраційний код візьміть з GIT або з попередньої статті "STM32: Бібліотека для роботи з LCD WH1602(4) або іншим сумісним дисплеєм з контролером HD44780". Та пам'ятайте, що для увімкнення, або вимкнення підсвітки дисплею, в варіанті для шини I2C, є окремі функції в бібліотеці.

Архів з бібліотекою

Фото проекту

LCD1602
LCD2004

Відео посібник



вівторок, 28 серпня 2018 р.

STM32: підключення SD CARD по SPI з FATFS

Передмова

Знадобилось мені в одному проекті вести запис всіляких подій у файл. Просто короткі текстові рядки з датою/часом і що за подія відбулась. Тому максимально просто і не заморочувався з DMA. Бібліотека FATFS є в комплекті з CubeMX, а ось вже як саме буде працювати мікроконтролер з флешкою по шині SPI на рівні залізяччя, треба вже потурбуватись самому. І дописати декілька функцій в заздалегідь призначених для цього місцях. Ідею поцупив звідси. Дуже стисло поділюсь з вами робочим прикладом. Хто захоче розібратись детально, то вже користуйтесь пошуком.

Залізяччя для макету

Схема макету

Зробіть з'єднання відповідно до схеми:
Макет для тестування
Клацайте на зображення щоб збільшити.

CubeMX for STM32

Запускаємо CubeMX, обираємо свій чип і робимо налаштування:
Налаштування вкладки pinout
  1. Додаємо до проекту бібліотеку FATFS
  2. Вмикаємо тактування від зовнішнього кварцового резонатору
  3. Вмикаємо Debug - Serial Wire
  4. Вмикаємо SPI2
  5. Та не забуваємо налаштувати PB12 на вихід GPIO_Output і надати ім'я "CS_SD_CARD", а також PB1 на вихід GPIO_Output і надати ім'я "LED_RED"
Переходимо до вкладки "clock configuration" і робимо так як на малюнку:
Налаштування вкладки clock configuration
Всі інші налаштування за замовчуванням, нічого не міняв.

Надаємо ім'я своєму проекту "STM32F103C8_SD_CARD_DEMO", вказуємо шлях, засіб розробки та генеруємо код.

Код для роботи FATFS по SPI

Відкриваємо в своєму засобі розробки згенерований CubeMX проект "STM32F103C8_SD_CARD_DEMO". Та перш за все навчимо бібліотеку FATFS працювати з нашою SD Card по шині SPI. Відкриваємо файл "user_diskio.h" і в секцію /* USER CODE BEGIN 0 */ такі рядки: 
  • #define SPI hspi2
  • extern SPI_HandleTypeDef SPI;
  • void sdcard_systick_timerproc(void);
Надалі, для зручності буду додавати весь код в користувацьких секціях і функціях де необхідно зробити зміни.

Ось так буде виглядати секція "USER CODE 0" у файлі "user_diskio.h" з нашим кодом:
/* USER CODE BEGIN 0 */
#define SPI hspi2
/* Includes ------------------------------------------------------------------*/
/* Exported types ------------------------------------------------------------*/
extern SPI_HandleTypeDef SPI;
/* Exported constants --------------------------------------------------------*/
/* Exported functions ------------------------------------------------------- */
extern Diskio_drvTypeDef  USER_Driver;

/*-----------------------------------------------------------------------*/
/* Device Timer Interrupt Procedure  (Platform dependent)                */
/*-----------------------------------------------------------------------*/
/* This function must be called in period of 1ms                        */

void sdcard_systick_timerproc(void);
/* USER CODE END 0 */

Далі відкриваємо файл "user_diskio.c" і до секції "/* USER CODE DECL */" вносимо необхідні визначення, змінні, функції.

Секція "/* USER CODE DECL */" в файлі "user_diskio.c" тепер буде виглядати так:
/* USER CODE BEGIN DECL */

/* Includes ------------------------------------------------------------------*/
#include <string.h>
#include "ff_gen_drv.h"

#include "user_diskio.h"
#include "stdbool.h"
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Definitions for MMC/SDC command */
#define CMD0 (0x40+0) /* GO_IDLE_STATE */
#define CMD1    (0x40+1) /* SEND_OP_COND */
#define CMD8    (0x40+8) /* SEND_IF_COND */
#define CMD9    (0x40+9) /* SEND_CSD */
#define CMD10 (0x40+10) /* SEND_CID */
#define CMD12 (0x40+12) /* STOP_TRANSMISSION */
#define CMD16 (0x40+16) /* SET_BLOCKLEN */
#define CMD17 (0x40+17) /* READ_SINGLE_BLOCK */
#define CMD18 (0x40+18) /* READ_MULTIPLE_BLOCK */
#define CMD23 (0x40+23) /* SET_BLOCK_COUNT */
#define CMD24 (0x40+24) /* WRITE_BLOCK */
#define CMD25 (0x40+25) /* WRITE_MULTIPLE_BLOCK */
#define CMD41 (0x40+41) /* SEND_OP_COND (ACMD) */
#define CMD55 (0x40+55) /* APP_CMD */
#define CMD58 (0x40+58) /* READ_OCR */

#define SELECT() HAL_GPIO_WritePin(CS_SD_CARD_GPIO_Port, CS_SD_CARD_Pin, GPIO_PIN_RESET)
#define DESELECT() HAL_GPIO_WritePin(CS_SD_CARD_GPIO_Port, CS_SD_CARD_Pin, GPIO_PIN_SET)
/* Private variables ---------------------------------------------------------*/
/* Disk status */
static volatile DSTATUS Stat = STA_NOINIT;

static BYTE CardType; /* b0:MMC, b1:SDC, b2:Block addressing */
static BYTE PowerFlag = 0; /* indicates if "power" is on */
static volatile BYTE Timer1, Timer2; /* 100Hz decrement timer */

static void xmit_spi(BYTE Data)
{
 while (HAL_SPI_GetState(&SPI) != HAL_SPI_STATE_READY);
 HAL_SPI_Transmit(&SPI, &Data, 1, 5000);
}

static BYTE rcvr_spi(void)
{
 unsigned char Dummy, Data;
 Dummy = 0xFF;
 Data = 0;
 while ((HAL_SPI_GetState(&SPI) != HAL_SPI_STATE_READY));
 HAL_SPI_TransmitReceive(&SPI, &Dummy, &Data, 1, 5000);

 return Data;
}

static void rcvr_spi_m(BYTE *dst)
{
 *dst = rcvr_spi();
}

/*-----------------------------------------------------------------------*/
/* Wait for card ready                                                   */
/*-----------------------------z------------------------------------------*/

static BYTE wait_ready(void)
{
 BYTE res;

 Timer2 = 50;
 rcvr_spi();
 do
  res = rcvr_spi();
 while ((res != 0xFF) && Timer2);

 return res;
}


static bool rcvr_datablock(BYTE *buff, /* Data buffer to store received data */
UINT btr /* Byte count (must be even number) */
)
{
 BYTE token;

 Timer1 = 10;
 do { /* Wait for data packet in timeout of 100ms */
  token = rcvr_spi();
 } while ((token == 0xFF) && Timer1);
 if (token != 0xFE)
  return false; /* If not valid data token, retutn with error */

 do { /* Receive the data block into buffer */
  rcvr_spi_m(buff++);
  rcvr_spi_m(buff++);
 } while (btr -= 2);
 rcvr_spi(); /* Discard CRC */
 rcvr_spi();

 return true; /* Return with success */
}

/*-----------------------------------------------------------------------*/
/* Send a data packet to MMC                                             */
/*-----------------------------------------------------------------------*/

#if _READONLY == 0
static bool xmit_datablock(const BYTE *buff, /* 512 byte data block to be transmitted */
BYTE token /* Data/Stop token */
)
{
 BYTE resp, wc;
 uint32_t i = 0;

 if (wait_ready() != 0xFF)
  return false;

 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);

  rcvr_spi();
  rcvr_spi();

  while (i <= 64) {
   resp = rcvr_spi(); /* Reveive data response */
   if ((resp & 0x1F) == 0x05) /* If not accepted, return with error */
    break;
   i++;
  }
  while (rcvr_spi() == 0)
   ;
 }
 if ((resp & 0x1F) == 0x05)
  return true;
 else
  return false;
}
#endif /* _READONLY */

static void power_on(void)
{
 unsigned char i, cmd_arg[6];
 unsigned int Count = 0x1FFF;

 DESELECT();

 for (i = 0; i < 10; i++)
  xmit_spi(0xFF);

 SELECT();

 cmd_arg[0] = (CMD0 | 0x40);
 cmd_arg[1] = 0;
 cmd_arg[2] = 0;
 cmd_arg[3] = 0;
 cmd_arg[4] = 0;
 cmd_arg[5] = 0x95;

 for (i = 0; i < 6; i++)
  xmit_spi(cmd_arg[i]);

 while ((rcvr_spi() != 0x01) && Count)
  Count--;

 DESELECT();
 xmit_spi(0XFF);

 PowerFlag = 1;
}

static void power_off(void) {
 PowerFlag = 0;
}
/*-----------------------------------------------------------------------*/
/* Send a command packet to MMC                                          */
/*-----------------------------------------------------------------------*/

static BYTE send_cmd(BYTE cmd, /* Command byte */
DWORD arg /* Argument */
)
{
 BYTE n, res;

 if (wait_ready() != 0xFF)
  return 0xFF;

 /* Send command packet */
 xmit_spi(cmd); /* Command */
 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;
 if (cmd == CMD0)
  n = 0x95; /* CRC for CMD0(0) */
 if (cmd == CMD8)
  n = 0x87; /* CRC for CMD8(0x1AA) */
 xmit_spi(n);

 /* Receive command response */
 if (cmd == CMD12)
  rcvr_spi(); /* Skip a stuff byte when stop reading */
 n = 10; /* Wait for a valid response in timeout of 10 attempts */
 do
  res = rcvr_spi();
 while ((res & 0x80) && --n);

 return res; /* Return with the response value */
}

static int chk_power(void) /* Socket power state: 0=off, 1=on */
{
 return PowerFlag;
}

/*-----------------------------------------------------------------------*/
/* Device Timer Interrupt Procedure  (Platform dependent)                */
/*-----------------------------------------------------------------------*/
/* This function must be called in period of 1ms                        */

void disk_timerproc(void)
{
 uint8_t n;

 n = Timer1; /* 100Hz decrement timer */
 if (n)
  Timer1 = --n;
 n = Timer2;
 if (n)
  Timer2 = --n;

}

volatile unsigned short int sdcard_timer;

void inline sdcard_systick_timerproc(void)
{
 ++sdcard_timer;
 if (sdcard_timer >= 10)
 {
  sdcard_timer = 0;
  disk_timerproc();
 }
}
/* USER CODE END DECL */

А далі вносимо зміни до функцій в файлі user_diskio.c:
  • DSTATUS USER_initialize (BYTE pdrv);
  • DSTATUS USER_status (BYTE pdrv);
  • DRESULT USER_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count);
  • DRESULT USER_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count);
  • DRESULT USER_ioctl (BYTE pdrv, BYTE cmd, void *buff);
Функція USER_initialize:
/**
  * @brief  Initializes a Drive
  * @param  pdrv: Physical drive number (0..)
  * @retval DSTATUS: Operation status
  */
DSTATUS USER_initialize (
 BYTE pdrv           /* Physical drive nmuber to identify the drive */
)
{
  /* USER CODE BEGIN INIT */
 BYTE n, ty, ocr[4];

 if (pdrv)
  return STA_NOINIT; /* Supports only single drive */
 if (Stat & STA_NODISK)
  return Stat; /* No card in the socket */

 power_on(); /* Force socket power on */
 //send_initial_clock_train();

 SELECT(); /* CS = L */
 ty = 0;
 if (send_cmd(CMD0, 0) == 1)
 { /* Enter Idle state */
  Timer1 = 100; /* Initialization timeout of 1000 msec */
  if (send_cmd(CMD8, 0x1AA) == 1)
  { /* SDC Ver2+ */
   for (n = 0; n < 4; n++)
    ocr[n] = rcvr_spi();
   if (ocr[2] == 0x01 && ocr[3] == 0xAA)
   { /* The card can work at vdd range of 2.7-3.6V */
    do {
     if (send_cmd(CMD55, 0) <= 1
       && send_cmd(CMD41, 1UL << 30) == 0)
      break; /* ACMD41 with HCS bit */
    } while (Timer1);
    if (Timer1 && send_cmd(CMD58, 0) == 0)
    { /* Check CCS bit */
     for (n = 0; n < 4; n++)
      ocr[n] = rcvr_spi();
     ty = (ocr[0] & 0x40) ? 6 : 2;
    }
   }
  }
  else
  { /* SDC Ver1 or MMC */
   ty = (send_cmd(CMD55, 0) <= 1 && send_cmd(CMD41, 0) <= 1) ? 2 : 1; /* SDC : MMC */
   do {
    if (ty == 2)
    {
     if (send_cmd(CMD55, 0) <= 1 && send_cmd(CMD41, 0) == 0)
      break; /* ACMD41 */
    }
    else
    {
     if (send_cmd(CMD1, 0) == 0)
      break; /* CMD1 */
    }
   } while (Timer1);
   if (!Timer1 || send_cmd(CMD16, 512) != 0) /* Select R/W block length */
    ty = 0;
  }
 }
 CardType = ty;
 DESELECT(); /* CS = H */
 rcvr_spi(); /* Idle (Release DO) */

 if (ty) /* Initialization succeded */
  Stat &= ~STA_NOINIT; /* Clear STA_NOINIT */
 else
  /* Initialization failed */
  power_off();

 return Stat;
  /* USER CODE END INIT */
}

Функція USER_status:
/**
  * @brief  Gets Disk Status 
  * @param  pdrv: Physical drive number (0..)
  * @retval DSTATUS: Operation status
  */
DSTATUS USER_status (
 BYTE pdrv       /* Physical drive number to identify the drive */
)
{
  /* USER CODE BEGIN STATUS */
 if (pdrv)
  return STA_NOINIT; /* Supports only single drive */
 return Stat;
  /* USER CODE END STATUS */
}

Функція USER_read:
/**
  * @brief  Reads Sector(s) 
  * @param  pdrv: Physical drive number (0..)
  * @param  *buff: Data buffer to store read data
  * @param  sector: Sector address (LBA)
  * @param  count: Number of sectors to read (1..128)
  * @retval DRESULT: Operation result
  */
DRESULT USER_read (
 BYTE pdrv,      /* Physical drive nmuber to identify the drive */
 BYTE *buff,     /* Data buffer to store read data */
 DWORD sector,   /* Sector address in LBA */
 UINT count      /* Number of sectors to read */
)
{
  /* USER CODE BEGIN READ */
 if (pdrv || !count)
  return RES_PARERR;
 if (Stat & STA_NOINIT)
  return RES_NOTRDY;

 if (!(CardType & 4))
  sector *= 512; /* Convert to byte address if needed */

 SELECT(); /* CS = L */

 if (count == 1)
 { /* Single block read */
  if ((send_cmd(CMD17, sector) == 0) /* READ_SINGLE_BLOCK */
  && rcvr_datablock(buff, 512))
   count = 0;
 }
 else
 { /* Multiple block read */
  if (send_cmd(CMD18, sector) == 0)
  { /* READ_MULTIPLE_BLOCK */
   do {
    if (!rcvr_datablock(buff, 512))
     break;
    buff += 512;
   } while (--count);
   send_cmd(CMD12, 0); /* STOP_TRANSMISSION */
  }
 }

 DESELECT(); /* CS = H */
 rcvr_spi(); /* Idle (Release DO) */

 return count ? RES_ERROR : RES_OK;
  /* USER CODE END READ */
}

Функція USER_write:
/**
  * @brief  Writes Sector(s)  
  * @param  pdrv: Physical drive number (0..)
  * @param  *buff: Data to be written
  * @param  sector: Sector address (LBA)
  * @param  count: Number of sectors to write (1..128)
  * @retval DRESULT: Operation result
  */
#if _USE_WRITE == 1
DRESULT USER_write (
 BYTE pdrv,          /* Physical drive nmuber to identify the drive */
 const BYTE *buff,   /* Data to be written */
 DWORD sector,       /* Sector address in LBA */
 UINT count          /* Number of sectors to write */
)
{ 
  /* USER CODE BEGIN WRITE */
  /* USER CODE HERE */
 if (pdrv || !count)
  return RES_PARERR;
 if (Stat & STA_NOINIT)
  return RES_NOTRDY;
 if (Stat & STA_PROTECT)
  return RES_WRPRT;

 if (!(CardType & 4))
  sector *= 512; /* Convert to byte address if needed */

 SELECT(); /* CS = L */

 if (count == 1)
 { /* Single block write */
  if ((send_cmd(CMD24, sector) == 0) /* WRITE_BLOCK */
  && xmit_datablock(buff, 0xFE))
   count = 0;
 }
 else
 { /* Multiple block write */
  if (CardType & 2)
  {
   send_cmd(CMD55, 0);
   send_cmd(CMD23, count); /* ACMD23 */
  }
  if (send_cmd(CMD25, sector) == 0)
  { /* WRITE_MULTIPLE_BLOCK */
   do {
    if (!xmit_datablock(buff, 0xFC))
     break;
    buff += 512;
   } while (--count);
   if (!xmit_datablock(0, 0xFD)) /* STOP_TRAN token */
    count = 1;
  }
 }

 DESELECT(); /* CS = H */
 rcvr_spi(); /* Idle (Release DO) */

 return count ? RES_ERROR : RES_OK;
  /* USER CODE END WRITE */
}
#endif /* _USE_WRITE == 1 */

Функція USER_ioctl:
/**
  * @brief  I/O control operation  
  * @param  pdrv: Physical drive number (0..)
  * @param  cmd: Control code
  * @param  *buff: Buffer to send/receive control data
  * @retval DRESULT: Operation result
  */
#if _USE_IOCTL == 1
DRESULT USER_ioctl (
 BYTE pdrv,      /* Physical drive nmuber (0..) */
 BYTE cmd,       /* Control code */
 void *buff      /* Buffer to send/receive control data */
)
{
  /* USER CODE BEGIN IOCTL */
 DRESULT res;
 BYTE n, csd[16], *ptr = buff;
 WORD csize;

 if (pdrv)
  return RES_PARERR;

 res = RES_ERROR;

 if (cmd == CTRL_POWER)
 {
  switch (*ptr) {
  case 0: /* Sub control code == 0 (POWER_OFF) */
   if (chk_power())
    power_off(); /* Power off */
   res = RES_OK;
   break;
  case 1: /* Sub control code == 1 (POWER_ON) */
   power_on(); /* Power on */
   res = RES_OK;
   break;
  case 2: /* Sub control code == 2 (POWER_GET) */
   *(ptr + 1) = (BYTE) chk_power();
   res = RES_OK;
   break;
  default:
   res = RES_PARERR;
  }
 }
 else
 {
  if (Stat & STA_NOINIT)
   return RES_NOTRDY;

  SELECT(); /* CS = L */

  switch (cmd) {
  case GET_SECTOR_COUNT: /* Get number of sectors on the disk (DWORD) */
   if ((send_cmd(CMD9, 0) == 0) && rcvr_datablock(csd, 16))
   {
    if ((csd[0] >> 6) == 1)
    { /* SDC ver 2.00 */
     csize = csd[9] + ((WORD) csd[8] << 8) + 1;
     *(DWORD*) buff = (DWORD) csize << 10;
    }
    else
    { /* MMC or SDC ver 1.XX */
     n = (csd[5] & 15) + ((csd[10] & 128) >> 7)
       + ((csd[9] & 3) << 1) + 2;
     csize = (csd[8] >> 6) + ((WORD) csd[7] << 2)
       + ((WORD) (csd[6] & 3) << 10) + 1;
     *(DWORD*) buff = (DWORD) csize << (n - 9);
    }
    res = RES_OK;
   }
   break;

  case GET_SECTOR_SIZE: /* Get sectors on the disk (WORD) */
   *(WORD*) buff = 512;
   res = RES_OK;
   break;

  case CTRL_SYNC: /* Make sure that data has been written */
   if (wait_ready() == 0xFF)
    res = RES_OK;
   break;

  case MMC_GET_CSD: /* Receive CSD as a data block (16 bytes) */
   if (send_cmd(CMD9, 0) == 0 /* READ_CSD */
   && rcvr_datablock(ptr, 16))
    res = RES_OK;
   break;

  case MMC_GET_CID: /* Receive CID as a data block (16 bytes) */
   if (send_cmd(CMD10, 0) == 0 /* READ_CID */
   && rcvr_datablock(ptr, 16))
    res = RES_OK;
   break;

  case MMC_GET_OCR: /* Receive OCR as an R3 resp (4 bytes) */
   if (send_cmd(CMD58, 0) == 0)
   { /* READ_OCR */
    for (n = 0; n < 4; n++)
     *ptr++ = rcvr_spi();
    res = RES_OK;
   }

  default:
   res = RES_PARERR;
  }

  DESELECT(); /* CS = H */
  rcvr_spi(); /* Idle (Release DO) */
 }

 return res;
  /* USER CODE END IOCTL */
}
#endif /* _USE_IOCTL == 1 */

Демо код

Відкриваємо файл stm32f1xx_it.c і до секції /* USER CODE 0 */ додаємо рядок:
/* USER CODE BEGIN 0 */
extern void sdcard_systick_timerproc(void);
/* USER CODE END 0 */

А до функції "SysTick_Handler" додаємо виклик функції "sdcard_systick_timerproc":
void SysTick_Handler(void)
{
  /* USER CODE BEGIN SysTick_IRQn 0 */

  /* USER CODE END SysTick_IRQn 0 */
  HAL_IncTick();
  HAL_SYSTICK_IRQHandler();
  /* USER CODE BEGIN SysTick_IRQn 1 */
  sdcard_systick_timerproc();
  /* USER CODE END SysTick_IRQn 1 */
}

І нарешті відкриваємо файл main.c нашого проекту і в секцію /* USER CODE 2 */ додаємо такий демо-код:
/* USER CODE BEGIN 2 */
  //-------------SD DEMO START-----------------
  char buffer[512];
  static FATFS g_sFatFs;
  FRESULT fresult;
  FIL file;
  int len;
  UINT bytes_written;
  HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_SET);
  fresult = f_mount(&g_sFatFs, "0:", 0);        //mount SD card
  fresult = f_open(&file, "testfile.txt", FA_OPEN_ALWAYS | FA_WRITE); //open file on SD card
  fresult = f_lseek(&file, file.fsize);         //go to the end of the file
  len = sprintf(buffer, "Hello, World!\r\nThis is demonstration write on SD Card\r\n");    //generate some string
  fresult = f_write(&file, buffer, len, &bytes_written);     //write data to the file
  fresult = f_close(&file);
  HAL_GPIO_WritePin(LED_RED_GPIO_Port, LED_RED_Pin, GPIO_PIN_RESET);
  //-------------SD DEMO END-----------------
  /* USER CODE END 2 */

Компілюємо проект, заливаємо до мікроконтролеру. Стартуємо мікроконтролер. Можна декілька разів перезавантажити. При цьому світлодіод має блимнути. Тепер можна витягти флешку з SD Card Adapter і вставити її до ПК та пересвідчитись що файл "testfile.txt" з декількома текстовими рядками "Hello, World!" та "This is demonstration write on SD Card" існує.

Файли