1. RS485固件升级为何需要自定义协议
在工业现场搞固件升级,最头疼的就是环境复杂。电磁干扰、线路老化、设备距离远,这些因素都可能导致数据传输出错。我当年第一次做RS485升级时就踩过坑,直接用Ymodem协议传固件,结果在电机启动的瞬间数据就错乱了,设备直接变砖。
RS485总线虽然抗干扰能力强,传输距离远,但本质上还是个半双工通信。这意味着我们需要自己管理数据流向,避免冲突。工业现场常见的变频器、电机驱动器都是干扰源,随便一个电磁脉冲就可能让数据包出错。
所以必须设计一套自定义协议,包含这几个核心要素:
- 帧头帧尾标识:明确数据包的开始和结束
- 长度字段:知道这个包有多长
- 校验机制:确保数据没被干扰
- 重传机制:出错后能自动恢复
我设计的协议格式是这样的:
0x91 | 0xAA*6 | 0xFF*6 | 数据长度(2字节) | 数据内容(n字节) | 校验和(2字节)
0x91是起始标志,连续6个0xAA表示ACK确认包,6个0xFF表示结束包。校验和用的是简单的累加和,实际项目中也可以用CRC16,更可靠。
2. 协议设计中的坑与解决方案
2.1 数据包设计要点
数据包不是越大越好。虽然RS485理论上可以传很长的包,但在工业环境下,小包更可靠。我一般把数据包控制在512字节以内,原因有三:
第一,小包传输时间短,受干扰概率低。一个大包传500ms,中间来个干扰就全完了。拆成10个50ms的小包,即使丢一个也能重传。
第二,内存占用小。嵌入式设备RAM有限,STM32F103只有20K内存,如果包太大,光接收缓冲区就占去一大半。
第三,响应及时。设备需要及时回复ACK,包太大就会阻塞通信。
校验机制要足够健壮。最初我用的是奇偶校验,发现根本扛不住工业干扰。后来改用16位校验和,效果好了很多。现在新项目都改用CRC16了,虽然计算量大点,但可靠性更高。
2.2 超时重传策略
超时重传是保证可靠性的关键。但超时时间设多久很有讲究:太短会误判,太长影响效率。
我的经验值是:
- ACK等待超时:100-500ms,根据线路长度调整
- 包间超时:2-5倍包传输时间
- 总升级超时:5-10分钟,防止卡死
实现时要用状态机管理升级流程:
typedef enum {
STATE_IDLE, // 空闲状态
STATE_WAIT_ACK, // 等待ACK
STATE_SENDING, // 发送数据中
STATE_END // 升级结束
} upgrade_state_t;
每个状态都要有超时处理,超时后根据策略重试或退出。我一般设置3次重试,超过3次就认为升级失败。
3. 代码实现详解
3.1 串口与中断配置
RS485需要配置为半双工模式,关键是要控制DE引脚(数据使能)。发送时拉高,接收时拉低。这个时序一定要准,否则数据会冲突。
// USART1初始化配置
huart1.Instance = USART1;
huart1.Init.BaudRate = 250000; // 工业常用波特率
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_2; // 2停止位更可靠
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_8; // 8倍过采样抗干扰
HAL_UART_Init(&huart1);
// 使能接收中断
HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
中断接收处理要注意效率。我见过有人在中断里做复杂处理,结果丢数据丢得厉害。正确做法是在中断里只收数据,放到缓冲区,在主循环中解析。
// 中断接收回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1) {
// 简单放入缓冲区,立即重新开启接收
ring_buf_put(&rx_ring, rx_byte);
HAL_UART_Receive_IT(huart, &rx_byte, 1);
}
}
3.2 数据包解析实现
数据包解析要用状态机,逐字节处理。首先找帧头0x91,找到后读长度字段,然后收数据,最后校验。
// 数据包解析状态机
typedef enum {
STATE_SYNC, // 同步状态,找0x91
STATE_LEN_H, // 读长度高字节
STATE_LEN_L, // 读长度低字节
STATE_DATA, // 读数据内容
STATE_CHECK_H, // 读校验和高字节
STATE_CHECK_L // 读校验和低字节
} parse_state_t;
void parse_packet(uint8_t byte)
{
static parse_state_t state = STATE_SYNC;
static uint16_t data_len = 0;
static uint16_t data_index = 0;
static uint32_t checksum = 0;
static uint8_t data_buf[MAX_PACKET_SIZE];
switch(state) {
case STATE_SYNC:
if(byte == 0x91) {
state = STATE_LEN_H;
checksum = byte;
}
break;
case STATE_LEN_H:
data_len = byte << 8;
checksum += byte;
state = STATE_LEN_L;
break;
// 其他状态处理...
}
}
校验通过后,要把数据写入Flash。这里要注意Flash写入前必须先擦除,而且要以页为单位擦除。
3.3 数据发送与ACK处理
发送数据包要封装成协议格式,计算校验和。发送前控制RS485为发送模式,发送完切回接收模式。
void send_packet(uint8_t *data, uint16_t len)
{
// 切换到发送模式
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
delay_us(10); // 等待稳定
// 构造协议包
uint8_t packet[MAX_PACKET_SIZE];
packet[0] = 0x91; // 帧头
packet[1] = (len >> 8) & 0xFF; // 长度高字节
packet[2] = len & 0xFF; // 长度低字节
memcpy(&packet[3], data, len); // 数据内容
// 计算校验和
uint32_t sum = 0;
for(int i = 0; i < len + 3; i++) {
sum += packet[i];
}
packet[len+3] = (sum >> 8) & 0xFF;
packet[len+4] = sum & 0xFF;
// 发送
HAL_UART_Transmit(&huart1, packet, len + 5, HAL_MAX_DELAY);
// 切回接收模式
delay_us(10);
HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
}
ACK处理要有超时机制。发送后启动定时器,在规定时间内没收到ACK就重发。
// 等待ACK,带超时
uint8_t wait_for_ack(uint32_t timeout_ms)
{
uint32_t start_time = HAL_GetTick();
while(HAL_GetTick() - start_time < timeout_ms) {
if(ack_received) {
ack_received = 0;
return 1;
}
// 处理其他任务,避免死等
background_tasks();
}
return 0; // 超时
}
4. Flash操作关键细节
4.1 Flash分区规划
Flash分区是IAP的基础,分不好后面全白搭。我的经验是:
- Bootloader放在开头,大小留足余量(至少16K)
- 主程序区放当前运行的程序
- 备份区放新下载的程序
- 标志区放升级标志和系统配置
具体分区表如下:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 16K | 引导程序 |
| Flag Area | 0x08004000 | 4K | 标志位和配置 |
| Main APP | 0x08005000 | 64K | 主程序区 |
| Backup APP | 0x08015000 | 64K | 备份程序区 |
标志区用来记录升级状态,我一般用几个关键标志:
- 升级标志:是否需要升级
- 校验标志:程序是否完整
- 版本信息:当前程序版本
4.2 Flash读写操作
Flash写操作有严格限制:必须先擦后写,只能由1变0。擦除是以页为单位的,不同型号STM32页大小不同。
// Flash页擦除
void flash_erase_page(uint32_t page_address)
{
HAL_FLASH_Unlock(); // 必须先解锁
FLASH_EraseInitTypeDef erase_init;
erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
erase_init.PageAddress = page_address;
erase_init.NbPages = 1;
uint32_t page_error;
HAL_FLASH_Erase(&erase_init, &page_error);
HAL_FLASH_Lock(); // 操作完上锁
}
// Flash写入,按字(32位)写入
void flash_write_word(uint32_t address, uint32_t data)
{
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);
HAL_FLASH_Lock();
}
写数据时要注意对齐。STM32 Flash编程要求32位对齐,所以最好一次写4字节。如果数据长度不是4的倍数,要补0对齐。
// 写入数据,自动处理对齐
void flash_write_data(uint32_t address, uint8_t *data, uint32_t len)
{
uint32_t words = (len + 3) / 4; // 计算需要写的字数
uint32_t buffer[words];
// 数据复制到缓冲区,补0对齐
memset(buffer, 0, words * 4);
memcpy(buffer, data, len);
// 逐字写入
for(uint32_t i = 0; i < words; i++) {
flash_write_word(address + i * 4, buffer[i]);
}
}
4.3 升级标志位管理
升级标志位很重要,它告诉Bootloader是否需要升级。标志位要放在独立页,避免被误擦除。
我一般这样设计标志位:
#define UPDATE_FLAG_ADDR 0x08004000
#define UPDATE_FLAG_VALUE 0xAA55AA55
// 设置升级标志
void set_update_flag(void)
{
flash_erase_page(UPDATE_FLAG_ADDR);
flash_write_word(UPDATE_FLAG_ADDR, UPDATE_FLAG_VALUE);
}
// 检查升级标志
uint8_t check_update_flag(void)
{
uint32_t flag = *(__IO uint32_t*)UPDATE_FLAG_ADDR;
return (flag == UPDATE_FLAG_VALUE);
}
// 清除升级标志
void clear_update_flag(void)
{
flash_erase_page(UPDATE_FLAG_ADDR);
flash_write_word(UPDATE_FLAG_ADDR, 0xFFFFFFFF);
}
5. Bootloader设计与跳转
5.1 Bootloader工作流程
Bootloader是系统启动的第一个程序,它的主要任务是:
- 初始化基本硬件
- 检查升级标志
- 如果需要升级,从备份区拷贝程序到主程序区
- 跳转到主程序
流程图如下:
上电 → 硬件初始化 → 检查升级标志 →
有升级标志? → 是 → 拷贝程序 → 清除标志 → 跳转主程序
↓
否 → 直接跳转主程序
关键代码结构:
int main(void)
{
// 基本硬件初始化
HAL_Init();
SystemClock_Config();
UART_Init();
Flash_Init();
// 检查升级标志
if(check_update_flag()) {
// 执行升级操作
copy_backup_to_main();
verify_program();
clear_update_flag();
}
// 跳转到主程序
jump_to_app(MAIN_APP_ADDRESS);
while(1); // 正常情况下不会执行到这里
}
5.2 程序跳转关键技术
程序跳转不是简单的函数调用,需要完成以下步骤:
- 禁用所有中断
- 设置主堆栈指针(MSP)
- 设置程序计数器(PC)
- 重新初始化向量表
void jump_to_app(uint32_t app_address)
{
// 禁用所有中断
__disable_irq();
// 检查栈顶地址是否合法
uint32_t msp = *(__IO uint32_t*)app_address;
if((msp & 0x2FF00000) != 0x20000000) {
// 栈地址不合法,不跳转
return;
}
// 设置主堆栈指针
__set_MSP(msp);
// 设置向量表偏移
SCB->VTOR = app_address;
// 获取复位地址并跳转
uint32_t reset_handler = *(__IO uint32_t*)(app_address + 4);
((void(*)())reset_handler)();
// 跳转后不会返回
}
跳转前一定要检查栈顶地址的合法性。STM32的RAM通常从0x20000000开始,所以栈顶地址应该在这个范围内。如果地址不合法,说明程序可能损坏,不能跳转。
5.3 异常处理机制
工业设备最怕升级变砖,所以异常处理一定要周全。我设计的异常处理包括:
电源异常处理:升级过程中断电是最危险的。我的做法是在升级开始时设置一个"升级中"标志,升级完成后清除。如果Bootloader发现这个标志被设置,说明上次升级中断了,需要恢复。
#define UPGRADE_IN_PROGRESS_FLAG 0x08004010
void handle_power_failure(void)
{
if(check_upgrade_in_progress_flag()) {
// 上次升级被中断,需要恢复
if(verify_backup_area() == SUCCESS) {
copy_backup_to_main();
clear_upgrade_flags();
} else {
// 备份区校验失败,无法恢复
enter_recovery_mode();
}
}
}
通信超时处理:升级过程中通信中断要能检测到并超时退出。我一般设置总升级超时时间为10分钟,超时后重启设备。
数据校验失败:每个数据包都要校验,校验失败要求重发。连续多个包校验失败则认为通信质量太差,退出升级。
6. 实战调试技巧
6.1 常见问题排查
做RS485升级最常遇到的问题是通信失败。排查步骤:
第一查硬件:用示波器看RS485波形,确认电平转换正常,DE控制信号时序正确。
第二查接线:RS485必须要接终端电阻,A/B线不能接反。我遇到过因为线序接反导致通信不稳定的情况。
第三查配置:波特率、数据位、停止位、校验位这些配置一定要两端一致。工业设备常用250000bps,2停止位。
第四查干扰:在电机、变频器附近通信时,很容易受干扰。可以尝试降低波特率,或者增加屏蔽措施。
6.2 性能优化建议
内存优化:嵌入式设备内存有限,要精心设计缓冲区大小。我的经验是:
- 接收缓冲区:不小于最大包长的2倍
- 发送缓冲区:足够存放一个完整数据包
- 临时缓冲区:用于Flash操作和数据处理
速度优化:RS485波特率不是越高越好。工业现场长距离传输时,高波特率反而容易出错。我的建议:
- 距离<50米:可用500Kbps
- 50-100米:用250Kbps
- 100-500米:用115200bps
-
500米:用9600bps
可靠性优化:重要数据要多次确认。我一般重要指令发3遍,收到任意一个就算成功。数据包要有序列号,避免重复处理。
6.3 测试验证方法
升级功能一定要充分测试,我总结了一套测试方法:
边界测试:测试最小和最大固件大小,测试包长为0和最大包长的情况。
异常测试:模拟各种异常情况:
- 断电测试:升级过程中随机断电
- 干扰测试:在设备旁开关电机、变频器
- 网络测试:模拟网络延迟、丢包
压力测试:连续升级100次,统计成功率。我要求工业级应用要达到99.9%的成功率。
兼容性测试:测试不同版本间的升级兼容性,包括向前兼容和向后兼容。
最后一定要做现场测试,实验室环境再好也模拟不了真实的工业现场。我去过一个钢厂,现场电磁干扰比实验室强几十倍,很多问题都是在现场才暴露出来的。
记得第一次在现场成功升级时的那种成就感,几十台设备同时升级,看着进度条一个个走完,所有设备都正常启动运行,那种感觉比中了彩票还高兴。工业现场升级就是这样,准备工作做得越充分,实际操作就越顺利。
265



被折叠的 条评论
为什么被折叠?



