2025最新STM32G4DFU升级实现与各种坑

因为项目开发需要,鼓捣一下stm32的usb dfu固件升级,搜了下网上教程,很多讲的很详细也很好,但是都比较老了,用的F4一类的比较老的型号和库,ST的HAL库又没事就改实现和接口,于是照着做下来发现坑深似海,遇到了一堆问题。

姑且记录一下实现的方法和遇到的各种坑点吧,仅供参考。

首先大家知道stm32的新型号一般是内置dfu bootloader的,通过一些boot引脚配置是可以上电引导到内置的dfu bl然后通过usb用cube programmer去下载。但是这种做法不太灵活,必须关机上电拉引脚才能实现升级,所以还是自己实现一个dfu固件,这样可以自由的通过软件代码切换如何进入dfu模式进行下载,实现诸如在线ota推送固件之类的功能。

然后现在st的cube programmer也是可以直接支持自己实现的dfu固件的下载烧录的,所以一些老教程里面说的efuse之类的工具都可以不要用了,这玩意st官方页面已经说明停止更新了,听说bug也挺多的。

那么我们开始。

flash空间和地址分配

用的芯片是STM32G431CBT6,产品页可以看到flash大小是128k

img

stm32的flash区地址默认是0x08000000,128k的结束地址是0x08020000

我们这里给dfu固件分配16k地址,差不多刚好能放下,也就是把dfu固件放在flash区起始这一块,0x08000000到0x08004000这一段空间里,上电默认就会先运行dfu固件,然后在dfu固件里面判断是否进入dfu下载模式或者直接跳转到用户app。

128k去掉16k剩下的用户app空间是112k,也就是0x1c000,从0x08004000到0x08020000这段空间。这些地址之后需要用到。

很多教程里给dfu固件分配的空间是0xc000也就是48k,是可以的但是因为我的mcu本身flash区比较小,就省着点用了。

下面这个图来自RM0440,STM32G4的 Reference manual

image-20250317120944129

也就是说默认情况下我们这片G431的128k flash是64个2k的page,一个bank,看下代码

image-20250317121116005

page size这里是2k

image-20250317121428350

一些老教程里有这张图,这个我查了下是F4的 Reference manual里的,同样的章节G4已经没有这个了,也搜不到sector相关的描述了,包括老教程里的一些sector擦除写入之类的函数接口库里也都找不到了,官方给的例程只有按page擦除了。所以我们也不管这个sector相关的东西。

DFU固件生成

先配置cube

先用cube创建一个空工程,配置时钟树,swd调试接口,打开你需要的IO(比如指示灯,进DFU模式的按键)

打开USB设备,勾上就行,默认。

在中间件里打开usb设备里的DFU,然后重点是下面的配置

image-20250317121953974

第一行是DFU程序占用的地址,也就是用户app的起始地址,之前已经说过了,占用16k也就是改成0x08004000

第二行描述的是flash的内存组织方法

因为我们的前16k是dfu固件不应该被操作,后面112k是用户app,需要通过dfu程序在线擦写。

每段flash的page是2k。

所以修改基地址后面的内存空间描述,也就是8个2k的只读地址(Ka代表不可写)后面56个2k的可读写(Kg代表可读写)空间,一共128k

@Internal Flash /0x08000000/08*02Ka,56*02Kg

以前的教程包括cube默认都会给你按照0116Ka,0316Kg这种写法来写,这个应该是基于之前的sector的写法,但是根据ST官方G4系列的demo板例程里面给的配置,确实是按照2k一页来组织的,实际来看也可以正常工作。

然后生成工程,开始改代码。

最核心的代码在usbd_dfu_flash.c里面,需要实现flash擦写等接口的代码。

这个文件似乎也改过名字。。以前教程的名字是找不到的=-=

但是内容基本是类似的

image-20250317122816428

里面是一些flash读写,初始化,擦除之类的函数实现,以下代码是从ST官方的demo里复制来的,直接替换里面原有的函数即可,实测验证可用。

在flash描述符下面加入两个宏定义

1
2
3
4
5
6
#define FLASH_DESC_STR      "@Internal Flash   /0x08000000/08*02Ka,56*02Kg"

