안녕하세요, 얼른 올려야 했는데 너무 늦었네요.


지난 번에 CUBEMX 툴로 부트로더 만들어서 실행하면 윈도우 장치 드라이버에 DFU 드라이버가 생성되는 것 까지 했었죠.

여기까지 1차로 진행이 잘 된 것이고,

두번째로 추가할 코드들이 있습니다.

두개의 파일에 손을 대야 합니다. 
main.c  과 usbd_dfu_if.c 입니다.

먼저 main.c 에서 고쳐야 할 내용은 다음과 같습니다.
1. APP 프로그램(부트로더 프로그램이 아닌) 이 현재 USBD_DFU_APP_DEFAULT_ADD 위치에 들어 있는지 검사하는 내용.
2. APP 가 존재하면 APP 프로그램으로 점프.
3. 강제로 부트로더 진입조건을 설정하여 DFU Upgrade 모드로 들어갈 수 있다.(User Switch를 누름으로써)
3. 존재하지 않으면 DFU Upgrade 프로그램 실행.
4. MX_USB_DEVICE_Init() 함수는 Cube MX 툴이 Source 코드를 생성할 때마다 자동으로 다시 위치를 앞쪽에 잡아 놓는데, APP 프로그램이 메모리에 없을때만 동작하도록 뒷쪽으로 위치 이동 시켜야 합니다.

해당 코드는 다음과 같습니다.
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
51
52
53
54
55
56
57
58
int main(void)
{
 
  /* USER CODE BEGIN 1 */
  /* USER CODE END 1 */
 
  /* MCU Configuration----------------------------------------------------------*/
 
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
 
  /* Configure the system clock */
  SystemClock_Config();
 
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  //MX_USB_DEVICE_Init();
 
  /* USER CODE BEGIN 2 */
    HAL_Delay(500);
    printf("DFU Program Start.. \n\r");
 
    // Test if User button on the Necleo kit is pressed 
  if (HAL_GPIO_ReadPin(USER_SW_IN_GPIO_Port,USER_SW_IN_Pin) != GPIO_PIN_RESET)
  {
            printf("USBD_DFU_APP_DEFAULT_ADD ..[%08X]\n\r",(*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD));
    // Check Vector Table: Test if user code is programmed starting from address 
    //   "APPLICATION_ADDRESS" 
    if (((*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD) & 0x2FFE0000 ) == 0x20000000)
    {
            printf("APP Start.. \n\r");
      // Jump to user application 
      JumpAddress = *(__IO uint32_t*) (USBD_DFU_APP_DEFAULT_ADD + 4);
      Jump_To_Application = (pFunction) JumpAddress;
      // Initialize user application's Stack Pointer 
      __set_MSP(*(__IO uint32_t*) USBD_DFU_APP_DEFAULT_ADD);
      Jump_To_Application();
    }
  }
    
    printf("DFU Upgrade Mode Start.. \n\r");
  MX_USB_DEVICE_Init();
 
  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */
 
  /* USER CODE BEGIN 3 */
 
  }
  /* USER CODE END 3 */
 
}
cs

여기서 설명이 좀 필요한 부분이 APP 시작 주소를 검사하여 프로그램이 메모리에 들어있는지를 검사하는 코드인데,
해당 코드는 다음과 같습니다.
1
2
3
4
    if (((*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD) & 0x2FFE0000 ) == 0x20000000)
    {
 
    }
cs

APP 시작 주소(USBD_DFU_APP_DEFAULT_ADD)는 나중에 부트로더에 의해 Upload될 APP프로그램이 위치할 시작 주소입니다.
이 위치에는 Stack 포인터가 위치하는데, 이 Stack Pointer 는 램 영역 내에 주소값을 갖어야 정상적인 프로그램으로 인식합니다.
128KByte 의 램영역은 0x2000 0000 ~ 0x2001 FFFF 입니다.

위의 계산식을 풀어서 쓰면 
1
2
3
4
if ( (*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD) >= 0x20000000  && \
     (*(__IO uint32_t*)USBD_DFU_APP_DEFAULT_ADD) <  0x20020000       )
{
}
cs
와 같습니다.
풀어서 보면 쉽게 아시겠죠? 저도 처음에 왜 이렇게 쓰는지 골머리를 알았습니다. ^^

그다음에 __set_MSP() 함수로 스택포인터를 초기화한 후, user Application 프로그램으로 점프하면 Upload된 user 프로그램이 실행됩니다.


이번에는 usbd_dfu_if.c 의 내용을 수정해야 하는데, 추가할 내용이 꽤 많네요.
내용의 설명은 하지 않고 수정할 코드만 표시하겠습니다. 내용은 내부 Flash 메모리 읽기/쓰기/지우기 내용들입니다.
내부 Flash 메모리 읽기/쓰기/지우기 내용은 나중에 따로 다룰 예정입니다.

다음과 같은 함수를 추가합니다.
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION */
static uint32_t GetSectorSize(uint32_t Sector);
static uint32_t GetSector(uint32_t Address);
/* USER CODE END PRIVATE_FUNCTIONS_DECLARATION */
 
/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */
static uint32_t GetSector(uint32_t Address)
{
  uint32_t sector = 0;
  
  if((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0))
  {
    sector = FLASH_SECTOR_0;  
  }
  else if((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1))
  {
    sector = FLASH_SECTOR_1;  
  }
  else if((Address < ADDR_FLASH_SECTOR_3) && (Address >= ADDR_FLASH_SECTOR_2))
  {
    sector = FLASH_SECTOR_2;  
  }
  else if((Address < ADDR_FLASH_SECTOR_4) && (Address >= ADDR_FLASH_SECTOR_3))
  {
    sector = FLASH_SECTOR_3;  
  }
  else if((Address < ADDR_FLASH_SECTOR_5) && (Address >= ADDR_FLASH_SECTOR_4))
  {
    sector = FLASH_SECTOR_4;  
  }
  else if((Address < ADDR_FLASH_SECTOR_6) && (Address >= ADDR_FLASH_SECTOR_5))
  {
    sector = FLASH_SECTOR_5;  
  }
  else if((Address < ADDR_FLASH_SECTOR_7) && (Address >= ADDR_FLASH_SECTOR_6))
  {
    sector = FLASH_SECTOR_6;  
  }
  else/*(Address < FLASH_END_ADDR) && (Address >= ADDR_FLASH_SECTOR_7))*/
  {
    sector = FLASH_SECTOR_7;  
  }
 
  return sector;
}
 
/**
  * @brief  Gets sector Size
  * @param  None
  * @retval The size of a given sector
  */
static uint32_t GetSectorSize(uint32_t Sector)
{
  uint32_t sectorsize = 0x00;
  if((Sector == FLASH_SECTOR_0) || (Sector == FLASH_SECTOR_1) || (Sector == FLASH_SECTOR_2) ||\
     (Sector == FLASH_SECTOR_3) )
  {
    sectorsize = 16 * 1024;
  }
  else if(Sector == FLASH_SECTOR_4)
  {
    sectorsize = 64 * 1024;
  }
  else
  {
    sectorsize = 128 * 1024;
  }  
  return sectorsize;
}
 
cs

내용이 빈 함수들에 다음과 같이 추가합니다.

uint16_t MEM_If_Init_FS(void) 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uint16_t MEM_If_Init_FS(void)
  /* USER CODE BEGIN 0 */ 
  return (USBD_OK);
  /* USER CODE END 0 */ 
}
 
// 다음과 같이 수정
uint16_t MEM_If_Init_FS(void)
  /* USER CODE BEGIN 0 */ 
    HAL_FLASH_Unlock();  
    __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR |  
                           FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);  
  return (USBD_OK);
  /* USER CODE END 0 */ 
}
cs

