连载教程,保姆级教学
设计思路
闲来无事,博文好酒也没更新了。
思来想去,还是觉得写个小文章。
stm32也接触那么多年了,期间也做了很多的小玩意,不过大都是一些以学习为目的的DIY。
博主本身的话还是比较喜欢一些精细小巧的东西,在画电路板的时候,体积往往是尽可能的小一些,导致很多时候电路板的空间并不富裕,甚至于连stm32的烧录接口都没有留,于是就有了IAP的需求。
设计步骤
一般的IAP,分为三个部分。
1 上位机:负责发送新的固件給单片机
2 bootload: 负责固件的版本检测和更新,app程序的跳转和备份
3 app:负责程序的运行
bootload的设计
bootload的设计,一般都是通过通讯接口来接受bin文件,包括但不限于uart,spi,iic。本期教程采用uart来做讲解
教程开始前,请完成一下功能的验证
1 串口数据的收发
2 flash的读写和擦除
3 默认以上两点都已学会并熟练掌握
4 没有了
开个玩笑,现在开始正式讲解
bootload在上文中已经提及过具体的内容,分别是固件的检测和更新,在程序运行的时候检测固件是否需要更新,方法有很多种,在这里笔者推荐2种方式
1 程序初始化后,在一定的时间内,循环向上位机发送数据,可将当前固件版本号发送给上位机,然后上位机进行版本对比,若需要升级,则下发命令,单片机进入接收数据的状态
2 板载按钮,程序初始化后,在一定的时间内,若检测到按钮被按下,则单片机进入接收数据的状态
以上方法皆可,包括也不限于这两种方法,笔者这里自己设计的板子,带有触摸显示屏,单片机开始的时候刷屏,提示是否进入固件接收状态,点击屏幕即可进入接收状态。
以下是程序逻辑
HAL_Init(); //hal init
SystemClock_Config(); //system init
MX_GPIO_Init(); //gpio init 初始化按键
MX_DMA_Init(); //dma init spi dma 初始化 刷屏使用
MX_SPI1_Init(); //spi init spi 初始化 刷屏使用
MX_USART1_UART_Init(); //uart init uart 初始化 用于和上位机通讯
MX_ADC2_Init(); //adc init 电池电量检测
MX_TIM2_Init(); //tim init 定时器
HAL_UART_Receive_IT(&huart1,&UART1struct.RX_Tmp,1); //开启串口接收中断
if(HAL_GPIO_ReadPin(PWR_BTN_GPIO_Port,PWR_BTN_Pin)== GPIO_PIN_SET)
{
//开机检测,判断是插电开机还是按键开机,并锁存供电按钮
HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port,PWR_CTRL_Pin,GPIO_PIN_SET);
while(check_pwr_btn());
}
//lcd init
LCD_Init();
LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
LCD_BLK_Set();
CST816T_Init();
UI_Init(); //重点,在此处进行判断,刷屏询问是否要升级固件,如果不需要升级,则进行app加载(判断bootflag的状态)
if(bootflag == 0) //不需要升级
{
JumpToApp(); //则进行程序跳转
}
UI_Upgrade(); //刷屏,显示程序进行升级中
while (1)
{
//在开始传输数据之前,每500ms向上位机发送一次数据,获取固件的信息
if(check_fw_flag == 0)
{
//ymodem协议,循环向上位机发送C,告诉上位机,自己已进入数据接收状态
HAL_UART_Transmit(&huart1,&C,1,100);
HAL_Delay(1000);
}
//关机
if(HAL_GPIO_ReadPin(PWR_BTN_GPIO_Port,PWR_BTN_Pin)== GPIO_PIN_SET)
{
while(check_pwr_btn());
HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port,PWR_CTRL_Pin,GPIO_PIN_RESET);
__set_FAULTMASK(1);
HAL_NVIC_SystemReset();
HAL_Delay(5000);
}
}
主程序大概的逻辑就是这样子了
bootload数据的接收处理函数
在学习接收数据的方法之前,要了解一个协议,Ymodem协议,网上有很多详细的讲解,笔者在这里简单的介绍一下,着重讲解一下实现
//符号 数值 含义
//SOH 0X01 128字节数据包
//STX 0X02 1024字节数据包
//EOT 0X04 结束传输
//ACK 0X06 回应
//NAK 0X15 不回应
//CA 0X18 传输终止
//C 0X43 请求数据包
在主函数的代码片里面可以看到
//ymodem协议,循环向上位机发送C,告诉上位机,自己已进入数据接收状态
HAL_UART_Transmit(&huart1,&C,1,100);
HAL_Delay(1000);
这里就是在上位机发送数据之前,每隔1ms向上位机发送一个0x43 的数据,告诉上位机自己进入等待数据接收的状态
然后我们看接收数据的实现,数据接收分为两部分,第一部分是不定长数据的接收,第二部分是不定长数据接收的判断
串口不定长接收处理
这里采用的是hal库,因此在串口接收到数据后,会进入串口接收回调函数里,在这里重写数据接收逻辑。笔者这里初始化了一个定时器,当串口在定时器计时时间内收到数据,定时器被重置,继续计时,当计时超时,则进行数据处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
UNUSED(huart);
UART1struct.RX_TempBuff[UART1struct.RX_TempLen] = UART1struct.RX_Tmp; //将接收到的数据放到缓存数组中
UART1struct.RX_TempLen++; //缓存下标+1
HAL_UART_Receive_IT(&huart1,&UART1struct.RX_Tmp,1); //开启串口接收中断
HAL_TIM_Base_Start_IT(&htim2); //开启定时器
__HAL_TIM_SET_COUNTER(&htim2,0); //定时器计时清零
}
定时器回调函数
//符号 数值 含义
//SOH 0X01 128字节数据包
//STX 0X02 1024字节数据包
//EOT 0X04 结束传输
//ACK 0X06 回应
//NAK 0X15 不回应
//CA 0X18 传输终止
//C 0X43 请求数据包
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
char file_name[64] = {0}; //接收到的文件名
int file_len = 0; //接收到的文件长度
int i = 0,j = 0; //临时变量
uint8_t number = 0; //临时变量
uint32_t write_addr = 0; //写入的地址
if(htim == &htim2) //判断中断是否来自于定时器2
{
HAL_TIM_Base_Stop_IT(&htim2); //关闭定时器
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin); //反转指示灯状态
switch(UART1struct.RX_TempBuff[0]) //判断帧头数据
{
case 0x01: //握手完成,上位机发送启示帧,协议开始传输,协议格式
{ //SOH | 0X00 0XFF | FileName+0x00 | FileSize+0x00 | NULL(0X00)
//0x01 | 0x00 0xff | watch.bin 0x00 | 123456 0x00 | 0x00...
check_fw_flag = 1; //将向上位机发送请求数据的标志位置1,之后单片机将被不会继续上上位机发送数据请求标识符
for(i = 0; i <UART1struct.RX_TempLen-3;i++) //拷贝数组
{
UART1struct.RX_Buff[i] = UART1struct.RX_TempBuff[i+3];
}
sscanf((const char*)UART1struct.RX_Buff,"%s",file_name); //解析文件名
j = strlen(file_name);
for(i = 0; i <UART1struct.RX_TempLen-3;i++)
{
UART1struct.RX_Buff[i] = UART1struct.RX_Buff[i+j+1];
}
sscanf((const char*)UART1struct.RX_Buff,"%d",&file_len); //解析文件大小
i = EraseFlash(APP_STAR_ADDR,APP_STOP_ADDR); //擦除flash
if(i == 2) //判断擦除是否成功
{
EraseFlash(APP_STAR_ADDR,APP_STOP_ADDR);
}
HAL_UART_Transmit(&huart1,&ACK,1,100); //向上位机发送应答信号
}break;
case 0x02: //数据接收函数处理
{
if(UART1struct.RX_TempBuff[1]+UART1struct.RX_TempBuff[2] == 0xff) //校验帧头数据的合法性
{
write_addr = UART1struct.RX_TempBuff[1]*1024; //flash写入数据的地址,等于帧头(包号*1024)
for(i = 0; i <UART1struct.RX_TempLen-3;i++) //将数据拷贝出来
{
UART1struct.RX_Buff[i] = UART1struct.RX_TempBuff[i+3];
}
UART1struct.RX_Len = UART1struct.RX_TempLen-3;
FlashWriteAPP(APP_STAR_ADDR+write_addr,UART1struct.RX_Buff,UART1struct.RX_Len); //将数据写入flash
HAL_UART_Transmit(&huart1,&ACK,1,100); //向上位机发送应答信号
}
}break;
case 0x04: //结束传输,程序重启
{
if(UART1struct.RX_TempBuff[1]+UART1struct.RX_TempBuff[2] == 0xff)
{
HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port,PWR_CTRL_Pin,GPIO_PIN_RESET);
__set_FAULTMASK(1);
HAL_NVIC_SystemReset();
HAL_Delay(5000);
// JumpToApp();
}
}
default:
break;
}
// FlashWriteAPP(APP_STAR_ADDR,UART1struct.RX_TempBuff,UART1struct.RX_TempLen);
for(i = 0; i < 1033; i++) //数组里面的数据处理后,将数据全部清0,以免数据出现意外
{
UART1struct.RX_TempBuff[i] = 0;
UART1struct.RX_Buff[i] = 0;
}
UART1struct.RX_TempLen= 0;
UART1struct.RX_Len = 0;
}
}
程序的跳转
将程序跳转也是分为两部分
第一部分是将程序跳转至app的地址,实现如下
__IO uint32_t BootAddr = 0x08010000;
void (*SysMemBootJump)(void);
static void JumpToApp(void)
{
uint32_t i = 0;
//关闭全局中断
DISABLE_INT();
//关闭滴答定时器,复位到默认值
SysTick->CTRL = 0;
SysTick->LOAD = 0;
SysTick->VAL = 0;
//关闭所有中断,清除中断挂起标志
for(i = 0; i < 8; i++)
{
NVIC->ICER[i] = 0xffffffff;
NVIC->ICPR[i] = 0xffffffff;
}
//使能全局中断
ENABLE_INT();
//跳转到系统bootload,首地址时MSP, 地址+4是复位中断服务程序地址
SysMemBootJump = (void (*)(void)) (*((uint32_t *) (BootAddr + 4)));
/* 设置主堆栈指针 */
__set_MSP(*(uint32_t *)BootAddr);
/* 在RTOS工程,这条语句很重要,设置为特权级模式,使用MSP指针 */
__set_CONTROL(0);
/* 跳转到系统BootLoader */
SysMemBootJump();
/* 跳转成功的话,不会执行到这里,用户可以在这里添加代码 */
while (1)
{
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
HAL_Delay(100);
}
}
第二部分是在app中实现的,因为app的起始地址和bootload的起始地址不一样,跳转完成后,中断向量映射的地址也不同,因此需要在app中重新设置中断向量映射地址,这里简单说一下
修改system_stm32xxx.h文件中的VECT_TAB_OFFSET地址,这里就是设置偏移地址的值,在函数SystemInit这里可看出具体的实现,这里就不多说了
void SystemInit(void)
{
/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << (10*2))|(3UL << (11*2))); /* set CP10 and CP11 Full Access */
#endif
/* Configure the Vector Table location add offset address ------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET
; /* Vector Table Relocation in Internal SRAM */
#endif /* USER_VECT_TAB_ADDRESS */
}
上位机的实现
上位机还是老一套,实现的功能为当打开程序时,自动检测串口,当识别为ch340的设备后打开该设备的com口,然后打开bin文件和下发文件,在这里只贴出效果和简单的串口命令下发的代码。如果需要源码,则可以自行下载
下图分别为未插入ch340设备的界面,插入ch340后打开的界面,以及选择bin文件后的界面
/// <summary>
/// 打开可用的串口
/// </summary>
private void OpenUseSerialPort()
{
if (ConnectState.BackColor != Color.Lime)
{
//"0403", "6001" USB转232
//"067B", "2303" USB转485
//"0986", "0320" PLC端口
//"1a86", "7523" CH340
//portName = FormMain.GetPortNameFormVidPid("0986", "0320");
List<string> availport;
availport = GetPortNameFormVidPid("1a86", "7523");
//portName = GetPortNameFormVidPid("067B", "2303");
if (availport != null)
{
string[] port = availport.ToArray();
CBVarPort.Items.AddRange(port);
//timer1.Enabled = true;
}
else
{
CBVarPort.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); //获取所有串口设备
DisplayLog(1, "NO find Correspond port!");
return;
}
if (CBVarPort.Items.Count > 0)
{
CBVarPort.SelectedIndex = 0;
if (mc.IsOpen == false)
{
try
{
portName = CBVarPort.Items[0].ToString();
Debug.WriteLine("Use port:" + portName);
// bool isOpen = mc.Open(portName, Baudrate, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One, 200, 200);
mc.PortName = portName;
mc.BaudRate = Baudrate;
mc.Parity = Parity.None;
mc.StopBits = StopBits.One;
mc.DataBits = 8;
mc.Open();
if (!mc.IsOpen)
{
//MessageBox.Show("串口打开失败");
Console.ForegroundColor = ConsoleColor.Red;
DisplayLog(1, "* Open port fail");
Console.ForegroundColor = ConsoleColor.Green;
}
else
{
//串口打开成功,modbus协议初始化成功,打开定时器
//MessageBox.Show("串口打开成功");
ConnectState.Text = "Connect";
ConnectState.BackColor = Color.Lime;
DisplayLog(1, "* Open port ok");
mc.Write("data\r\n");
}
}
catch (Exception ex)
{
MessageBox.Show("Open port error" + ex, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
ConnectState.Text = "NULL";
ConnectState.BackColor = Color.Yellow;
Console.ForegroundColor = ConsoleColor.Red;
DisplayLog(1, "* Open port error");
Console.ForegroundColor = ConsoleColor.Green;
}
}
}
else
{
MessageBox.Show("NO Use Serialport", "Waring", MessageBoxButtons.OK, MessageBoxIcon.Question);
Console.ForegroundColor = ConsoleColor.Red;
DisplayLog(1, "* No available port");
Console.ForegroundColor = ConsoleColor.Green;
}
}
else
{
mc.Close();
ConnectState.Text = "NULL";
ConnectState.BackColor = Color.Yellow;
DisplayLog(1, "* Port close");
}
//mc.ReadTimeout = 50;
mc.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
}
//Ymodem传输协议
//符号 数值 含义
//SOH 0X01 128字节数据包
//STX 0X02 1024字节数据包
//EOT 0X04 结束传输
//ACK 0X06 回应
//NAK 0X15 不回应
//CA 0X18 传输终止
//C 0X43 请求数据包
byte SOH = 0x01;
byte STX = 0X02;
byte EOT = 0X04;
byte ACK = 0X06;
byte NAK = 0X15;
byte CA = 0X18;
byte C = 0X43;
static byte[] sendData= new byte[1033];
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
Thread.Sleep(100);
try
{
string indata = sp.ReadExisting();
try
{
if (sendFlag == true)
{
if (indata[0] == C) //请求数据传输
{
DisplayLog(1, "handshake");
int len1 = 0;
int len2 = 0;
sendData[0] = SOH; //帧头
sendData[1] = 0x00; //包号
sendData[2] = 0xff; //包号反码
byte[] decBytes = System.Text.Encoding.UTF8.GetBytes(label1.Text);
decBytes.CopyTo(sendData, 3);//将文件名追加到sendData中
len1 = label1.Text.Length;
sendData[3 + len1] = 0x00;
byte[] decBytes1 = System.Text.Encoding.UTF8.GetBytes(label4.Text);
decBytes1.CopyTo(sendData, 4 + len1);
len2 = label4.Text.Length;
for (int i = (4 + len1 + len2); i < 131; i++)
{
sendData[i] = 0x00;
}
mc.Write(sendData, 0, 131);
}
if (indata[0] == ACK)
{
if (number < point)
{
sendData = lists[number].ToArray();
mc.Write(sendData, 0, 1027);
number++;
progressBar1.PerformStep();
int j = (int)(((float)number / (float)point)*100);
label5.Text = j.ToString() + "%";
DisplayLog(1, point.ToString()+" -> "+number);
}
else
{
sendData[0] = EOT;
sendData[1] = 0x00;
sendData[2] = 0xff;
for (int i = 3; i < 133; i++)
{
sendData[i] = 0x00;
}
mc.Write(sendData, 0, 131);
DisplayLog(1, "Send Over!");
point = 0;
number = 0;
sendFlag = false;
}
}
}
}
catch (Exception ex)
{
}
}
catch (Exception ex)
{
}
}
/// <summary>
/// 通过vid,pid获得串口设备号
/// </summary>
/// <param name="vid">vid</param>
/// <param name="pid">pid</param>
/// <returns>串口号</returns>
public static List<string> GetPortNameFormVidPid(string vid, string pid)
{
Guid myGUID = Guid.Empty;
string enumerator = "USB";
//string enumerator = "CH340";
List<string> AvailPort = new List<string>(16);
try
{
IntPtr hDevInfo = HardWareLib.SetupDiGetClassDevs(ref myGUID, enumerator, IntPtr.Zero, HardWareLib.DIGCF_ALLCLASSES | HardWareLib.DIGCF_PRESENT);
if (hDevInfo.ToInt32() == HardWareLib.INVALID_HANDLE_VALUE)
{
throw new Exception("没有该类设备");
}
HardWareLib.SP_DEVINFO_DATA deviceInfoData;//想避免在api中使用ref,就把structure映射成类
deviceInfoData = new HardWareLib.SP_DEVINFO_DATA();
deviceInfoData.cbSize = 28;//如果要使用SP_DEVINFO_DATA,一定要给该项赋值28=16+4+4+4
deviceInfoData.devInst = 0;
deviceInfoData.classGuid = System.Guid.Empty;
deviceInfoData.reserved = 0;
UInt32 i;
StringBuilder property = new StringBuilder(HardWareLib.MAX_DEV_LEN);
for (i = 0; HardWareLib.SetupDiEnumDeviceInfo(hDevInfo, i, deviceInfoData); i++)
{
// Console.Write(deviceInfoData.classGuid.ToString());
// HardWareOperation.SetupDiGetDeviceInstanceId(hDevInfo, deviceInfoData, porperty, (uint)porperty.Capacity, 0);
HardWareLib.SetupDiGetDeviceRegistryProperty(hDevInfo, deviceInfoData, (uint)HardWareLib.SPDRP_.SPDRP_CLASS, 0, property, (uint)property.Capacity, IntPtr.Zero);
if (property.ToString().ToLower() != "ports") continue;//首先看看是不是串口设备(有些USB设备不是串口设备)
HardWareLib.SetupDiGetDeviceRegistryProperty(hDevInfo, deviceInfoData, (uint)HardWareLib.SPDRP_.SPDRP_HARDWAREID, 0, property, (uint)property.Capacity, IntPtr.Zero);
if (!(property.ToString().ToLower().Contains(vid.ToLower()) && property.ToString().ToLower().Contains(pid.ToLower()))) continue;//找到对应于vid&pid的设备
HardWareLib.SetupDiGetDeviceRegistryProperty(hDevInfo, deviceInfoData, (uint)HardWareLib.SPDRP_.SPDRP_FRIENDLYNAME, 0, property, (uint)property.Capacity, IntPtr.Zero);
// break; //查询到一个就退出
string friendlyName = property.ToString();
char[] separatorMark = { '(', ')' };
string[] strList1 = friendlyName.Split(separatorMark);
if (strList1[1].Substring(0, 3) == "COM")
{
AvailPort.Add(strList1[1]);
}
}
HardWareLib.SetupDiDestroyDeviceInfoList(hDevInfo);//记得用完释放相关内存
if (AvailPort.Count > 0)
{
return AvailPort;
}
else
return null;
}
catch (Exception ex)
{
// MessageBox.Show(ex.Message);
return null;
}
}
/// <summary>
/// 打开BIN文件得到路径,读取其中的二进制内容
/// </summary>
/// <returns>返回二进制数字符串</returns>
///
public static int OpenBinFile()
{
//string bin_str = "";
//byte[] binchar = new byte[] { };
int file_len;
//打开文件类
OpenFileDialog dialog = new OpenFileDialog();
//使用当前目录作为初始目录
dialog.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
//文件过滤,仅打开bin
dialog.Filter = "bin文件(*.bin)|*.bin";
//关闭选择多文件
dialog.Multiselect = false;
if (dialog.ShowDialog() == DialogResult.OK)
{
//文件流类//用于文件的读写与关闭//来自于System.IO
FileStream fileStream = new FileStream(dialog.FileName, FileMode.Open);
//读二进制文件类
BinaryReader br = new BinaryReader(fileStream, Encoding.UTF8);
//获取bin文件长度
file_len = (int)fileStream.Length;
//得到所有字节
//binchar = br.ReadBytes(file_len);
dddd = br.ReadBytes(file_len);
file_name = fileStream.Name;
file_name = Path.GetFileName(file_name);
fileStream.Close();
return file_len;
}
//返回字符串
return 0;
}
/// <summary>
/// start/stop按钮单击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
//private List<byte> list = new List<byte>();
private List<byte> list = null;
private List<List<byte>> lists = new List<List<byte>>();
private void button2_Click(object sender, EventArgs e)
{
int len = OpenBinFile();
int leng = len % 1024;
open_file_len = len;
label4.Text = len.ToString();
label1.Text = file_name;
var index = 0;
//list = null;
foreach (var item in dddd)
{
if(list == null)
{
list = new List<byte>();
}
if (list.Count == 0)
{
list.Add(STX);
list.Add(((byte)point));
list.Add((byte)~point);
}
list.Add(item);
if (++index == 1024)
{
lists.Add(list);
list =null;
index = 0;
point++;
}
}
for (int i = 0; i < (1024-leng); i++)
{
list.Add(0x1A);
}
lists.Add(list);
point++;
progressBar1.Visible = true;
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.Maximum = (int)point;
progressBar1.Minimum = 0;
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Step = 1;
label5.Text = "0%";
label5.Visible = true;
}
各位看官记得收藏关注,后续将上位机源码上传