вівторок, 4 червня 2019 р.

STM32: Бібліотека для 7-сегментного 8 розрядного дисплею на MAX7219 по SPI шині


Передмова

Мабуть самий з найпоширеніших дисплеїв в радіоаматорскій практиці є семисегментний світлодіодний дисплей. Це й простота і дешевизна. Але й є недоліки. Це інформаційне обмеження щодо варіантів символів, які можна зобразити і велика кількість ніжок на мікроконтролері (7 або 8 ніжок для сегментів плюс на кожен розряд по 1 ніжці мікоконтролера).

За допомоги мікросхеми MAX7219 можна усунути недолік з великою кількістю ніжок. За допомоги однієї мікросхеми MAX7219 можна керувати 8 розрядним 7-ми сегментним дисплеєм, плюс режим декодування цифр, плюс регулювання яскравості свічення дисплею, плюс не потрібно турбуватись про динамічну індикацію. А данні до мікросхеми надсилати по шині SPI, яка обмежена швидкістю в 10 Мбіт/сек.

В цій статті розглянемо невеличку бібліотеку для зручного друку інформації на семисегментний дисплей з мікросхемою MAX7219:
  • Очищення дисплею
  • Встановлення рівню яскравості
  • Надсилання даних на MAX7219
  • Вмикання/вимикання дисплею
  • Встановлення/скидання режиму декодування
  • Друк цифр (символів) в заданому розряді
  • Друк чисел типу INT та FLOAT

Залізяччя

Для прикладу буду використовувати такі матеріали:

Створюємо проект в STM32CubeMX або STM32CubeIDE

Обираємо свій мікроконтролер або дошку. Та налаштовуємо проект.
Налаштування проекту в Cube
  1. Тиснемо RCC -> High Speed Clock (HSE) -> Crystal/Ceramic Resonator
  2. SYS -> Debug -> Serial Wire
  3. Тиснемо SPI1
  4. Обираємо шпильку Chip Select -> наприклад PB0, обираємо режим GPIO_OUTPUT, та називаємо її CS_MAX7219
  5. Обираємо режим для SPI1 -> Transmit only master
  6. Слідкуємо щоб Baud Rate не перевищував 10 MBits/sec, якщо перевищує збільшіть Prescaler (for Baud Rate)
Генеруємо проект та переходимо до підключення дисплейного модуля до мікроконтролера, завантаження бібліотеки та написання демо-коду.

Підключення дисплейного модуля до мікроконтролера

STM32F103C8T6 Дисплейний модуль
Vcc 3.3V
Vcc
GND
GND
PA7 SPI1_MOSI
DIN
PB0 CS_MAX7219
CS
PA5 SPI1_SCK
CLK

Бібліотека MAX7219

Завантажити бібліотеку можна з GitHub. Бібліотека складається з двох файлів: max7219.h та max7219.c та має такі функції:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void max7219_Init(uint8_t intensivity);
void max7219_SetIntensivity(uint8_t intensivity);
void max7219_Clean(void);
void max7219_SendData(uint8_t addr, uint8_t data);
void max7219_Turn_On(void);
void max7219_Turn_Off(void);
void max7219_Decode_On(void);
void max7219_Decode_Off(void);
void max7219_PrintDigit(MAX7219_Digits position, MAX7219_Numeric numeric, bool point);
MAX7219_Digits max7219_PrintItos(MAX7219_Digits position, int value);
MAX7219_Digits max7219_PrintNtos(MAX7219_Digits position, uint32_t value, uint8_t n);
MAX7219_Digits max7219_PrintFtos(MAX7219_Digits position, float value, uint8_t n);
  1. Ініціалізація дисплею. На вхід "рівень яскравості" (0x0 - 0xF);
  2. Встановлення рівня яскравості. На вхід "рівень яскравості" (0x0 - 0xF);
  3. Очищення дисплею;
  4. Надсилання в шину SPI до MAX7219 даних. На вхід: адреса, дані;
  5. Увімкнення дисплею;
  6. Вимкнення дисплею;
  7. Увімкнути режим декодування;
  8. Вимкнути режим декодування;
  9. Друк цифри (символу). На вхід: позиція (1 - 8), цифра (0 - 9), точка (true/false);
  10. Друк числа типу INT. На вхід: стартова позиція числа на дисплеї, число INT. На виході: поточна позиція курсору;
  11. Друк числа типу INT з фіксованою кількістю розрядів. На вхід: стартова позиція числа на дисплеї, число INT, кількість розрядів. На виході: поточна позиція курсору;
  12. Друк числа типу FLOAT. На вхід: стартова позиція числа на дисплеї, число FLOAT, кількість знаків після коми (не більше 4-х). На виході: поточна позиція курсору;