/* USER CODE BEGIN PRIVATE_DEFINES */
#define FLASH_ERASE_TIME (uint16_t)50
#define FLASH_PROGRAM_TIME (uint16_t)50
/* USER CODE END PRIVATE_DEFINES */

加入自定义函数声明

1
2
3
4
/* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION */
static uint32_t GetPage(uint32_t Address);
static uint32_t GetBank(uint32_t Address);
/* USER CODE END PRIVATE_FUNCTIONS_DECLARATION */

注意检查下描述符是不是对的,cube似乎有个bug,有时候不会正确更新这里的描述符,需要重新生成一下。

从FLASH_If_Init的实现开始改,一直到文件末尾全部直接改成下面的代码

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/* Private functions ---------------------------------------------------------*/
/**
* @brief Memory initialization routine.
* @retval USBD_OK if operation is successful, MAL_FAIL else.
*/
uint16_t FLASH_If_Init(void)
{
/* USER CODE BEGIN 0 */
/* Unlock the internal flash */
HAL_FLASH_Unlock();

return 0;
/* USER CODE END 0 */
}

/**
* @brief De-Initializes Memory
* @retval USBD_OK if operation is successful, MAL_FAIL else
*/
uint16_t FLASH_If_DeInit(void)
{
/* USER CODE BEGIN 1 */
/* Lock the internal flash */
HAL_FLASH_Lock();

return 0;
/* USER CODE END 1 */
}

/**
* @brief Erase sector.
* @param Add: Address of sector to be erased.
* @retval 0 if operation is successful, MAL_FAIL else.
*/
uint16_t FLASH_If_Erase(uint32_t Add)
{
/* USER CODE BEGIN 2 */
FLASH_EraseInitTypeDef eraseinitstruct;
uint32_t PageError = 0U;
HAL_StatusTypeDef status;


/* Unlock the Flash to enable the flash control register access */
HAL_FLASH_Unlock();

/* Clear OPTVERR bit set on virgin samples */
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);

/* Get the number of sector to erase from 1st sector */
eraseinitstruct.TypeErase = FLASH_TYPEERASE_PAGES;
eraseinitstruct.Banks = GetBank(Add);
eraseinitstruct.Page = GetPage(Add);
eraseinitstruct.NbPages = 1U;
status = HAL_FLASHEx_Erase(&eraseinitstruct, &PageError);

if (status != HAL_OK)
{
return 1U;
}
return 0U;
/* USER CODE END 2 */
}

/**
* @brief Memory write routine.
* @param src: Pointer to the source buffer. Address to be written to.
* @param dest: Pointer to the destination buffer.
* @param Len: Number of data to be written (in bytes).
* @retval USBD_OK if operation is successful, MAL_FAIL else.
*/
uint16_t FLASH_If_Write(uint8_t *src, uint8_t *dest, uint32_t Len)
{
/* USER CODE BEGIN 3 */
uint32_t i = 0;

/* Clear OPTVERR bit set on virgin samples */
__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_OPTVERR);

for (i = 0; i < Len; i += 8)
{
/* Device voltage range supposed to be [2.7V to 3.6V], the operation will
* be done by byte */
if (HAL_FLASH_Program
(FLASH_TYPEPROGRAM_DOUBLEWORD, (uint32_t) (dest + i),
*(uint64_t *) (src + i)) == HAL_OK)
{
/* Check the written value */
if (*(uint64_t *) (src + i) != *(uint64_t *) (dest + i))
{
/* Flash content doesn't match SRAM content */
return 2;
}
}
else
{
/* Error occurred while writing data in Flash memory */
return 1;
}
}
return 0;
/* USER CODE END 3 */
}

/**
* @brief Memory read routine.
* @param src: Pointer to the source buffer. Address to be written to.
* @param dest: Pointer to the destination buffer.
* @param Len: Number of data to be read (in bytes).
* @retval Pointer to the physical address where data should be read.
*/
uint8_t *FLASH_If_Read(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 a valid address to avoid HardFault */
return (uint8_t *) (dest);

/* USER CODE END 4 */
}