uint16_t MEM_If_DeInit_FS(void) 함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint16_t MEM_If_DeInit_FS(void)
  /* USER CODE BEGIN 1 */ 
  return (USBD_OK);
  /* USER CODE END 1 */ 
}
 
// 다음과 같이 수정
uint16_t MEM_If_DeInit_FS(void)
  /* USER CODE BEGIN 1 */ 
    HAL_FLASH_Lock();  
  return (USBD_OK);
  /* USER CODE END 1 */ 
}
cs


uint16_t MEM_If_Erase_FS(uint32_t Add) 함수
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
uint16_t MEM_If_Erase_FS(uint32_t Add)
{
  /* USER CODE BEGIN 2 */ 
  return (USBD_OK);
  /* USER CODE END 2 */ 
}
 
// 다음과 같이 수정
uint16_t MEM_If_Erase_FS(uint32_t Add)
{
  /* USER CODE BEGIN 2 */ 
    uint32_t UserStartSector;  
    uint32_t SectorError;  
    FLASH_EraseInitTypeDef pEraseInit;  
    MEM_If_Init_FS();  
    
    UserStartSector = GetSector(Add);  
    
    pEraseInit.TypeErase = TYPEERASE_SECTORS;  
    pEraseInit.Sector = UserStartSector;  
    pEraseInit.NbSectors = 3;  
    pEraseInit.VoltageRange = VOLTAGE_RANGE_3;  
    if(HAL_FLASHEx_Erase(&pEraseInit,&SectorError)!=HAL_OK)  
    {  
            return (USBD_FAIL);  
    }     
  return (USBD_OK);
  /* USER CODE END 2 */ 
}
cs

uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len) 함수
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
uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* USER CODE BEGIN 3 */ 
  return (USBD_OK);
  /* USER CODE END 3 */ 
}
 
// 다음과 같이 수정
uint16_t MEM_If_Write_FS(uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* USER CODE BEGIN 3 */ 
    uint32_t i = 0;   
    for(i = 0; i < Len; i = i + 4)  
    {  
            if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,(uint32_t)(dest + i),*(uint32_t *)(src + i)) == HAL_OK)  
            {  
                    if(*(uint32_t *)(src + i) != *(uint32_t *)(dest + i))  
                    {  
                            return 2;  
                    }  
            }  
            else  
            {  
                    return 1;  
            }  
    }     
  return (USBD_OK);
  /* USER CODE END 3 */ 
}
cs

uint8_t *MEM_If_Read_FS (uint8_t *src, uint8_t *dest, uint32_t Len)함수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
uint8_t *MEM_If_Read_FS (uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* Return a valid address to avoid HardFault */
  /* USER CODE BEGIN 4 */ 
  return (uint8_t*)(USBD_OK);
  /* USER CODE END 4 */ 
}
 
// 다음과 같이 수정
uint8_t *MEM_If_Read_FS (uint8_t *src, uint8_t *dest, uint32_t Len)
{
  /* Return a valid address to avoid HardFault */
  /* USER CODE BEGIN 4 */ 
    uint32_t i = 0;  
    uint8_t *psrc = src;  
    for( i = 0; i < Len ; i++ )  
    {  
            dest[i] = *psrc++;  
    }  
  return (uint8_t*)(USBD_OK);
  /* USER CODE END 4 */ 
}
cs


uint16_t MEM_If_GetStatus_FS (uint32_t Add, uint8_t Cmd, uint8_t *buffer) 함수
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
uint16_t MEM_If_GetStatus_FS (uint32_t Add, uint8_t Cmd, uint8_t *buffer)
{
  /* USER CODE BEGIN 5 */ 
  switch (Cmd)
  {
  case DFU_MEDIA_PROGRAM:
 
    break;
    
  case DFU_MEDIA_ERASE:
  default:
 
    break;
  }                             
  return  (USBD_OK);
  /* USER CODE END 5 */  
}
 