Демо-код

Відкриваємо свій засіб розробки та експортуємо, згенерований CubeMX або CubeIDE, код. 
Копіюємо файли бібліотеки до проекту. Файли з розширенням *.h до теки INC, файли з розширенням *.c до теки SRC.
Відкриваємо файл max7219.h та налаштовуємо бібліотеку:
  • Кількість розрядів #define NUMBER_OF_DIGITS 8
  • Порт SPI до якого приєднано дисплейний модуль #define SPI_PORT hspi1
Відкриваємо файл main.c і в секції USER CODE Includes вкладаємо нашу бібліотеку:
/* USER CODE BEGIN Includes */
#include "max7219.h"
/* USER CODE END Includes */

В користувацьку секцію 2 додаємо ініціалізацію дисплею з встановленим рівнем яскравості 7 і вмикаємо режим декодування цифр:
/* USER CODE BEGIN 2 */
  max7219_Init(7);
  max7219_Decode_On();
  /* USER CODE END 2 */

В безкінечному циклі друкуємо від'ємне число Pi. Та слово HELP. Чекаємо 1.5 секунди, очищаємо дисплей та друкуємо число 765 на 4-ри розряди (додається незначащий нуль), та ціле число 321. Знову чекаємо 1.5 секунди і все з початку.
/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   max7219_Clean();
   max7219_PrintFtos(DIGIT_8, -3.14, 2);
   max7219_PrintDigit(DIGIT_4, LETTER_H, false);
   max7219_PrintDigit(DIGIT_3, LETTER_E, false);
   max7219_PrintDigit(DIGIT_2, LETTER_L, false);
   max7219_PrintDigit(DIGIT_1, LETTER_P, false);
   HAL_Delay(1500);
   max7219_Clean();
   max7219_PrintNtos(DIGIT_8, 765, 4);
   max7219_PrintItos(DIGIT_3, 321);
   HAL_Delay(1500);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
Щоб друкувати довільні символи, то треба вимкнути режим декодування цифр і в масив static uint8_t SYMBOLS[] файлу max7219.c додати ці символи. А в нумерований список MAX7219_Numeric в файлі max7219.h додати позначення і порядковий номер в масиві - нового символу.

Приклад роботи

Приклад роботи

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

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

Передмова

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

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

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

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

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

Демо-код

