mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
570 lines
16 KiB
C
570 lines
16 KiB
C
/* Copyright 2025 muzkr
|
|
* https://github.com/muzkr
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <string.h>
|
|
|
|
#include "driver/py25q16.h"
|
|
#include "driver/gpio.h"
|
|
#include "py32f071_ll_bus.h"
|
|
#include "py32f071_ll_system.h"
|
|
#include "py32f071_ll_spi.h"
|
|
#include "py32f071_ll_dma.h"
|
|
#include "driver/system.h"
|
|
#include "driver/systick.h"
|
|
#include "external/printf/printf.h"
|
|
#include "misc.h"
|
|
|
|
/* MBMARK was an on-screen SPI trace used while bringing up multiboot (M1/M2).
|
|
* The tracer is gone; keep the call sites as no-ops. */
|
|
#define MBMARK(s)
|
|
|
|
// #define DEBUG
|
|
|
|
#define SPIx SPI2
|
|
#define CHANNEL_RD LL_DMA_CHANNEL_4
|
|
#define CHANNEL_WR LL_DMA_CHANNEL_5
|
|
|
|
#define CS_PIN GPIO_MAKE_PIN(GPIOA, LL_GPIO_PIN_3)
|
|
|
|
#define SECTOR_SIZE 0x1000
|
|
#define PAGE_SIZE 0x100
|
|
|
|
static uint32_t SectorCacheAddr = 0x1000000;
|
|
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
|
|
/* The restore-only RAM stub is copied over this cache immediately before it
|
|
* erases internal flash. A reset always follows, so the cache is never needed
|
|
* again after the overlay becomes active. */
|
|
static uint8_t SectorCache[SECTOR_SIZE]
|
|
__attribute__((section(".bss.mb_workspace"), aligned(4), used));
|
|
#else
|
|
static uint8_t SectorCache[SECTOR_SIZE];
|
|
#endif
|
|
static uint8_t BlackHole[4] __attribute__((aligned(4)));
|
|
static volatile bool TC_Flag;
|
|
|
|
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
|
/* Active settings-profile base (see py25q16.h). 0 = profile 0 / historical
|
|
* config region, i.e. an identity mapping. */
|
|
static uint32_t ProfileBase = 0;
|
|
|
|
void PY25Q16_SetProfileBase(uint32_t Base)
|
|
{
|
|
ProfileBase = Base;
|
|
}
|
|
|
|
/* Redirect config-region accesses (addr < boundary) into the active bank.
|
|
* Calibration/logo/slots/marker (addr >= boundary) are returned unchanged.
|
|
* ProfileBase is sector-aligned, so alignment done by callers is preserved. */
|
|
static inline uint32_t ProfileMap(uint32_t Address)
|
|
{
|
|
return (Address < PY25Q16_PROFILE_SHARED_FROM) ? (Address + ProfileBase) : Address;
|
|
}
|
|
#else
|
|
static inline uint32_t ProfileMap(uint32_t Address)
|
|
{
|
|
return Address;
|
|
}
|
|
#endif
|
|
|
|
static inline void CS_Assert()
|
|
{
|
|
GPIO_ResetOutputPin(CS_PIN);
|
|
}
|
|
|
|
static inline void CS_Release()
|
|
{
|
|
GPIO_SetOutputPin(CS_PIN);
|
|
}
|
|
|
|
static void SPI_Init()
|
|
{
|
|
LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_SPI2);
|
|
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_DMA1);
|
|
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
|
|
|
|
do
|
|
{
|
|
// SCK: PA0
|
|
// MOSI: PA1
|
|
// MISO: PA2
|
|
|
|
LL_GPIO_InitTypeDef InitStruct;
|
|
LL_GPIO_StructInit(&InitStruct);
|
|
InitStruct.Mode = LL_GPIO_MODE_ALTERNATE;
|
|
InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
|
|
InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
|
|
InitStruct.Pull = LL_GPIO_PULL_UP;
|
|
|
|
InitStruct.Pin = LL_GPIO_PIN_0;
|
|
InitStruct.Alternate = LL_GPIO_AF8_SPI2;
|
|
LL_GPIO_Init(GPIOA, &InitStruct);
|
|
|
|
InitStruct.Pin = LL_GPIO_PIN_1 | LL_GPIO_PIN_2;
|
|
InitStruct.Alternate = LL_GPIO_AF9_SPI2;
|
|
LL_GPIO_Init(GPIOA, &InitStruct);
|
|
|
|
} while (0);
|
|
|
|
LL_SYSCFG_SetDMARemap(DMA1, CHANNEL_RD, LL_SYSCFG_DMA_MAP_SPI2_RD);
|
|
LL_SYSCFG_SetDMARemap(DMA1, CHANNEL_WR, LL_SYSCFG_DMA_MAP_SPI2_WR);
|
|
|
|
NVIC_SetPriority(DMA1_Channel4_5_6_7_IRQn, 1);
|
|
NVIC_EnableIRQ(DMA1_Channel4_5_6_7_IRQn);
|
|
|
|
LL_SPI_InitTypeDef InitStruct;
|
|
LL_SPI_StructInit(&InitStruct);
|
|
InitStruct.Mode = LL_SPI_MODE_MASTER;
|
|
InitStruct.TransferDirection = LL_SPI_FULL_DUPLEX;
|
|
InitStruct.ClockPhase = LL_SPI_PHASE_2EDGE;
|
|
InitStruct.ClockPolarity = LL_SPI_POLARITY_HIGH;
|
|
InitStruct.BaudRate = LL_SPI_BAUDRATEPRESCALER_DIV2;
|
|
InitStruct.BitOrder = LL_SPI_MSB_FIRST;
|
|
InitStruct.NSS = LL_SPI_NSS_SOFT;
|
|
InitStruct.CRCCalculation = LL_SPI_CRCCALCULATION_DISABLE;
|
|
LL_SPI_Init(SPIx, &InitStruct);
|
|
|
|
LL_SPI_Enable(SPIx);
|
|
}
|
|
|
|
static void SPI_ReadBuf(uint8_t *Buf, uint32_t Size)
|
|
{
|
|
LL_SPI_Disable(SPIx);
|
|
LL_DMA_DisableChannel(DMA1, CHANNEL_RD);
|
|
LL_DMA_DisableChannel(DMA1, CHANNEL_WR);
|
|
|
|
LL_DMA_ClearFlag_GI4(DMA1);
|
|
|
|
LL_DMA_ConfigTransfer(DMA1, CHANNEL_RD, //
|
|
LL_DMA_DIRECTION_PERIPH_TO_MEMORY //
|
|
| LL_DMA_MODE_NORMAL //
|
|
| LL_DMA_PERIPH_NOINCREMENT //
|
|
| LL_DMA_MEMORY_INCREMENT //
|
|
| LL_DMA_PDATAALIGN_BYTE //
|
|
| LL_DMA_MDATAALIGN_BYTE //
|
|
| LL_DMA_PRIORITY_MEDIUM //
|
|
);
|
|
|
|
LL_DMA_ConfigTransfer(DMA1, CHANNEL_WR, //
|
|
LL_DMA_DIRECTION_MEMORY_TO_PERIPH //
|
|
| LL_DMA_MODE_NORMAL //
|
|
| LL_DMA_PERIPH_NOINCREMENT //
|
|
| LL_DMA_MEMORY_NOINCREMENT //
|
|
| LL_DMA_PDATAALIGN_BYTE //
|
|
| LL_DMA_MDATAALIGN_BYTE //
|
|
| LL_DMA_PRIORITY_MEDIUM //
|
|
);
|
|
|
|
LL_DMA_SetMemoryAddress(DMA1, CHANNEL_RD, (uint32_t)Buf);
|
|
LL_DMA_SetPeriphAddress(DMA1, CHANNEL_RD, LL_SPI_DMA_GetRegAddr(SPIx));
|
|
LL_DMA_SetDataLength(DMA1, CHANNEL_RD, Size);
|
|
|
|
LL_DMA_SetMemoryAddress(DMA1, CHANNEL_WR, (uint32_t)BlackHole);
|
|
LL_DMA_SetPeriphAddress(DMA1, CHANNEL_WR, LL_SPI_DMA_GetRegAddr(SPIx));
|
|
LL_DMA_SetDataLength(DMA1, CHANNEL_WR, Size);
|
|
|
|
TC_Flag = false;
|
|
LL_DMA_EnableIT_TC(DMA1, CHANNEL_RD);
|
|
LL_DMA_EnableChannel(DMA1, CHANNEL_RD);
|
|
LL_DMA_EnableChannel(DMA1, CHANNEL_WR);
|
|
|
|
LL_SPI_EnableDMAReq_RX(SPIx);
|
|
LL_SPI_Enable(SPIx);
|
|
LL_SPI_EnableDMAReq_TX(SPIx);
|
|
|
|
while (!TC_Flag)
|
|
;
|
|
}
|
|
|
|
static void SPI_WriteBuf(const uint8_t *Buf, uint32_t Size)
|
|
{
|
|
LL_SPI_Disable(SPIx);
|
|
LL_DMA_DisableChannel(DMA1, CHANNEL_RD);
|
|
LL_DMA_DisableChannel(DMA1, CHANNEL_WR);
|
|
|
|
LL_DMA_ClearFlag_GI4(DMA1);
|
|
|
|
LL_DMA_ConfigTransfer(DMA1, CHANNEL_RD, //
|
|
LL_DMA_DIRECTION_PERIPH_TO_MEMORY //
|
|
| LL_DMA_MODE_NORMAL //
|
|
| LL_DMA_PERIPH_NOINCREMENT //
|
|
| LL_DMA_MEMORY_NOINCREMENT //
|
|
| LL_DMA_PDATAALIGN_BYTE //
|
|
| LL_DMA_MDATAALIGN_BYTE //
|
|
| LL_DMA_PRIORITY_LOW //
|
|
);
|
|
|
|
LL_DMA_ConfigTransfer(DMA1, CHANNEL_WR, //
|
|
LL_DMA_DIRECTION_MEMORY_TO_PERIPH //
|
|
| LL_DMA_MODE_NORMAL //
|
|
| LL_DMA_PERIPH_NOINCREMENT //
|
|
| LL_DMA_MEMORY_INCREMENT //
|
|
| LL_DMA_PDATAALIGN_BYTE //
|
|
| LL_DMA_MDATAALIGN_BYTE //
|
|
| LL_DMA_PRIORITY_LOW //
|
|
);
|
|
|
|
LL_DMA_SetMemoryAddress(DMA1, CHANNEL_RD, (uint32_t)BlackHole);
|
|
LL_DMA_SetPeriphAddress(DMA1, CHANNEL_RD, LL_SPI_DMA_GetRegAddr(SPIx));
|
|
LL_DMA_SetDataLength(DMA1, CHANNEL_RD, Size);
|
|
|
|
LL_DMA_SetMemoryAddress(DMA1, CHANNEL_WR, (uint32_t)Buf);
|
|
LL_DMA_SetPeriphAddress(DMA1, CHANNEL_WR, LL_SPI_DMA_GetRegAddr(SPIx));
|
|
LL_DMA_SetDataLength(DMA1, CHANNEL_WR, Size);
|
|
|
|
TC_Flag = false;
|
|
LL_DMA_EnableIT_TC(DMA1, CHANNEL_RD);
|
|
LL_DMA_EnableChannel(DMA1, CHANNEL_RD);
|
|
LL_DMA_EnableChannel(DMA1, CHANNEL_WR);
|
|
|
|
LL_SPI_EnableDMAReq_RX(SPIx);
|
|
LL_SPI_Enable(SPIx);
|
|
LL_SPI_EnableDMAReq_TX(SPIx);
|
|
|
|
while (!TC_Flag)
|
|
;
|
|
}
|
|
|
|
static uint8_t SPI_WriteByte(uint8_t Value)
|
|
{
|
|
while (!LL_SPI_IsActiveFlag_TXE(SPIx))
|
|
;
|
|
LL_SPI_TransmitData8(SPIx, Value);
|
|
while (!LL_SPI_IsActiveFlag_RXNE(SPIx))
|
|
;
|
|
return LL_SPI_ReceiveData8(SPIx);
|
|
}
|
|
|
|
static void WriteAddr(uint32_t Addr);
|
|
static uint8_t ReadStatusReg(uint32_t Which);
|
|
static void WaitWIP();
|
|
static void WriteEnable();
|
|
static void SectorErase(uint32_t Addr);
|
|
static void SectorProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
|
|
static void PageProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
|
|
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size);
|
|
|
|
void PY25Q16_Init()
|
|
{
|
|
CS_Release();
|
|
SPI_Init();
|
|
}
|
|
|
|
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size)
|
|
{
|
|
MBMARK("RD cmd"); // about to assert CS + send read command
|
|
CS_Assert();
|
|
|
|
SPI_WriteByte(0x03); // Send read command
|
|
MBMARK("RD addr"); // command sent, about to send address
|
|
WriteAddr(Address); // Send address (3 bytes)
|
|
|
|
MBMARK("RD flush"); // address sent, about to flush RX FIFO
|
|
// CRITICAL: Flush RX FIFO before DMA to remove residual data
|
|
while (LL_SPI_RX_FIFO_EMPTY != LL_SPI_GetRxFIFOLevel(SPIx))
|
|
{
|
|
LL_SPI_ReceiveData8(SPIx); // Read and discard
|
|
}
|
|
|
|
MBMARK("RD data"); // FIFO flushed, about to read the data
|
|
if (Size >= 16) {
|
|
SPI_ReadBuf((uint8_t *)pBuffer, Size);
|
|
} else {
|
|
for (uint32_t i = 0; i < Size; i++)
|
|
{
|
|
((uint8_t *)(pBuffer))[i] = SPI_WriteByte(0xff);
|
|
}
|
|
}
|
|
|
|
MBMARK("RD end"); // data read, about to release CS
|
|
CS_Release();
|
|
}
|
|
|
|
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
|
|
{
|
|
ReadBufferRaw(ProfileMap(Address), pBuffer, Size);
|
|
}
|
|
|
|
// Like PY25Q16_ReadBuffer, but waits for the flash to be idle first (WIP=0),
|
|
// exactly as PY25Q16_WriteBuffer does before its internal reads. A standalone
|
|
// read issued while the chip is still busy from a prior program/erase never
|
|
// returns the expected data.
|
|
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size)
|
|
{
|
|
MBMARK("SAFE wip"); // about to WaitWIP()
|
|
WaitWIP();
|
|
MBMARK("SAFE rb"); // WaitWIP done, about to ReadBuffer
|
|
PY25Q16_ReadBuffer(Address, pBuffer, Size);
|
|
}
|
|
|
|
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append)
|
|
{
|
|
Address = ProfileMap(Address); /* map once; internal reads use *Raw below */
|
|
|
|
#ifdef DEBUG
|
|
printf("spi flash write: %06x %ld %d\n", Address, Size, Append);
|
|
#endif
|
|
|
|
//#ifdef ENABLE_FEAT_F4HWN_DEBUG
|
|
// gDebug++;
|
|
//#endif
|
|
|
|
uint32_t SecIndex = Address / SECTOR_SIZE;
|
|
uint32_t SecAddr = SecIndex * SECTOR_SIZE;
|
|
uint32_t SecOffset = Address % SECTOR_SIZE;
|
|
uint32_t SecSize = SECTOR_SIZE - SecOffset;
|
|
|
|
while (Size)
|
|
{
|
|
// CRITICAL FIX #1: Wait for flash ready before processing each sector
|
|
WaitWIP();
|
|
|
|
if (Size < SecSize)
|
|
{
|
|
SecSize = Size;
|
|
}
|
|
|
|
if (SecAddr != SectorCacheAddr)
|
|
{
|
|
/* SecAddr is already in mapped space (Address was mapped above), so
|
|
* read raw to avoid mapping a second time. */
|
|
ReadBufferRaw(SecAddr, SectorCache, SECTOR_SIZE);
|
|
SectorCacheAddr = SecAddr;
|
|
}
|
|
|
|
if (0 != memcmp(pBuffer, (char *)SectorCache + SecOffset, SecSize))
|
|
{
|
|
bool Erase = false;
|
|
const uint8_t *oldData = SectorCache + SecOffset;
|
|
const uint8_t *newData = (const uint8_t *)pBuffer;
|
|
|
|
for (uint32_t i = 0; i < SecSize; i++)
|
|
{
|
|
// NOR flash programming can only change bits from 1 to 0.
|
|
if ((oldData[i] & newData[i]) != newData[i])
|
|
{
|
|
Erase = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
memcpy(SectorCache + SecOffset, pBuffer, SecSize);
|
|
|
|
if (Erase)
|
|
{
|
|
SectorErase(SecAddr);
|
|
|
|
// CRITICAL FIX #2: Erase takes ~300ms, must complete before program starts
|
|
WaitWIP();
|
|
|
|
if (Append)
|
|
{
|
|
SectorProgram(SecAddr, SectorCache, SecOffset + SecSize);
|
|
memset(SectorCache + SecOffset + SecSize, 0xff, SECTOR_SIZE - SecOffset - SecSize);
|
|
}
|
|
else
|
|
{
|
|
SectorProgram(SecAddr, SectorCache, SECTOR_SIZE);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SectorProgram(Address, pBuffer, SecSize);
|
|
}
|
|
}
|
|
|
|
Address += SecSize;
|
|
pBuffer += SecSize;
|
|
Size -= SecSize;
|
|
|
|
SecAddr += SECTOR_SIZE;
|
|
SecOffset = 0;
|
|
SecSize = SECTOR_SIZE;
|
|
} // while
|
|
|
|
// CRITICAL FIX #3: Ensure all writes complete before function returns
|
|
WaitWIP();
|
|
}
|
|
|
|
void PY25Q16_SectorErase(uint32_t Address)
|
|
{
|
|
Address = ProfileMap(Address);
|
|
Address -= (Address % SECTOR_SIZE);
|
|
SectorErase(Address);
|
|
if (SectorCacheAddr == Address)
|
|
{
|
|
memset(SectorCache, 0xff, SECTOR_SIZE);
|
|
}
|
|
}
|
|
|
|
void PY25Q16_InvalidateCache(void)
|
|
{
|
|
/* Same "no sector cached" sentinel as the initial value: the next write
|
|
* re-reads its sector from flash instead of trusting SectorCache. */
|
|
SectorCacheAddr = 0x1000000;
|
|
}
|
|
|
|
static inline void WriteAddr(uint32_t Addr)
|
|
{
|
|
SPI_WriteByte(0xff & (Addr >> 16));
|
|
SPI_WriteByte(0xff & (Addr >> 8));
|
|
SPI_WriteByte(0xff & Addr);
|
|
}
|
|
|
|
static uint8_t ReadStatusReg(uint32_t Which)
|
|
{
|
|
uint8_t Cmd;
|
|
switch (Which)
|
|
{
|
|
case 0:
|
|
Cmd = 0x5;
|
|
break;
|
|
case 1:
|
|
Cmd = 0x35;
|
|
break;
|
|
case 2:
|
|
Cmd = 0x15;
|
|
break;
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
CS_Assert();
|
|
SPI_WriteByte(Cmd);
|
|
uint8_t Value = SPI_WriteByte(0xff);
|
|
CS_Release();
|
|
|
|
return Value;
|
|
}
|
|
|
|
static void WaitWIP()
|
|
{
|
|
for (int i = 0; i < 1000000; i++)
|
|
{
|
|
uint8_t Status = ReadStatusReg(0);
|
|
if (1 & Status) // WIP
|
|
{
|
|
SYSTICK_DelayUs(10);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void WriteEnable()
|
|
{
|
|
CS_Assert();
|
|
SPI_WriteByte(0x6);
|
|
CS_Release();
|
|
}
|
|
|
|
static void SectorErase(uint32_t Addr)
|
|
{
|
|
#ifdef DEBUG
|
|
printf("spi flash sector erase: %06x\n", Addr);
|
|
#endif
|
|
WriteEnable();
|
|
WaitWIP();
|
|
|
|
CS_Assert();
|
|
SPI_WriteByte(0x20);
|
|
WriteAddr(Addr);
|
|
CS_Release();
|
|
|
|
WaitWIP();
|
|
}
|
|
|
|
static void SectorProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size)
|
|
{
|
|
uint32_t Size1 = PAGE_SIZE - (Addr % PAGE_SIZE);
|
|
|
|
while (Size)
|
|
{
|
|
if (Size < Size1)
|
|
{
|
|
Size1 = Size;
|
|
}
|
|
|
|
PageProgram(Addr, Buf, Size1);
|
|
|
|
Addr += Size1;
|
|
Buf += Size1;
|
|
Size -= Size1;
|
|
|
|
Size1 = PAGE_SIZE;
|
|
}
|
|
}
|
|
|
|
static void PageProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size)
|
|
{
|
|
#ifdef DEBUG
|
|
printf("spi flash page program: %06x %ld\n", Addr, Size);
|
|
#endif
|
|
|
|
WriteEnable();
|
|
// WaitWIP();
|
|
|
|
CS_Assert();
|
|
|
|
SPI_WriteByte(0x2);
|
|
WriteAddr(Addr);
|
|
|
|
if (Size >= 16)
|
|
{
|
|
SPI_WriteBuf(Buf, Size);
|
|
}
|
|
else
|
|
{
|
|
for (uint32_t i = 0; i < Size; i++)
|
|
{
|
|
SPI_WriteByte(Buf[i]);
|
|
}
|
|
}
|
|
|
|
CS_Release();
|
|
|
|
WaitWIP();
|
|
}
|
|
|
|
void DMA1_Channel4_5_6_7_IRQHandler()
|
|
{
|
|
if (LL_DMA_IsActiveFlag_TC4(DMA1) && LL_DMA_IsEnabledIT_TC(DMA1, CHANNEL_RD))
|
|
{
|
|
LL_DMA_DisableIT_TC(DMA1, CHANNEL_RD);
|
|
LL_DMA_ClearFlag_TC4(DMA1);
|
|
|
|
// Wait a tiny bit for SPI to finish
|
|
SYSTICK_DelayUs(10); // ← ADD THIS
|
|
|
|
uint32_t timeout = 10000;
|
|
|
|
while ((LL_SPI_TX_FIFO_EMPTY != LL_SPI_GetTxFIFOLevel(SPIx)) && timeout--)
|
|
;
|
|
|
|
timeout = 10000;
|
|
while (LL_SPI_IsActiveFlag_BSY(SPIx) && timeout--)
|
|
;
|
|
|
|
timeout = 10000;
|
|
while ((LL_SPI_RX_FIFO_EMPTY != LL_SPI_GetRxFIFOLevel(SPIx)) && timeout--)
|
|
;
|
|
|
|
LL_SPI_DisableDMAReq_TX(SPIx);
|
|
LL_SPI_DisableDMAReq_RX(SPIx);
|
|
|
|
TC_Flag = true;
|
|
}
|
|
}
|