/**
* @brief Get status routine
* @param Add: Address to be read from
* @param Cmd: Number of data to be read (in bytes)
* @param buffer: used for returning the time necessary for a program or an erase operation
* @retval USBD_OK if operation is successful
*/
uint16_t FLASH_If_GetStatus(uint32_t Add, uint8_t Cmd, uint8_t *buffer)
{
/* USER CODE BEGIN 5 */
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 */
}

/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */
/**
* @brief Gets the page of a given address
* @param Address: Address of the FLASH Memory
* @retval The page of a given address
*/
static uint32_t GetPage(uint32_t Address)
{
uint32_t page = 0U;

if (Address < (FLASH_BASE + FLASH_BANK_SIZE))
{
/* Bank 1 */
page = (Address - FLASH_BASE) / FLASH_PAGE_SIZE;
}
else
{
/* Bank 2 */
page = (Address - (FLASH_BASE + FLASH_BANK_SIZE)) / FLASH_PAGE_SIZE;
}

return page;
}
/**
* @brief Gets the Bank of a given address
* @param Address: Address of the FLASH Memory
* @retval The Flash bank of a given address
*/
static uint32_t GetBank(uint32_t Address)
{
uint32_t bank = 0U;

if (Address < (FLASH_BASE + FLASH_BANK_SIZE))
{
/* Bank 1 */
bank = FLASH_BANK_1;
}
else
{
/* Bank 2 */
//bank = FLASH_BANK_2;
bank=2;
//应该没有用,bank2没有定义
}

return bank;
}
/* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */

/**
* @}
*/

/**
* @}
*/

注意到这部分代码,因为我的型号没有定义flash bank2,所以编译会过不去,我就把这里注释掉了,反正不会走到这里,不影响功能。

如果你的单片机flash比较大可以考虑把这里注释去掉看看能不能正常编译

image-20250317123847162

原始代码是这样

1
2
3
4
5
else
{
/* Bank 2 */
bank = FLASH_BANK_2;
}

这里有几个坑点。