// 다음과 같이 수정
uint16_t MEM_If_GetStatus_FS (uint32_t Add, uint8_t Cmd, uint8_t *buffer)
{
  /* USER CODE BEGIN 5 */ 
    uint16_t FLASH_PROGRAM_TIME = 50;  
    uint16_t FLASH_ERASE_TIME = 50;  
  switch (Cmd)
  {
  case DFU_MEDIA_PROGRAM:
        buffer[1= (uint8_t)FLASH_PROGRAM_TIME;  
        buffer[2= (uint8_t)(FLASH_PROGRAM_TIME << 8);  
        buffer[3= 0;  
    break;
    
  case DFU_MEDIA_ERASE:
  default:
        buffer[1= (uint8_t)FLASH_ERASE_TIME;  
        buffer[2= (uint8_t)(FLASH_ERASE_TIME << 8);  
        buffer[3= 0;  
    break;
  }                             
  return  (USBD_OK);
  /* USER CODE END 5 */  
}
cs


설명할 내용이 많아서 다음과 같이 3번에 나눠서 글을 써야겠네요.

1/3 : DFU-Bootloader 툴 설정
2/3 : DFU-Bootloader 프로그램 수정
3/3 : DFU-Bootloaderble APP 프로그램 수정

오늘은 1/3 : DFU-Bootloader 툴 설정 에 관해서 글을 써 보겠습니다.

STM32F4xx 중에서 제가 테스트한 MCU는 STM32F411 과 STM32F446 입니다.
두 디바이스 모두, DFU(Device Firmware Upgrade) 동작이 잘 됩니다.

DFU는 2가지 방법으로 구현을 할 수 있는데,
1. Embeded DFU mode : MCU 내부에 이미 들어있는 기능으로 평소에 JTAG로 동작하던 일반적인 펌웨어를 특정 Flash Memory 주소(0x08000000)에 다운로드할 수 있고,다음과 같은 특정한 핀 설정으로 진입한다. 초기 부팅 시 Boot1 핀은 PB2 핀과 기능을 공유한다.



2. Custom DFU Mode : MCU Flash 메모리에 User가 필요에 따라서 각종 DFU 진입 조건(사용자가 일반 핀입력이나 여러가지 조건으로 지정할 수 있다)과 APP Flash Memory 시작 주소를 수정할 수 있다. APP 프로그램의 설정을 수정해야 하는 번거러움이 있다.


제가 Embeded DFU를 사용하지 않고 Custom DFU를 사용하려 하는 이유는,
APP f/w 가 Flash MEM 의 최초 시작 주소부터 프로그램의 기능에 따라 늘어나는데, 얼마나 크게 만들어 질지 몰라서 
1. Flash data Memory(EEPROM Emulation 기능)를 위치시킬 주소를 정하기가 번거롭고,

여유롭게 뒤쪽에 있는 Sector에 data Flash를 위치 시키려고 보니,
뒤쪽으로 갈수록 Sector 당 Flash MEM 크기가 너무 큰 단위로 증가를 해서 버리는 메모리가 많았습니다.
Flash 메모리를 쓸려면 일단 Erase 해야 하는데, Erase 방법이 Setor 별로 지우던가 전체 메모리를 지워던가 2가지밖에 없었습니다.
그래서 또 다음과 같은 불합리한 문제가 있어서 Custom DFU를 사용하게 되었습니다.
2. 뒤로 갈수로 Sector 당 Flash MEM 크기가 증가하여 낭비되는 메모리가 많다.
3. Sector 당 메모리가 크면 1 sector를 지우는데 시간이 많이 걸린다.

STM32F411 과 STM32F446 은 플래쉬 메모리의 크기에 따라 2가지(256KB,512KB)로 나뉩니다.
그에 따른 Sector 구조는 다음과 같습니다.







Necleo 보드는 USB 콘넥터가 보드내에 없기 때문에 DFU 테스트를 위해서는 안쓰는 USB 케이블을 하나 잘라서 연결을 해야 합니다. 다음은 제가 만들어 붙인 USB 케이블 모습입니다.



이제 DFU를 테스트하기 위한 하드웨어는 다 준비가 됐기 때문에, CUBE-MX 툴로 프로그램을 만들어 보겠습니다.
예전에 쓴 글에 MCU 설정은 많이 나와 있으므로 바로 PINOUT 설정 내용을 그림으로 올리겠습니다.
1. DFU용 USB-FS 핀 2개
2. 외부 CLOCK 입력 핀 : 4개
3. 디버깅용 UART2(Necleo 보드의 JTAG IC에 의해 USB-to-UART 와 연결되어 있다)



클럭 설정은 USB를 사용하기 때문에 USB CLK 를 48MHz로 맞추다 보니, System Clk은 96MHz로 설정된다.
System Clk 에 STM32F411의 최대 클럭인 100MHz 를 입력하면 자동으로 최적의 클럭을 계산해 준다.





기본으로 위와 같이 Disable 로 설정이 안되어 있어서 얼마전에 Custom DFU가 안되는 줄 알고 한참 해멨는데, 주의해야 겠습니다.

 

다음으로 CUBE MX툴의 Config 탭의 USB DEVICE 버튼을 클릭해서 파라메터를 설정합니다.





여기서 눈여겨 볼 내용은 2가지 입니다.
1. USBD_DFU_APP_DEFAULT_ADD : 나중에 업로드할 APP f/w 의 Flash Memory Start Address 를 입력합니다.
Sector 단위로 구분하여 App f/w 를 위치시킬 Sector의 시작 주소를 넣어 줍니다. 저는 Sector 2 부터 사용하려고 0x08008000을 입력했습니다.
2. USBD_DFU_MEDIA Inteface : 부트로더 프로그램의 시작 주소와 각각의 메모리의 Sector 별 특징을 넣어 줍니다.
이 파라메터는 꽤 복잡하게 구성되어 있는데, 해당 입력란을 클릭하면 아래 설명이 나와 있습니다.

USBD_DFU_MEDIA Interface USBD_DFU_MEDIA Parameter Description: The description of the flash (used by PC tool DFuSe) Each Alternate setting string descriptor must follow this memory mapping so that the PC Host Software can decode the right mapping for the selected device: ● @: To detect that this is a special mapping descriptor (to avoid decoding standard descriptor) ● /: for separator between zones ● Maximum 8 digits per address starting by “0x” ● /: for separator between zones ● Maximum of 2 digits for the number of sectors ● *: For separator between number of sectors and sector size ● Maximum 3 digits for sector size between 0 and 999 ● 1 digit for the sector size multiplier. Valid entries are: B (byte), K (Kilo), M (Mega) ● 1 digit for the sector type as follows: – a (0x41): Readable – b (0x42): Erasable – c (0x43): Readable and Erasabled (0x44): Writeable – e (0x45): Readable and Writeable – f (0x46): Erasable and Writeable – g (0x47): Readable, Erasable and Writeable Note: If the target memory is not contiguous, the user can add the new sectors to be decoded just after a slash"/" as shown in the following example: "@Flash /0xF000/1*4Ka/0xE000/1*4Kg/0x8000/2*24Kg"

제가 위의 내용을 토대로 그림으로 좀 정리를 해 봤습니다.



이 파라메터는 나중에 DFU F/W Upgrade 툴인 DfuSe 프로그램에서 읽어서 사용하게 됩니다.
어떻게 사용되는지 미리 알아 보겠습니다.



다음은 DFU 모드로 들어가기위해, 여러가지 조건들을 만들어 넣을 수 있겠지만 Necleo 보드의 User 스위치를 누르면 진입하도록 GPIO 설정에서 핀내부 풀업해 줍니다. (스위치가 누르면 GND와 붙기 때문에)



인터넷에서 DFU 관련 사이트를 찾아보니 HEAP 크기를 늘려 주라고 되어 있었는데, 왜 그런지는 모르겠습니다.
예로써, 0x2000 크기를 Heap으로 잡아 놨던데, 실험 결과 0x800 만 잡아서 문제는 (아직까지) 없었습니다.
Heap 을 늘리면 RAM 의 ZI(Zero Initialize Data)가 늘어 나는 것이 차이라면 차이랄까?



여기까지 CUBE MX툴에서 할 일은 모두 끝났습니다.
다음 단계로 KEIL 컴파일러용 코드를 만들어서 JTAG로 프로그램을 다운로드하고 리셋을 눌러서 부트로더를 실행해서 윈도우의 장치관리자에서 STM Device in DFU Mode 가 뜨면 성공입니다.

아직 프로그램을 추가하고 수정해야 하지만, 이 상태로 STM Device in DFU Mode 가 떠야 그 다음으로 넘어갈 수 있습니다.





회사 일이 많이 바빠서 테스트는 다 했지만 글을 못 올리네요. 약간 설명할 내용이 많아서 시간을 내기가 더 힘든 것 같습니다.
곧 프로그램 수정방법도 올려 보겠습니다.

궁금하신 분은 KEIL 소스코드를 올려 놓을 테니, 미리 보고 공부하셔도 됩니다. ^^


다음의 소스 코드 첨부합니다.


STM32F411 에서는 SDIO DMA 로 SD-Card가 잘 읽혔습니다.


방법이 어렵지 않아서 금방 테스트가 끝났는데, STM32F446 에서는 DMA를 사용하면 f_mount() 함수에서 멈춰버렸습니다.
DMA를 사용할 경우 sd_diskio.c 의 SD_read 함수 내의 
BSP_SD_ReadBlocks() 함수를 BSP_SD_ReadBlocks_DMA() 함수로 바꿔주면 됩니다.
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
/**
  * @brief  Reads Sector(s)
  * @param  lun : not used
  * @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 SD_read(BYTE lun, BYTE *buff, DWORD sector, UINT count)
{
  DRESULT res = RES_OK;
  /*if(BSP_SD_ReadBlocks((uint32_t*)buff, 
                       (uint64_t) (sector * BLOCK_SIZE), 
                       BLOCK_SIZE, 
                       count) != MSD_OK)*/
  if(BSP_SD_ReadBlocks_DMA((uint32_t*)buff, 
                       (uint64_t) (sector * BLOCK_SIZE), 
                       BLOCK_SIZE, 
                       count) != MSD_OK)
  {
    res = RES_ERROR;
  }
  
  return res;
}
 
/**
  * @brief  Writes Sector(s)
  * @param  lun : not used
  * @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 SD_write(BYTE lun, const BYTE *buff, DWORD sector, UINT count)
{
  DRESULT res = RES_OK;
  /*if(BSP_SD_WriteBlocks((uint32_t*)buff, 
                        (uint64_t)(sector * BLOCK_SIZE), 
                        BLOCK_SIZE, count) != MSD_OK)*/
  if(BSP_SD_WriteBlocks_DMA((uint32_t*)buff, 
                        (uint64_t)(sector * BLOCK_SIZE), 
                        BLOCK_SIZE, count) != MSD_OK)
  {
    res = RES_ERROR;
  }
  
  return res;
}
cs


STM32F446 Nucleo 보드에서 DMA를 사용하지 않으면 동작은 잘 되는데, 
가끔 Error이 발생해서 SD-Card 와의 선을 짧게 하니 잘 동작 했습니다.
보드의 아트웍 영향인건지는 확실히 모르겠는데, 배선도 좀 짧으면 좋은 것 같습니다.



이 상태로 13KByte 의 데이터를 읽는데 10ms 정도의 시간이 걸렸으니 
거의 1MB/sec 의 속도밖에 안나와서 SDIO를 왜 쓰나 싶더군요.

그래서 처음부터 다시 프로젝트를 만들어서 동작을 시켜봤는데, 역시나 결과는 DMA 기능은 구현이 안되네요.

그런데, 뭔가 틀려진 점이 있는데, DMA를 안 썼을 때 13KB를 읽는데 10ms 걸렸던 것이 2ms 로 대폭 줄었습니다.
24KB를 읽었더니 3ms 고요. 마치 DMA가 동작되는 듯이 느껴질 정도로 빨라졌지만 DMA는 동작되지 않는 상황.
왜 빨라졌는지 원인은 못 찾았지만 CUBEMX 툴이 아직 안정화가 덜 됐나 봅니다.

빨라진 이유는 제가 코드를 잘 못 만들어서 그렇네요. f_read()함수가 512 Byte씩 읽어야 되는 줄 알고 24K 파일을 
512Byte 단위로 f_read()함수를 이용해 여러번 나눠서 읽었더니 느리고 한번에 24KB를 다 읽으니 빠르네요.
예전에 테스트를 했을 때, 512-Byte 이상 읽으면 안 읽혔었는데 이번에 해 보니 한번에 많은 데이터도 읽기 가능하네요. 

[512 Byte 씩 여러번 읽어서 느렸던 코드내용]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint8_t read_f_dat(uint8_t *pf_dat,FIL* fp)
{
    UINT i,testBytes;
    uint32_t f_size;
 
    f_size = 13000;
 
    // #define _MAX_SS 512
    
    for (i=0;i<f_size;i+=_MAX_SS)
    {
        if ((f_size - i) < _MAX_SS)
        {
            res = f_read(fp, &pf_dat[i],(f_size - i), &testBytes);
        }
        else
        {
            res = f_read(fp, &pf_dat[i],_MAX_SS, &testBytes);
        }
    }
}
cs

[한번에 많은 데이터를 읽어서 빨라지도록 수정된 코드]
1
2
3
4
5
6
7
8
9
uint8_t read_f_dat(uint8_t *pf_dat,FIL* fp)
{
    UINT testBytes;    
    uint32_t f_size;
 
    f_size = 13000;
    
    res = f_read(fp, &pf_dat[0],f_size, &testBytes);
}
cs

아뭏든 현재 첨부한 프로젝트는 DMA는 동작이 안되는 것으로 보이고, 계속해서 방법을 찾아보다가 
잘 되면 DMA SDIO-4bit 관련 글을 다시 올려 보겠습니다.


다음은 Nucleo - STM32F446 보드에서 DMA가 안되서 새로 프로젝트를 만들었던 과정입니다.

1. 먼저 MCU를 STM32F446로 선택하고, SDIO,UART2,GPIO 1개 출력(오실로 스코프로 시간이 얼마나 걸리는지 체크를 위해)



2. SDIO 신호와 u-SD-Card를 점퍼선으로 연결.



3. SDIO 를 DMA 로 동작하도록 CUBEMX 에서 설정.



4. sd_diskio.c 파일에서 SD_read/SD_write 함수 안의 BSP_SD_ReadBlocks/BSP_SD_WriteBlocks 함수를 바꾸지 않고 그대로 사용했는데 24KB 읽는데 걸리는 시간이 3ms 걸렸음.



5. 혹시 다 읽힌 것 맞는지 알아보려고 해리포터 소설을 UART로 뿌려보니 제대로 읽혀 있었습니다.
DMA가 동작한 것으로 믿어야겠죠? (제가 미쳤나 봅니다. 이런 상황을 가지고 DMA가 된다고 생각했다니.. 사기꾼이 될 뻔한 것을 글을 다시 보고 수정하면서 정정합니다.)
SDIO는 많은 양을 읽을수록 속도가 점점 더 빨라지는 듯 합니다.

그리고 STM32F411 은 42MHz 로도 DMA가 동작하는데, STM32F446 은 45MHz로 일반 방식에서는 동작하지 않았습니다.
(차후 DMA 구현되면 다시 비교해 볼 예정)

[STM32F4xx 에서 SDIO CLK를 최대(411:42MHz,446:45MHz)로 설정한 코드 내용]
1
2
3
4
5
6
7
8
9
10
11
12
/* SDIO init function */
static void MX_SDIO_SD_Init(void)
{
 
  hsd.Instance = SDIO;
  hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
  hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_ENABLE;
  hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
  hsd.Init.BusWide = SDIO_BUS_WIDE_1B;
  hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
  hsd.Init.ClockDiv = 0;
}
cs

STM32F446 이 DMA가 안된 상태에서 비교를 하니 의미가 많이 없지만, 
또한 411은 SDIO_CLK=42MHz 이고 446은 22.5MHz 이기때문에, 
이 상태에서는 (당연하게도)STM32F411 이 더 빨랐습니다. 

STM32F446(SDIO_CLK:22.5MHz)은 13KB 읽는데 2ms, 24KB 3ms가 걸렸는데, 
STM32F411(SDIO_CLK:42MHz) 은 13KB/1.4ms , 24KB/2ms 밖에 안걸립니다. 

24KB 읽는 것을 기준으로 STM32F411 은 최대 속도 12MB/s , STM32F446 은 최대속도 8MB/s 나오네요.

이 속도는 앞서서도 말씀 드렸듯이 한번에 많이 읽으면 더 빨라집니다.

6. 해리포터 소설을 24KB 만큼 읽어서 출력해 봄.



7. main() 주요 동작 코드
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
int main(void)
{
 
  /* USER CODE BEGIN 1 */
 
  /* USER CODE END 1 */
 
  /* MCU Configuration----------------------------------------------------------*/
 
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
 
  /* Configure the system clock */
  SystemClock_Config();
 
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SDIO_SD_Init();
  MX_USART2_UART_Init();
  MX_FATFS_Init();
 
  /* USER CODE BEGIN 2 */
    read_TAR_file_DMA();
  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
  /* USER CODE END WHILE */
 
  /* USER CODE BEGIN 3 */
 
  }
  /* USER CODE END 3 */
 
}
 
/* SDIO init function */
static void MX_SDIO_SD_Init(void)
{
 
  hsd.Instance = SDIO;
  hsd.Init.ClockEdge = SDIO_CLOCK_EDGE_RISING;
  hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_DISABLE;
  //hsd.Init.ClockBypass = SDIO_CLOCK_BYPASS_ENABLE;
  hsd.Init.ClockPowerSave = SDIO_CLOCK_POWER_SAVE_DISABLE;
  hsd.Init.BusWide = SDIO_BUS_WIDE_1B;
  hsd.Init.HardwareFlowControl = SDIO_HARDWARE_FLOW_CONTROL_DISABLE;
  hsd.Init.ClockDiv = 0;
 
}
 
/** 
  * Enable DMA controller clock
  */
static void MX_DMA_Init(void
{
  /* DMA controller clock enable */
  __HAL_RCC_DMA2_CLK_ENABLE();
 
  /* DMA interrupt init */
  /* DMA2_Stream3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 10);
  HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
  /* DMA2_Stream6_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 20);
  HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);
 
}
 
/* USER CODE BEGIN 4 */
void read_TAR_file_DMA(void)
{
     #if _USE_LFN
    TCHAR lfn[_MAX_LFN + 1];
    fno_t.lfname = lfn;
    fno_t.lfsize = sizeof lfn;
    #endif
    
    BYTE testBuffer[24000];
   uint8_t path[13];            
 
    DWORD cnt_rd_Byte=24000;
  UINT testBytes;
    DWORD cnt_i;
    UINT i,cnt_512;
    uint32_t image_offset;
 
 
    //sprintf(path,"1.tar");
    sprintf(path,"aaa.txt");
    printf("%s\n\r",path);
    
    res_t = f_mount(&fs32_t,SD_Path,1);
    printf("SD Mount : res f_mount : %02X\n\r",res_t);
    res_t = f_open(&fil_t, (char*)path, FA_READ); 
    printf("res f_open : %02X, fil_t.fsize : %d\n\r",res_t,fil_t.fsize);
 
    printf("Read Txt File Test\n\r");
    
    GPIOB->ODR ^= GPIO_PIN_5;    
    res_t = f_read(&fil_t, testBuffer, cnt_rd_Byte, &testBytes);
    GPIOB->ODR ^= GPIO_PIN_5;    
    printf("res_t = f_read : %02X\n\r",res_t);
    
    for(i=0;i<24000;i++)
        printf("%c",testBuffer[i]);
}
 
/* USER CODE END 4 */
 
cs



추가로 DMA 인터럽트도 DMA 동작이 안되는 것과 관련이 있을 수도 있다 싶어서 인터럽트 설정 부분도 올려 봅니다.
혹시 왜 DMA가 안되는지 아시는분 알려주시면 대단히 고맙겠습니다.

아래와 같이 인터럽트를 여러가지로 변경해 봤는데, DMA 동작은 여전히 안됐습니다. 
그런데 DMA를 사용하지 않아도 SDIO_CLK 22.5MHz의 속도로써 늦은 속도는 아닌 것 같습니다.
(STM32F411 SDIO_CLK 42MHz(DMA)에서 13KB 전송시 1.4ms 이고, STM32F446 SDIO_CLK 22.5MHz(No DMA) 에서 2ms 걸렸기 때문에)
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
/** 
  * Enable DMA controller clock
  */
static void MX_DMA_Init(void
{
  /* DMA controller clock enable */
  __HAL_RCC_DMA2_CLK_ENABLE();
 
  /* DMA interrupt init */
  /* DMA2_Stream3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 20);
  HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
  /* DMA2_Stream6_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 30);
  HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);
}
 
static void MX_DMA_Init(void
{
  /* DMA controller clock enable */
  __HAL_RCC_DMA2_CLK_ENABLE();
  /* DMA interrupt init */
  /* DMA2_Stream3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 10);
  HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
  /* DMA2_Stream6_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 20);
  HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);
}
 
static void MX_DMA_Init(void
{
  /* DMA controller clock enable */
  __HAL_RCC_DMA2_CLK_ENABLE();
  /* DMA interrupt init */
  /* DMA2_Stream3_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 00);
  HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
  /* DMA2_Stream6_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Stream6_IRQn, 00);
  HAL_NVIC_EnableIRQ(DMA2_Stream6_IRQn);
}
cs



다음의 소스코드 첨부합니다.

sdio_pure_v1.zip


메뉴얼에서는 못 찾았는데, 구글 검색해 보니 잘 나와 있네요.


이 내용의 원문은 다음과 같습니다.

아무리 메뉴얼을 뒤져봐도 못 찾았던 내용인데, 이 사람은 어디서 찾은 걸까요?
제가 꼭 알고 싶었던 내용입니다. 
타이머를 쓰긴 쓰는데, Clock 소스가 180 MHz 인지, 90MHz 인지, 45MHz 인지 통 모르겠드만...
그래서 오실로 스코프로 찍어보고 알아야만 했거든요. ㅜㅜ

맨 마지막에 APB 번호가 나와 있어서 CubeMX 툴의 Clock Configuration 과 비교해 보면 알 수 있습니다.




TIM2 는 이전에 해봐서 90MHz가 Clock Source 라는 것을 알았고, 만약 TIM1 으로 테스트 하면 Clock Source가 180MHz 겠군요.

클럭은 Cube Mx로 다음과 같이 설정하고,





소스 코드는 아래와 같이 TIM1 Update 인터럽트가 걸릴때 마다 PA5를 토글하도록 만들어 봤습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
 if (htim->Instance == TIM1)
 {
  GPIOA->ODR ^= GPIO_PIN_8;
 }
}
 
int main(void)
{
  HAL_Init();
 
  SystemClock_Config();
 
  MX_GPIO_Init();
  MX_TIM1_Init();
 
  HAL_TIM_Base_Start_IT(&htim1);
 
  while (1)
  {
  }
}
cs


그 결과 PA5 를 오실로 스코프로 찍어 보니, 위의 표와 계산한 값과 같이 10ms 마다 토글하도록 출력이 됩니다.




[이 프로젝트에 사용된 MCU는 NUCLEO-F446RE(STM32F446RET6) 입니다.]


제가 지금까지 타이머 인터럽트를 안다뤘더군요. 

가장 많이 쓰는 기능일텐데, 이제서야 여러 기능들을 합쳐서 프로그램을 짜 넣으려고 하다 보니 빠진 것을 알았습니다.

먼저 CubeMx 툴을 실행.
TIM2 의 Clock Source 를 Internal Clock으로 하여 기능을 살려 놓습니다. 
그리고 TIM2 Update 인터럽트가 제대로 걸리고 있는지 확인을 위해 포트를 토글하려고 PA5를 GPIOOUT으로 설정.



다음은 클럭 설정으로 가서 STM32F446의 시스템 클럭을 180MHz로 가장 높게 맞췄습니다.
참고로 메뉴얼을 봐도 TIM2 의 클럭 소스가 어떤 것인지 몰라서 직접 실험으로 알아본 결과 APB1 Timer Clock(90MHz) 네요.
누가 혹시 이 내용에 대해 자세히 나온 글이 있는지 아시면 알려 주시면 감사하겠습니다. (전 도저히 못 찾겠네요)



configuration 탭으로 가서 TIM2 의 세부 설정을 위해 TIM2를 클릭.



10ms 마다 TIM2 Update 인터럽트가 걸리게 하기 위해, Parameter 설정 탭으로 가서 Period 를 900,000 으로 설정했습니다.
공식은 period count = 0.01(sec) x 90Mhz = 900,000 입니다.



인터럽트를 쓸 것이기 때문에 NVIC Setting으로 가서 인터럽트를 인에이블 체크를 합니다.



메뉴 중, project->Generate Code 를 눌러 컴파일러에 맞게 코드를 생성합니다.



1. HAL_TIM_Base_Start_IT 함수로 타이머 업데이트 인터럽트가 걸리도록 합니다.
2. stm32f4xx_hal_tim.c 의 __weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) 함수에서 
--weak 를 제거하고 main.c 에 옮겨서 TIM2 update 인터럽트가 걸릴때 실행될 코드를 추가하거나 수정합니다.
TIM2 update 인터럽트가 걸릴때 HAL_TIM_PeriodElapsedCallback 함수를 호출하도록 되어 있지요.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
  * @brief  Period elapsed callback in non blocking mode 
  * @param  htim: pointer to a TIM_HandleTypeDef structure that contains
  *                the configuration information for TIM module.
  * @retval None
  */
__weak void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(htim);
  /* NOTE : This function Should not be modified, when the callback is needed,
            the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
   */
}
cs

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
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* Prevent unused argument(s) compilation warning */
    if (htim->Instance == TIM2)
    {
        HAL_GPIO_TogglePin(LED_OUT_GPIO_Port,LED_OUT_Pin);
    }
    else if (htim->Instance == TIM1)
    {
    }
  /* NOTE : This function Should not be modified, when the callback is needed,
            the __HAL_TIM_PeriodElapsedCallback could be implemented in the user file
   */
}
 
 
int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
 
  /* Configure the system clock */
  SystemClock_Config();
 
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();
  MX_TIM1_Init();
  MX_USART2_UART_Init();
 
  /* USER CODE BEGIN 2 */
  printf("UART Test...\n\r");
    
  HAL_Delay(500);
  HAL_TIM_Base_Start_IT(&htim2);
    
  while (1)
  {
            HAL_GPIO_TogglePin(LED_OUT_GPIO_Port,LED_OUT_Pin);
  }
 
}
cs


그런데 주의할 점은 어떤 Timer 인터럽트가 실행되도 호출되는 callback 함수는 HAL_TIM_PeriodElapsedCallback 입니다.
따라서, HAL_TIM_PeriodElapsedCallback 함수 내에서 어떤 타이머에서 날 불렀는지 확인할 필요가 있습니다.

그래서 다음과 같이 확인하는 코드가 필요합니다.





즉 날 호출한 게, TIM1 이냐? TIM2 냐? 이런 뜻이죠. 
저는 TIM2에 의해 호출되도록 만들었기 때문에, TIM2에 의해 호출되면 A5 핀을 토글하도록 해서 결과는 다음과 같습니다.





정확히 10ms 마다 포트가 토글 되네요.
동작 잘 되네요~~^^

다음의 소스 코드를 첨부합니다.


[이 프로젝트에 사용된 MCU는 NUCLEO-F446RE(STM32F446RET6) 입니다.]

SPI 통신을 DMA로 송신했을 시, 파형이 기존 SPI에 비해 끊김 없이 최대 45MBit/s 로 나옵니다.

DMA를 안 쓰면 데이터 Rate가 절반도 안나왔는데, DMA로 하면 효율이 100% 입니다.

SPI tx 를 DMA로 보냈을 때 파형이 어떤지만 간단하게 설명하고 끝낼 예정입니다. ^^



다음의 소스 코드를 첨부합니다.

SPI_DMA.zip


[이 프로젝트에 사용된 MCU는 NUCLEO-F446RE(STM32F446RET6) 입니다.]
이번에는 memory to GPIO 를 DMA로 출력하는 테스트를 할 예정입니다.

타이머 1개를 트리거 신호로 사용하여 65535개까지 연속으로 데이터를 출력할 수 있으며,
속도는 최대 22.7MHz 까지 나옵니다.

8 비트 데이터 버스로 사용한다면 대략 180Mbit/s 의 속도가 나오므로 꽤 나오죠?
저는 동시에 여러개의 SPI 버스의 데이터를 송신하는 용도로 사용하려고 테스트를 진행중입니다.
(예전에 테스트를 어느정도 해 놓고, 정리 차원에서 카페에 글을 올리는 중입니다.)

예전에 테스트한 보드는 Nucleo STM32F411 보드에서 했었고,
이번에는 Nucleo STM32F446 보드에서 테스트할 예정입니다.

참고로 한 내용은 st 홈페이지의 AN4666 입니다.

예제 프로그램을 위의 링크된 사이트에서 다운 받을 수 있는데, 회원을 가입해야 했던 것 같습니다.

STM32F476G Discovery 보드의 예제인데, TIM2 의 CH1을 DMA 트리거 소스로 프로그램 되어 있습니다.
하지만 STM32F411,STM32F446 에서는 TIM2/CH1 을 DMA 소스로 했을 때 트리거 신호가 나오지 않는 문제가 발생했습니다.

아무리 해도 안되서 TIM1/CH1 을 트리거 소스로 사용하니 트리거 신호가 동작했습니다.

원인은 모르겠으나, TIM1 과 TIM2 의 큰 차이는 Repetition Count 가 있고 없고의 차이인데 확실히 트리거 신호가 안나오는 이유는 모르겠지만, TIM1/CH1 은 동작합니다. (원인을 알고 계시면 알려 주시면 감사하겠습니다)

다음은, STM32CubeMX 툴로 프로젝트를 만드는 과정을 그림으로 올려 보겠습니다.

먼저 Pinout 탭에서 사용할 기능을 설정합니다.
NuCleo 보드에서 클럭을 8MHz Bypass 클락과 32.768 KHz 크리스탈을 사용하므로 RCC를 그에 맞게 설정하고,
트리거 소스로 사용할 TIM1 의 Clock Source 를 Internal Clock, 출력 채널을 PWM Generation CH1 으로 설정합니다.
이 때, GPIO 출력으로 사용할 포트는 CubeMX 툴에 표시되지 않습니다. (나중에 알았는데, GPIO OUT 으로 설정하면 녹색으로 표시 된답니다. 속성으로 하다 보니, 모르는 것이 많네요.^^)



실제 Nucleo 보드의 출력핀은 다음과 같습니다.



다음은 큐브 툴에서 Clock을 설정합니다. 입력 클럭 8MHz,32.768 KHz 클럭을 소스로 시스템 클럭은 STM32F446 의 최대 클럭인 180MHz 로 설정을 했습니다. 180MHz 를 쓰고 엔터키만 치만 나머지를 자동으로 맞춰주니 골치아파할 필요가 없습니다.



다음은 TIM1 주기와 듀티 및 DMA 설정을 위해서 CubeMX 툴의 configuration 탭을 선택하고 TIM1 을 클릭합니다.



parameter Setting 에서 제 경험상 가장 빠른 출력 주기인 7로 주기를 설정하고, High 로 유지할 펄스는 2로 설정합니다.
주기를 더 빠르게 하면 트리거 출력이 나오다 없어지거나 처음부터 안나옵니다.



다음은 DMA Setting 입니다.
수정할 부분은 Direction 을 Memory to Peripheral 로 고치고, 저는 Data Width를 Byte로 해서 출력 하려고 Byte로 설정했습니다.



이렇게 하고 나서 Keil compiller 프로젝트 형식으로 소스 코드를 만들기 위해 project/Setting 메뉴에서 Toolchain / IDE 옵션을 MDK-ARM V5 로 선택합니다.



그런 다음, project/Generate Code 를 클릭하면 Keil 프로젝트가 만들어 집니다.



main.c 의 main() 함수에 추가한 내용은 아래 붉은색 네모의 내용이 다 입니다.



그런데 가장 중요한 부분은 stm32f4xx_it.c 에 있는 DMA IRQHandler 함수에 TIM1 을 멈추게 하는 코드를 넣어야 한다는 점입니다.
저는 예제를 받아서 수정을 한 것이 아니고, 참고로 해서 CubeMX 툴에서 다시 만들었기 때문에 main.c 파일만 보고 수정된 부분을 비교해서 수정을 했기 때문에 stm32f4xx_it.c 파일을 수정해야 한다는 생각을 못하고 프로그램을 실행하니 아무리 해도 동작이 안되었습니다. 원인은 이 코드를 수정하지 않아서 트리거 클락이 끝없이 나가는 것이 문제였습니다.

그리고 트리거 클럭은 앞뒤로 더미 클럭이 나가니, GPIO를 사용할 경우 주의하시기 바랍니다. SPI로 동작 시킬 경우에는 데이터에 /CS 데이터를 실어서 출력하면 더미데이터를 신경 안 쓰고 사용할 수 있을 것 같습니다.
더미 데이터는 클럭이 빠를수록 더 많이 나옵니다.



다음은 첨부된 프로젝트 파일의 실행 결과 입니다.



다음의 소스 코드를 첨부합니다.

mem_t_GPIO_v1.zip


휴, 안도의 한숨이 나옵니다.

일주일 가량의 시간이 걸려 SDIO(4Bit Bus Width, DMA) 로 micro-SD Card 읽기를 성공했네요.

속도는 약 8.23 MByte / s  정도 나오네요. 아마도 오실로 스코프 상에서는 좀 더 빨라서 9 MBte /s 에 더 가까울 것 같습니다.
DMA를 사용하지 않았을 때는 1.2 MByte / s 정도 나왔으니 8배 정도 빠를 것 같습니다.



검색해 보니, 현재 SD 카드가 엄청 느린 녀석이었는데 좀 더 좋은 것을 쓰면 속도가 더 나올 수 있는 것 같습니다.
어떤 사람은 19 MByte/s 까지 나온다고 합니다. (링크)



DMA 의 개념은, 보통 메모리에 있는 데이터를 읽어서 SPI/UART/SDIO 통신으로 내보내는 경우에 메모리에서 데이터를 읽는 시간이 상당이 걸리는데 그 시간을 줄이는 목적이 있습니다. 
다른 좋은 장점이 또 있을 것 같은데요, 일단은 제가 실감한 것이 그렇습니다.

예를 들자면 다음과 같은 그림으로 잘 설명이 될 것 같습니다.



실제로 제가 측정한 내용은 위와같이, 데이터를 읽는 시간이 SDIO로 보내는 시간 보다 엄청 걸렸습니다. 
위의 그림과 예전의 테스트 상황이 99.9%(?) 일치 했습니다. ^^

그림으로 보니 엄청 빨라질 것 같죠? (DMA를 쓰면 빨라 집니다. 확실히~)

DMA를 사용하려다가 몇가지 복병을 만났었는데, 소개해 볼 까 합니다.
(참고로 저는 Receive 동작만 테스트 했습니다)
1. DMA 인터럽트 우선순위를 설정하지 않아서 프로그램이 멈추는 문제 발생
   - DMA RX 인터럽트 추가한 후, 인터럽트 번호를 잘 설정해 줄 것.
2. DMA 로 f_read 함수를 실행 시, BLOCK_SIZE(512 Byte) 이상을 읽으면 Error 리턴.
   - Err 리턴을 무시할 것. (현재 프로그램을 정리하다 보니 이 현상이 없어 졌는데, 조금 더 두고 봐야 겠습니다)
3. 4gb 이상의 u-SD 카드 동작시 SD_read() 함수 입력 변수 계산 오류
   - SD_read() 함수 내의 BSP_SD_ReadBlocks() 함수의 입력 변수 계산 코드 수정.
     (아직 테스트는 못 해봄. 4gb 이상 sd-card 없음)

위 내용의 해결 과정에 앞서서, 일반 polling 방식을 DMA 방식으로 바꾸는 방법을 알아보겠습니다.
저는 거의 모든 STM32 시리즈의 SDIO,u-SD Card 문서 및 웹사이트를 다 돌아 다니면서 코드를 분석도 해 봤는데 별로 건지지 못했고, 노가다 테스트로 해결했습니다.

결론은.. 진짜, 쉽습니다. 

Cube 툴로 생성된 Keil 소스코드에서, Middlewares/FatFs 폴더의 sd_diskio.c 를 열어서 SD_read()함수의 내부 코드를 바꾸면 됩니다.
SD_read() 함수 내의 폴링 방식의 Read 함수인 BSP_SD_ReadBlocks 를 BSP_SD_ReadBlocks_DMA 로,
   단지 이름만 바꿔 주면 됩니다.





/**
  * @brief  Reads Sector(s)
  * @param  lun : not used
  * @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 SD_read(BYTE lun, BYTE *buff, DWORD sector, UINT count)
{
  DRESULT res = RES_OK;
  /*
  if(BSP_SD_ReadBlocks((uint32_t*)buff, 
                       (uint64_t) (sector * BLOCK_SIZE), 
                       BLOCK_SIZE, 
                       count) != MSD_OK)*/
/*
  if(BSP_SD_ReadBlocks_DMA((uint32_t*)buff, 
                       (uint64_t) (sector * BLOCK_SIZE), 
                       BLOCK_SIZE, 
                       count) != MSD_OK)*/
  if(BSP_SD_ReadBlocks_DMA((uint32_t*)buff, 
                       ((uint64_t)sector * BLOCK_SIZE)
                       BLOCK_SIZE, 
                       count) != MSD_OK)
  {
    res = RES_ERROR;
  }
  
  return res;
}

그럼 다음으로 제가 만난 복병과 그 해결 방법을 설명 드리겟습니다.
먼저 위에 그림에서 추가로 4gb이상의 sd Card를 읽을 때, 수정해야 하는 내용이 먼저 나와서,
4g sd-card가 없어서 아직 테스트는 못해 봤지만 코드의 수정 내용을 알려 드립니다.

[4gb 이상의 SD Card Access 시 수정할 사항]
현재 sd_diskio.c 파일 내에 있는 SD_read 함수 내부의 코드에서 
BSD_SD_ReadBlocks/BSD_SD_ReadBlocks_DMA 함수의 2번째 파라 메터의 계산 식이 틀려서 4gb 이상의 Sd card가 Access 되지 않는다고 합니다.
 (uint64_t) (sector * BLOCK_SIZE) 을 ((uint64_t) sector * BLOCK_SIZE)로 수정하면 된다고 하네요.

다음은 4gb sd card 이상의 동작에서 오류 수정을 당부하는 내용의 원문입니다. [링크]



[DMA로 수정한 상태에서 f_read 동작이 안되는 경우]
제가 처음에 SD_read 함수에서 BSP_SD_ReadBlocks() 함수를 BSP_SD_ReadBlocks_DMA()함수로 바꿨는데 프로그램이 멈춰버리더군요. 원인은 우연히 찾았는데, DMA 인터럽트 우선순위를 CUBE 툴에서 설정하지 않아서 우선순위가 0으로 되어 있었던 것을 Rx는 1, Tx는 2 로 수정을 했더니 프로그램이 정상 동작을 했습니다.
CUBE 툴에서 인터럽트 우선 순위는 따로 사용자가 결정을 해 줘야 하는 것 같습니다.

FRESULT 에러 리스트 (ff.h)
typedef enum {
FR_OK = 0, /* (0) Succeeded */
FR_DISK_ERR, /* (1) A hard error occurred in the low level disk I/O layer */
FR_INT_ERR, /* (2) Assertion failed */
FR_NOT_READY, /* (3) The physical drive cannot work */
FR_NO_FILE, /* (4) Could not find the file */
FR_NO_PATH, /* (5) Could not find the path */
FR_INVALID_NAME, /* (6) The path name format is invalid */
FR_DENIED, /* (7) Access denied due to prohibited access or directory full */
FR_EXIST, /* (8) Access denied due to prohibited access */
FR_INVALID_OBJECT, /* (9) The file/directory object is invalid */
FR_WRITE_PROTECTED, /* (10) The physical drive is write protected */
FR_INVALID_DRIVE, /* (11) The logical drive number is invalid */
FR_NOT_ENABLED, /* (12) The volume has no work area */
FR_NO_FILESYSTEM, /* (13) There is no valid FAT volume */
FR_MKFS_ABORTED, /* (14) The f_mkfs() aborted due to any parameter error */
FR_TIMEOUT, /* (15) Could not get a grant to access the volume within defined period */
FR_LOCKED, /* (16) The operation is rejected according to the file sharing policy */
FR_NOT_ENOUGH_CORE, /* (17) LFN working buffer could not be allocated */
FR_TOO_MANY_OPEN_FILES, /* (18) Number of open files > _FS_SHARE */
FR_INVALID_PARAMETER /* (19) Given parameter is invalid */
} FRESULT;



다시 2달이 지난 현재(2016.11.07) STM32F446 에서 테스트를 진행 하고 있는데, 
u-SD CARD 를 mount 할 경우에 FR_NO_FILESYSTEM 에러(13)가 나더니, 예전에 사용한 방식(DMA 인터럽트 우선순위를 바꾸는) 을 사용해도 에러가 났고, UART2 인터럽트 priority 를 6으로 했더니 에러가 없어졌습니다.
(그냥 복불복 입니다. 됭 때도 있고 안될 때도 있는....)

하아~ 그런데 에러가 없어 진 다음에 다시 DMA interrupt priority 를 rx,tx 각각 0으로 돌리고, UART interrupt priority도 0으로
복구했는데도 정상 동작을 합니다. 그리고 나서 전원을 껐다 키니까 처음에 FR_NO_FILESYSTEM 에러가 나네요.
ㅎ~~ 원인은 접촉 불량 내지 선이 너무 길어서 노이즈가 탄 것 같습니다. 현재 u-SD Card 와 Nucleo 보드간 케이블 길이가
약 10cm 정도 되서 노이즈가 좀 심하리라 생각이 드네요. 
따라서 인터럽트를 바꿔서 정상동작을 하고 안하고의 문제나 512 Byte 읽을 때 Error 가 발생하는 원인도 접촉 불량이나 노이즈에 의한 문제로 보입니다. 

아.. 정말정말 정확한 이유는 모르겠지만, 인터럽트 우선순위를 변경하는 것과 u-SD-Card 인식은 관계가 없는 것은 확실합니다.
됐다 안됐다 하는 문제는 접촉 불량과 노이즈인 것 같습니다. 

선을 반으로 줄여 봤어도, 에러가 자꾸 났는데 납땜 문제로 노이즈가 더 심해 졌을 지도 모르겠고..
다시 u-SD-Card 소켙을 바꾸고 긴 케이블을 연결하니 잘 되는 것을 보니 u-SD-Card 소켙의 접촉 불량이나
선 불량 으로 보이고,  통신 속도를 늦춰도 똑같은 현상이 나타나는 것을 보아 고속 통신으로 인한 데이터의 지연 문제는 아닌 것 같습니다.

그냥 아트웍할 때, 신경써서 하면 잘 될 것 같네요.

되긴 하나, 왠지 찝찝 하여.. 다시 껐다 켜고를 반복 하니 또 에러가 났습니다.
u-SD Card 쪽 핀헤더에 연결한 점퍼 와이어를 꾹 눌러주면 되고 띠면 안되는 것으로 보아,
콘넥터 접촉불량으로 결론을 냈습니다. 제가 혹시 혼란을 가중 시켰나 모르겠습니다.

[결론] 

u-SD Card 인터페이스 시, 접촉 불량 안나도록 배선을 잘하라. ^^





[DMA로 수정한 상태에서 BLOCK_SIZE(512 Byte) 이상을 읽으면, f_read() 함수의 return 값이 Error을 리턴한다]
위의 소제목과 같이 Error를 리턴 합니다만, 그냥 무시하고 쓰면 됩니다. 
원인을 찾아서 고치면 좋겠는데, 시간과 능력이 부족하여 저는 그냥 쓰기로 했습니다.
이상하게, 에러는 발생하지만 동작에는 문제가 없었습니다.
(현재 프로그램을 정리하다 보니 이 현상이 없어 졌는데, 조금 더 두고 봐야 겠습니다)

참고로, 인터넷에서 알랑방구 좀 뀌는 외국인의 글을 소개하자면..
u-SD Card 속도를 올리기 위해서는 다음같은 사항을 점검하라고 하네요.
1. 데이터 버스폭은 4Bit 로 했는지 검토.
2. 코드를 DMA로 바꿨는지 검토.
3. 클럭 스피드 검토(Cube 툴에서 기본 설정: 24MHz, 최대 48MHz)

^^   저는 이 분이, sd 카드 DMA에 대해서 알려 주는 것도 같지만.. 지가 한 내용을 가지고 속도 빠르다고 자랑하는 것 같아 보였어요. 소스 코드도 옛날 것이고 받은 코드도 파일이 좀 빠져 있고, 결국엔 아무 도움도 안됐네요.

다만, DMA를 사용하면 빨라진다는 정도만 소득으로 얻었습니다.(링크 주소)



마지막으로 제가 첨부하는 예제의 설명 입니다.
여러가지 테스트 함수들이 들어 있는데, 소스 코드 보시고 참고하시고.. 실제로 실행되는 함수는 read_BMP_file_DMA() 함수입니다.
이 함수 내부의 GPIO ON/OFF 코드는 오실로 스코프로 DMA 시간 테스트를 위해서 만들어 놓은 것이니 괜히 고민하지 마시길 바랍니다. 또한 __nop() 함수는 MCU가 100MHz 동작하니까, 오실로 스코프로 봐도 GPIO 변환 지점이 잘 안보여서 딜레이 대용으로 넣은 것이니 신경 쓰지 않으셔도 됩니다.

예제 코드에서는 64x64 BMP 파일을 DMA 읽어서 UART2 포트로 전부 뿌리도록 되어 있습니다.
UART 출력 결과는 다음과 같습니다.



아가~~ ㄱ ^^ 
정말로 중요한 내용이 빠졌네요.
f_read 에서 한 번에 많은 데이터를 읽을 때, 데이터 버퍼의 크기에 맞게 스택 사이즈 늘려주셔야 합니다.
이전에 다룬 내용을 참고 하시기 바랍니다 




제가 테스트한 보드에서 사용된 MCU의 핀맵입니다.



소스코드 첨부합니다.

sd_LEDSigne_v100_2.zip


다음은 USE_LFN(Use Long Filename) 설정하는 방법에 대해서 알아 보겠습니다.


아주 오래 전에 윈도우에서 dir 하고 command line 명령을 치면,
파일명이 xxxxx~1 라고 나온 경험이 좀 있으실 겁니다.

파일 이름이 긴 파일을 표시하지 못하는 경우입니다. 
이것을 해결하는 방법은 간단합니다.

큐브 툴에서 Configuration 을 클릭해서 창이 하나 뜨고 거기에서 FATFS 를 클릭합니다.
그러면 다시 FATFS Configuration 창이 하나 뜨는데, 
여기서 Locale and Namespace Parameters 의 USE_LFN 항목을 클릭하여 Disable 되어 있던 것을 
Enable with static working buffer on the BSS(1) 로 바꿔 주면 됩니다.



그런 다음 project > Generate Code 하시고 Keil 컴파일러에서 컴파일해서 STM32f4xx 보드에 다운로드하고 실행하면
다음과 같이 긴 파일 이름을 쓰실 수 있습니다.


아래 13번 글과 함께 사용했던 소스 파일 첨부합니다.

sd_LEDSigne_v100.zip


STM32F4xx에서 uart2 로 printf()함수를 사용하는 방법을 테스트 해보고 정보를 공유합니다.


다음과 같은 코드를 main.c 파일에 추가하면 됩니다.

/* USER CODE BEGIN 0 */
#ifdef __GNUC__
 #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
 #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
 
PUTCHAR_PROTOTYPE
{
 HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 1);
 return ch;
}
/* USER CODE END 0 */



이 코드를 추가하기 전에 huart2 는 정의를 해야겠죠.
그리고 uart HAL 함수 HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 1); 를 사용해서 폴링으로 1Byte 전송하도록 되어 잇습니다.


+ Recent posts