Відкриваємо файл OneWire.h та налаштовуємо бібліотеку OneWire - зазначаємо на якому UART буде шина 1-Wire:
#define HUARTx    huart1
#define USARTx    USART1
Відкриваємо файл проекту main.c та додаємо заголовні файли бібліотек до коду:
/* USER CODE BEGIN Includes */
#include "OneWire.h"
#include "DallasTemperature.h"
/* USER CODE END Includes */
Додамо функцію відправки тексту по USART2 та функцію друкування адреси сенсорів:
/* USER CODE BEGIN PFP */
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++)
  {
 char buf[4];
 sprintf(buf, "%02X ", deviceAddress[i]);
 printf(buf);
  }
}
/* USER CODE END PFP */
Приклад розрахований для двох DS18B20 на одній лінії. Як у вас один сенсор, приберіть код для другого сенсора з індексом 1.
В секцію 2 додамо перевірку чи на лінії є якісь сенсори 1-Wire, взнаємо кількість підключених до лінії сенсорів, задамо роздільну здатність сенсорів, та прочитаємо їхню адресу і поточну роздільну здатність:
/* USER CODE BEGIN 2 */
  printf("Debug UART is OK!\r\n");

  if(OW_Reset() == OW_OK)
  {
   printf("OneWire devices are present :)\r\n");
  }
  else
  {
   printf("OneWire no devices :(\r\n");
  }

   // arrays to hold device address
  CurrentDeviceAddress insideThermometer;

  // locate devices on the bus
  printf("Locating devices...");
  DT_Begin();
  printf("Found ");
  char buf[8];
  sprintf(buf, "%d", DT_GetDeviceCount());
  printf(buf);
  printf(" devices.\r\n");

  // report parasite power requirements
  printf("Parasite power is: ");
  if (DT_IsParasitePowerMode()) printf("ON\r\n");
  else printf("OFF\r\n");

  if (!DT_GetAddress(insideThermometer, 0)) printf("Unable to find address for Device 0\r\n");

 printf("Device 0 Address: ");
 printAddress(insideThermometer);
 printf("\r\n");

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

  printf("Device 0 Resolution: ");
  sprintf(buf, "%d", DT_GetResolution(insideThermometer));
  printf(buf);
  printf("\r\n");

  if (!DT_GetAddress(insideThermometer, 1)) printf("Unable to find address for Device 1\r\n");

 printf("Device 1 Address: ");
 printAddress(insideThermometer);
 printf("\r\n");

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

 printf("Device 1 Resolution: ");
 sprintf(buf, "%d", DT_GetResolution(insideThermometer));
 printf(buf);
 printf("\r\n");
  /* USER CODE END 2 */
Приклад розрахований для двох DS18B20 на одній лінії. Як у вас один сенсор, приберіть код для другого сенсора з індексом 1.
А в бескінечному циклі раз на 2 секунди будемо опитувати сенсори і виводити в послідовний порт отриману температуру:
/* USER CODE BEGIN WHILE */
  while (1)
  {
 // call sensors.requestTemperatures() to issue a global temperature
 // request to all devices on the bus
 printf("Requesting temperatures...");
 DT_RequestTemperatures(); // Send the command to get temperatures
 printf("DONE\r\n");
 // 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("Temperature for the device 1 (index 0) is: ");
 sprintf(buf, "%.2f\r\n", DT_GetTempCByIndex(0));
 printf(buf);
 printf("Temperature for the device 2 (index 1) is: ");
 sprintf(buf, "%.2f\r\n", DT_GetTempCByIndex(1));
 printf(buf);
 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
Компілюємо, заливаємо. Запускаємо будь-яку термінальну програму і спостерігаємо:
Приклад роботи двох датчиків на лінії

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

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

Передмова

Бібліотека призначена для роботи з 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. В файлі lcd_cfg.h розкоментувати рядок з "#define USE_LCD2004".

Передмова

У попередній статті "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". Обираємо свій засіб розробки. В мене це Atolic Truestudio for STM32. Тиснемо "ОК". По завершенню генерації відкриваємо свій засіб розробки.

Atolic Truestudio for STM32

Відкриваємо File->Import->Existing Projects into Workspace->Next->Browse і вказуєте теку з проектом згенерованим CubeMX. 
Додаємо до проекту файли бібліотеки. Файли "lcd_cfg.h" та "hd44780.h" до теки "inc" проекту, а файл "hd44780.c" до теки "src" проекту.
Відкриваємо файл проекту "main.c", та підключаємо до головного файлу заголовний файл бібліотеки "hd44780.h":
/* USER CODE BEGIN Includes */
#include "hd44780.h"
/* USER CODE END Includes */
Далі переходимо до файлу "lcd_cfg.h":
#include "stm32f1xx.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
Де зазначаємо серію нашого мікроконтролера "stm32f1xx.h", визначаємо що будемо використовувати шину 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" в файлі lcd_cfg.h

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

Демонстраційний код візьміть з попередньої статті "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" існує.

Файли