首先这里面的接口函数名字全都是改过的,网上的教程基本上都是老版本,MEM_If_Init_HS之类的函数名字(哭

然后很多以前可用的函数调用现在都没有了,比如Sector Erase根本没这个模式了,只能用page擦除

还有program flash函数也没法用word模式写了,最小就是DWORD,一次写64bit,这里我之前尝试了好久用以前的代码的word模式手动改成DWORD写入,但是一直不成功,每次都报校验失败,用STLINK读内存区一看,偶数地址全都是0没有写入,也就是DWORD的64bit只写了前32bit(哭

img

img

ps:后面对比官方代码看应该是32bit指针和64bit指针导致的问题,我对于C的指针理解还是太不熟练了,这玩意太容易搞出问题了

总之无视上面这一堆话,直接copy官方的代码就能稳定跑起来了,让人感叹天下代码一大抄,自己写永远不如找官方例程抄(乐

最后实现main里的用户程序跳转逻辑和自己的代码功能

实际上dfu的部分是不需要对main做任何修改的,以上的代码改完就可以实现dfu下载的功能了

但是这样上电会永远默认进入DFU模式,也没法跳转进入自己的app,就一直卡在dfu模式了。

所以我们需要在main里加入判断是否进入DFU升级模式的代码和不进入DFU时候直接跳转到用户app代码执行的功能。

首先一点,如果上电时候执行了 MX_USB_Device_Init();函数(cube自动生成的)就会直接进usb dfu,电脑会识别到dfu接口

所以我们把cube自动生成的函数调用注释掉,手动加入判断逻辑来决定是否初始化usb dfu

在main函数里定义跳转变量

1
2
3
4
5
6
7
8
9
int main(void)
{

/* USER CODE BEGIN 1 */
typedef void (*pFunction)(void);

pFunction JumpToApplication;
uint32_t JumpAddress;
/* USER CODE END 1 */

在主循环前加入判断是否进入DFU的代码

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
 /* USER CODE BEGIN 2 */
if(!HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2)) //上电时PA2 key按下 进入DFU模式
{
MX_USB_Device_Init(); //初始化DFU USB

while(1) //DFU程序
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_14);
//如果进入了dfu程序,绿灯闪烁
HAL_Delay(500);
}
}

//没有进DFU就直接跳到用户程序

if (((*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD) & 0x2FFC0000) == 0x20000000)
{
/* Jump to user application */
JumpAddress = *(__IO uint32_t *) (USBD_DFU_APP_DEFAULT_ADD + 4);
JumpToApplication = (pFunction) JumpAddress;

/* Initialize user application's Stack Pointer */
HAL_RCC_DeInit();
HAL_DeInit(); //关掉你在dfu里初始化的所有外设,防止重新初始化冲突导致程序死掉
__set_MSP(*(__IO uint32_t *) USBD_DFU_APP_DEFAULT_ADD);
JumpToApplication();
}
/* USER CODE END 2 */

如果上电时候按着PA2的按键,就会执行usb初始化,识别到dfu usb设备,然后进入死循环,不再执行下面的跳转代码

这里的循环里可以实现自己想要的dfu固件功能,想写点啥都可以。

也可以自己改进dfu的逻辑,比如通过用户程序复位重新初始化和读写某个存储器里的标志位判断进入dfu来实现代码控制自动升级。

如果没有按下按键,就会往下走,执行跳转用户程序代码。

这部分代码主要是把栈指针和PC程序指针都跳转到用户程序区开始的位置,也就是之前在Cube里定义的那个DFU区结束地址。

注意关掉外设那两句,必须要加,我之前就总是莫名其妙的在跳转进用户程序后单片机死掉,大概是因为冲突导致死进错误中断里了。

到这里DFU固件就改完了,可以编译生成hex

image-20250317131141208

注意改一下代码空间限制,防止写爆了超出DFU固件区。

image-20250317131215950

因为usb代码还挺大的,为了省空间塞进16k里面,我开了这个。

当然如果你的单片机flash够大可以直接给dfu区开个32k或者48k啥的。

image-20250317131431826

空间不太富裕。

然后直接用stlink把dfu固件烧进去就ok了

如果你安装好了cube programmer的驱动,这时候按住按键,插入usb给单片机上电,应该就能看到LED闪烁,并且设备管理器里可以识别出dfu设备了。说明已经正确进入dfu固件。

image-20250317131737051

下面就是编写用户app,需要做一些小修改。

编写用户程序

正常创建一个工程,随便写你正常需要用的功能

image-20250317143429029

我这里让红绿两个LED都闪烁

image-20250317143633705

然后修改下程序起始地址和空间大小(还剩112k,之前算过,填进去)

image-20250317143845503

最后修改下中断向量表偏移,也就是偏移掉DFU的16k地址区,记得把前面那行取消注释,不然不会生效

编译,生成hex文件。

DFU下载

进到dfu模式,打开cube programmer

image-20250317144225144

选usb,刷新一下选你找到的端口,连接

image-20250317144245980

这是正常连接的样子

image-20250317144316240

选刚才编译的用户程序hex文件,正常下载即可。

image-20250317144344243

下载成功。因为选了运行程序,所以会自动复位跳转到用户程序,usb连接会丢失。

此时已经可以看到用户程序正常运行,两个led灯都在闪烁。

一些测试

连stlink读一下内存

image-20250317144650962

DFU固件的代码大概到了3900这个位置,也就是14k的代码大小

image-20250317144759822

4000开始是用户程序代码,可以看到第一个数就是我们MSP堆栈指针的值

我在cube programmer里用stlink重新刷写了一遍用户程序,对比了下内存空间,发现是一致的,说明DFU烧录的代码是正确的。

并且也不会覆盖掉前面的DFU代码(用cube prog重新烧写dfu固件也不会覆盖用户程序)

所以依然可以正常使用stlink进行用户程序的调试和下载,不影响正常的编写调试

尝试用DFU升级DFU固件,会报错失败,而且把程序写坏掉,不要这么做(前面16k地址空间是不可写的)

待续

  • 使用CubeIDE实现(需要改链接脚本来控制地址偏移)
  • 程序控制的自动重启OTA
  • 自定义的DFU上位机集成