Продолжаем тему программирования контроллера STM32F4.
И сегодня мы попробуем к данному контроллеру подключить последовательную FLASH-память серии W25Q.
Данная последовательная память может быть различной ёмкости — 8, 16, 32, 64, 128, 256 Мбит и т. д.
Подключается такая память по интерфейсу SPI, а также по многопроводным интерфейсам Dual SPI, Quad SPI и QPI.
Мы же пока будем подключим данную микросхему по обычному интерфейсу SPI.
Краткие основные характеристики W25Q:
Потребляемая мощность и температурный диапазон:
- Напряжение питания 2.7…3.6 В
- Типичный потребляемый ток: 4 мА (активный режим), <1 мкА (в режиме снижения мощности)
- Рабочий температурный диапазон -40°C…+85°C.
Гибкая архитектура с секторами размером 4 кбайт:
- Посекторное стирание (размер каждого сектора 4 кбайт)
- Блочное стирание (32кбайт и 64 кбайт)
- Программирование от 1 до 256 байт
- До 100 тыс. циклов стирания/записи
- 20-летнее хранение данных
Максимальная частота работы микросхемы:
- 104 МГц в режиме SPI
- 208/416 МГц — Dual / Quad SPI
Есть ещё много различных возможностей, но это самые основные.
Также микросхема существует в различных корпусах, но в большинстве случаев распространён корпус SMD SO8.
Распиновка микросхемы следующая
Назначение каждого вывода:
Номер вывода | Название | Ввод/вывод (I/O) | Назначение |
1 | /CS | I | Выбор чипа |
2 | DO (IO1) | I/O | Вывод данных (Ввод/вывод данных #1 для режимов S-SPI и D-SPI) |
3 | /WP (IO2) | I/O | Вход защиты записи (Ввод/вывод данных #2 для режима Q-SPI) |
4 | GND | Общий провод | |
5 | DI(IO0 ) | I/O | Ввод данных (Ввод/вывод данных #0 для режимов S-SPI и D-SPI) |
6 | CLK | I | Ввод тактовых импульсов |
7 | /HOLD or /RESET (IO3 ) | I/O | Ввод/вывод данных #3 для режима Q-SPI |
8 | VCC | Напряжение питания |
В зависимости от типа подключения схема подключения микросхемы к контроллеру также может различаться.
В режиме обычного SPI подключение будет следующее:
Следует отметить, что микросхема W25Q устроена так же как FLASH-память у stm32, то есть память у неё разбита на страницы по 256 байт, страницы объединены в секторы по 4096 байт, а секторы в блоки по 65536 байт. Организацию памяти микросхемы можно посмотреть на схеме
Перед тем как что-то записать, нужно стереть сектор (4096 байт) или блок (65536 байт). Можно стереть несколько секторов или блоков, или весь чип полностью. Во время стирания ячейки заполняются значениями 0xFF.
Каким образом организован процесс стирания, а также чтения и записи микросхемы, мы разберём в дальнейших занятиях, а сегодня наша задача — подключить микросхему к контроллеру, настроить проект для организации кода работы контроллера с микросхемой и в качестве испытания считать некоторые данные из неё.
Поэтому мы пока не будем забивать себе голову изучением всех регистров и их битов данной микросхемы W25Q, так как их там очень много, а будем их изучать по мере их использования.
Я буду использовать микросхему 25Q32FVSIG в корпусе SMD SO8, в которой 32 мегабита памяти, которую я припаял на переходник SO8-DIP, а также распаял штырьевые линейки для удобства подключения к контроллеру, получилась вот такая табуреточка
Подключим нашу микросхему к отладочной плате
Подключим отладочную плату к USB компьютера и приступим к проекту, который мы сегодня сделаем с нуля.
Запустим Cube MX создадим проект, выбрав для начала контроллер
Выберем отладчик
Включим HSE и задействуем кварцевый резонатор
В Clock Configuration внесем следующие настройки
Включим USART1, оставив все его настройки по умолчанию
У нас включились необходимые ножки
Включим SPI1
Увеличим делитель, так как у нас длинные провода, да и скорость нам сейчас не нужна
Задействуем на выход ножку PA4 — это у нас будет Chip Select
Увеличим скорость данной ножки, так как вдруг потом мы всё же решимся поднять частоту SPI
Настроим свойства проекта
Сгенерируем проект, откроем его в Cube IDE, и в файле main.c первым делом подключим стандартные библиотеки по работе со строками
1 2 3 |
/* USER CODE BEGIN Includes */ #include <stdio.h> #include <string.h> |
Добавим также более простые функции для приёма и передачи байтов по шине SPI
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* USER CODE BEGIN 0 */ //------------------------------------------------------------- void SPI1_Send (uint8_t *dt, uint16_t cnt) { HAL_SPI_Transmit (&hspi1, dt, cnt, 5000); } //------------------------------------------------------------- void SPI1_Recv (uint8_t *dt, uint16_t cnt) { HAL_SPI_Receive (&hspi1, dt, cnt, 5000); } //------------------------------------------------------------- |
Прежде чем работать с микросхемой, нужно произвести её сброс.
Чтобы сделать сброс, нужно сначала отправить команду разрешения сброса, а затем команду самого сброса
Объявим макросы для таких команд
1 2 3 4 5 |
/* USER CODE BEGIN PM */ //------------------------------------------------------------- #define W25_ENABLE_RESET 0x66 #define W25_RESET 0x99 //------------------------------------------------------------- |
Объявим также макросы для управления ножкой выбора
1 2 3 4 5 |
#define W25_RESET 0x99 //------------------------------------------------------------- #define cs_set() HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET) #define cs_reset() HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET) //------------------------------------------------------------- |
Также объявим буферы для приёма и передачи данных по SPI
1 2 3 |
/* USER CODE BEGIN PV */ uint8_t rx_buf[1025]; uint8_t tx_buf[10]; |
Ниже функции SPI1_Recv добавим функцию сброса микросхемы
1 2 3 4 5 6 7 8 9 10 |
//------------------------------------------------------------- void W25_Reset (void) { cs_set(); tx_buf[0] = W25_ENABLE_RESET; tx_buf[1] = W25_RESET; SPI1_Send(tx_buf, 2); cs_reset(); } //------------------------------------------------------------- |
Так как сегодня мы будем только читать данные из памяти микросхемы, то нам нужно сначала узнать, как это делается.
Сначала передаётся команда чтения данных из памяти. Таких команд несколько, нам нужна будет пока только вот эта
Данная команда работает только в обычном режиме SPI, а мы сейчас с таким и работаем.
После команды передаётся 24-битный адрес и после этого мы на проводе MISO получаем интересующие нас байты памяти. В принципе, почти так же, как и в случае с EEPROM, только здесь используется команда и адресация 24-разрядная.
Объявим макрос для данной команды
1 2 |
#define W25_RESET 0x99 #define W25_READ 0x03 |
Добавим функцию чтения определённого количества байтов из нашей микросхемы ниже функции W25_Reset
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//------------------------------------------------------------- void W25_Read_Data(uint32_t addr, uint8_t* data, uint32_t sz) { cs_set(); tx_buf[0] = W25_READ; tx_buf[1] = (addr >> 16) & 0xFF; tx_buf[2] = (addr >> 8) & 0xFF; tx_buf[3] = addr & 0xFF; SPI1_Send(tx_buf, 4); SPI1_Recv(data, sz); cs_reset(); } //------------------------------------------------------------- |
Ниже добавим функцию инициализации нашей микросхемы, в которой сначала произведём её сброс
1 2 3 4 5 6 7 8 |
//------------------------------------------------------------- void W25_Ini(void) { HAL_Delay(100); W25_Reset(); HAL_Delay(100); } //------------------------------------------------------------- |
Затем нам нужно будет считать идентификатор микросхемы, чтобы определить, какая именно микросхема у нас.
Таких команд также несколько, мы пока воспользуемся вот этой
Объявим для данной команды также макрос
1 2 |
#define W25_READ 0x03 #define W25_GET_JEDEC_ID 0x9f |
Ниже функции W25_Read_Data добавим функцию, возвращающую идентификатор микросхемы
1 2 3 4 5 6 7 8 9 10 11 12 |
//------------------------------------------------------------- uint32_t W25_Read_ID(void) { uint8_t dt[4]; tx_buf[0] = W25_GET_JEDEC_ID; cs_set(); SPI1_Send(tx_buf, 1); SPI1_Recv(dt,3); cs_reset(); return (dt[0] << 16) | (dt[1] << 8) | dt[2]; } //------------------------------------------------------------- |
В функции W25_Ini вызовем данную функцию
1 2 |
HAL_Delay(100); unsigned int id = W25_Read_ID(); |
Также вызовем функцию инициализации микросхемы в функции main()
1 2 |
/* USER CODE BEGIN 2 */ W25_Ini(); |
Объявим глобальный символьный массив
1 2 |
/* USER CODE BEGIN PV */ char str1[30]; |
А также объявим удобную структуру для хранения информации о микросхеме, а также переменную типа данной структуры
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* USER CODE BEGIN PV */ typedef struct { uint16_t PageSize; uint32_t PageCount; uint32_t SectorSize; uint32_t SectorCount; uint32_t BlockSize; uint32_t BlockCount; uint32_t NumKB; uint8_t SR1; uint8_t SR2; uint8_t SR3; }w25_info_t; w25_info_t w25_info; |
В функции W25_Ini отправим информацию об идентификаторе в терминальную программу
1 2 3 4 |
unsigned int id = W25_Read_ID(); HAL_UART_Transmit(&huart1,(uint8_t*)"\r\n",2,0x1000); sprintf(str1,"ID:0x%X\r\n",id); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); |
Соберём код, прошьём контроллер, запустим терминальную программу, перезагрузим контроллер и посмотрим, какой идентификатор считался из нашей микросхемы
Заглянем в документацию
Всё правильно.
Для микросхем различного объёма идентификатор разный, поэтому ниже добавим оператор вариантов, в теле которого в каждом кейсе выведем информацию о той или иной микросхеме на основе полученного идентификатора, предварительно обнулив старший байт
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); id &= 0x0000ffff; switch(id) { case 0x401A: w25_info.BlockCount=1024; sprintf(str1,"w25qxx Chip: w25q512\r\n"); break; case 0x4019: w25_info.BlockCount=512; sprintf(str1,"w25qxx Chip: w25q256\r\n"); break; case 0x4018: w25_info.BlockCount=256; sprintf(str1,"w25qxx Chip: w25q128\r\n"); break; case 0x4017: w25_info.BlockCount=128; sprintf(str1,"w25qxx Chip: w25q64\r\n"); break; case 0x4016: w25_info.BlockCount=64; sprintf(str1,"w25qxx Chip: w25q32\r\n"); break; case 0x4015: w25_info.BlockCount=32; sprintf(str1,"w25qxx Chip: w25q16\r\n"); break; case 0x4014: w25_info.BlockCount=16; sprintf(str1,"w25qxx Chip: w25q80\r\n"); break; case 0x4013: w25_info.BlockCount=8; sprintf(str1,"w25qxx Chip: w25q40\r\n"); break; case 0x4012: w25_info.BlockCount=4; sprintf(str1,"w25qxx Chip: w25q20\r\n"); break; case 0x4011: w25_info.BlockCount=2; sprintf(str1,"w25qxx Chip: w25q10\r\n"); break; default: sprintf(str1,"w25qxx Unknown ID\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); return; } HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); |
Соберём теперь код и посмотрим результат
Некоторые параметры известны, а некоторые можно рассчитать из количества блоков.
Проделаем это и отправим данную информацию в терминальную программу
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); w25_info.PageSize=256; w25_info.SectorSize=0x1000; w25_info.SectorCount=w25_info.BlockCount*16; w25_info.PageCount=(w25_info.SectorCount*w25_info.SectorSize)/w25_info.PageSize; w25_info.BlockSize=w25_info.SectorSize*16; w25_info.NumKB=(w25_info.SectorCount*w25_info.SectorSize)/1024; sprintf(str1,"Page Size: %d Bytes\r\n",(unsigned int)w25_info.PageSize); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); sprintf(str1,"Page Count: %u\r\n",(unsigned int)w25_info.PageCount); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); sprintf(str1,"Sector Size: %u Bytes\r\n",(unsigned int)w25_info.SectorSize); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); sprintf(str1,"Sector Count: %u\r\n",(unsigned int)w25_info.SectorCount); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); sprintf(str1,"Block Size: %u Bytes\r\n",(unsigned int)w25_info.BlockSize); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); sprintf(str1,"Block Count: %u\r\n",(unsigned int)w25_info.BlockCount); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); sprintf(str1,"Capacity: %u KB\r\n",(unsigned int)w25_info.NumKB); HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000); |
Запустим теперь наш код на выполнение, после чего получим следующий результат
Отлично! Мы получили информацию о нашей микросхеме. Думаю, это будет достаточно для первого знакомства с линейкой W25Q. В дальнейших уроках мы попробуем что-нибудь в память нашей микросхемы записать, а также прочитать.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь STM32F429I-DISCO
Микросхему FLASH-памяти W25Q32FVSSIG SOP8 (10 штук) можно приобрести здесь W25Q32FVSSIG
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Спасибо Вам! Вопрос, в CubeIDE ведь уже встроен MX, или Вы просто привыкли к отдельной программе?
Больно глючный он как-то.
Здравствуйте! Спасибо за урок. Подскажите куда копать: не могу запустить флешку w25q16. Пробовал на регистрах и с помощью вашего проекта, результат один- не заводится Флешка и не отвечает на команды. При этом флешка прекрасно читается и пишется при помощи программатора XL866
Добрый День.
С Новым Годом!!!
Спасибо Вам огромное за уроки. Отличные темы.
По CAN BUS будут уроки?
Здравствуйте! Спасибо! CAN — очень нелёгкая тема особенно в плане практики. Поэтому пока неизвестно. У меня есть щиток приборов, есть CAN — Hacker, ряд плат с контроллерами с физикой CAN, переходники и т.д. Поэтому кое-что по практике можно слепить, но я просто не люблю незаконченности. Поэтому много надо ещё будет докупать, изучать, да и автомобиль у меня со старым интерфейсом K-Line, поэтому вроде как что-то есть, но пока точных планов не строю.
а зачем в HAL если можно хотя бы в LL
А зачем LL если можно на ассемблере?
Но не нужно.Ассемблер громоздкий
А есть ли рабочий вариант на базе скажем AT45DB641E-SHN2B-T