软件仿真STM32F10X系列单片机配置方法
keil中软件仿真STM32F10X系列单片机,因初始化时需配置晶振源,仿真时SystemInit会一直等待外部晶振就位,而陷入错误,处理对策:
- Options for target->debug->“Use Simulator";
- 配置"Dialog DLL"和"Parameter",按下图所示:

软件仿真,串口配置步骤
- Option for target ->Target->Use Cross-module optimization;

- 启动debug

- View->System Viewer->Usart->目标串口

UCOS移植
- 在UCOS官网上下载基于STM32的移植工程案例(其中的AN-1018是作者写的移植文档,讲的很详细,可供参考)。
- AN-1018中有一张图,明确表达了移植的工作内容。

- 复制UCOS-ii的源文件,即:OS_CORE.C、OS_FLAG.C、OS_MBOX.C、OS_MEM.C......等11个文件,到自己的工程模板指定文件夹中;
- 复制Port文件,包括:OS_CPU.C、OS_CPU_A.ASM、OS_CPU.H、OS_DBG.C,到自己工程模板指定的文件夹中;
- 复制应用层文件到自己的工程指定文件夹中,应用层包括APP.C、APP_VECT.C、APP_CFG.H、INCLUDES.H、OS_CFG.H,只需要OS_CFG.H、NCLUDES.H两个即可(其他3个是案例中这对官方开发板创建的任务,在不适用官方版时没有必要)。
- 新建2个空文件,APP.C、APP_CFG.H,做为应用层的补充;
- 配置工程模板,确保所有.h文件包含在目录内,如下图:

- 修改配置文件
官网的案例是在IAR编译环境下完成的,要使用MDK环境,则需要做一些编译器的适应性更改:
- OS_CPU_C.C
在STDLib3.5库中,SysTick配置采用更安全的SysTick_Config()函数,所以可屏蔽掉该函数OS_CPU_SysTickInit(),并在main函数中调用SysTick_Config(SystemCoreClock/1000)配置SysTick。
- OS_CPU_A.ASM


- OS_DBG.C
将#define OS_COMPILER_OPT __root修改为:#define OS_COMPILER_OPT
为了降低移植难度,这里禁用钩子函数(必要时候再开启钩子函数),修改OS_CFG.H文中#define OS_APP_HOOKS_EN 1;为#define OS_APP_HOOKS_EN 0;
接下来是最核心的步骤:UCOS与工程的联系纽带是STM32的特殊中断PendSV和SysTick,其中PendSV负责任务调度,SysTick定时器为UCOS提供时基。将启动代码中所有"PendSV_Handler“替换成”OS_CPU_PendSVHandler",“SysTick_Handler”替换成"OS_CPU_SysTickHandler",将这两个中断服务程序定向到OS_CPU_A.ASM和OS_CPU_C.C中。
提醒:启动代码默认是只读的,如
,右键->属性->去掉“只读”属性即可启动编辑,如图

HARD FAULT定位
DEBUG模式下,VIEW->Call Stack Window。
Stack Window中自下而上保存了程序运行的过程,绿色框就是发生HardFault时执行的最后一条代码,调用“Show Caller Code"即可转到对应的代码段分析。

我犯的一个错误:系统移植后新建3个测试任务,分配任务堆栈是将任务数目作为任务堆栈大小误用,从而造成堆栈溢出,发生HardFault错误,查了2天,示例代码如下:
#define TASK_STK_NUM 3
#define TASK_STK_SIZE 512
OS_STK TASK_STK[TASK_STK_NUM][TASK_STK_SIZE];
OSTaskCreate(
&taskFunctionName,
"Print message",
&TASK_STK[0][TASK_STK_NUM-1], //问题出在这里,应该用TASK_STK_SIZE
TASK_PRO
);
配置串口
- 配置时钟
- 配置串口所在的IO口的时钟;
- 配置串口时钟,串口的IO口工作在复用功能模式,所以要配置所在IO的复用功能时钟;
- 配置IO口
- 串口使用的RX、TX口的工作模式不同,需要分别配置;
- 配置中断
- 如使用中断,首先配置中断优先级;
- 配置能引起中断的中断源;
- 配置串口工作模式;
- 配置工程选项,选中“use microlib”;
- 重定向printf函数,即重写putchar函数;
配置RTC
RTC的核心和时钟配置寄存器(RCC_BDCR)在备份数据区(注意,涉及到时钟的寄存器中,RCC_BDCR寄存器是唯一只能有备份数据复位的寄存器)。
系统复位并不会引起备份数据区复位,对备份数据区的访问必须执行设定的程序,如下:
- 在APB1总线时钟使能寄存器中(RCC_APB1ENR)使能PWR和BKP接口时钟,示例代码:
//STD3.5库的示例代码
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP|RCC_APB1Periph_PWR,ENABLE);
//直接操作寄存器
RCC->APB1ENR |= RCC_APB1ENR_BKPEN|RCC_APB1ENR_PWREN;
- 在PWR_CR寄存器中使能DBP,允许访问备份区寄存器和RTC模块,示例代码:
//STD3.5库的示例代码
PWR_BackupAccessCmd(ENABLE);
//直接操作寄存器
PWR->CR |= PWR_CR_DBP;
- 在RCC_BDCR寄存器中配置RTC的时钟,示例代码:
//STD3.5库示例,配置为HSE/128
RCC_RTCCLKConfig(RCC_RTCCLKSource_HSE_Div128);
RCC_RTCCLKCmd(ENABLE);
//操作寄存器示例,配置为HSE/128
RCC->BDCR |= RCC_BDCR_RTCSEL_HSE;
RCC->BDCR |= RCC_BDCR_RTCEN;
- 等待时钟APB1总线同步,注意RCC_BDCR寄存器在备份数据区,对其操作存在接口同步问题,同步后硬件置位RTC_CRL寄存器的RSF位,所以配置后必须等待RSF置位,示例代码:
//STD3.5库
RTC_WaitForSynchro();
//操作寄存器示例
while(!(RTC->CRL & RTC_CRL_RSF));
- 查询RTC_CRL的RTOFF位,“1”代表可以配置;
//STD3.5库
RTC_WaitForLastTask();
//操作寄存器
while(!(RTC->CRL & RTC_CRL_RTOFF);
- 置位RTC_CRL的CNF进入配置备份区寄存器模式,示例代码:
//STD3.5库
RTC_EnterConfigMode();
//操作寄存器
while(!(RTC->CRL & RTC_CRL_CNF);
- 配置一个或多个寄存器;
- 清除CNF位,跳出config模式,示例代码:
//STD3.5库
RTC_ExitConfigMode();
//操作寄存器
RTC->CRL &= ~RTC_CRL_CNF;
- 查询RTOFF位,确保完成写入寄存器,示例代码:
//STD3.5库
RTC_WaitForLastTask();
//操作寄存器
while(!(RTC->CRL & RTC_CRL_CNF);
USB移植
移植环境
基于stsw_stm32121_v4.1.0库(Flib3.5的外置USB驱动库)中的Custom_HID例程,将USB移植到工程中。
移植过程
stsw_stm32121_v4.1.0中USB驱动的分层

- 其中Low Layer和Medium Layer不允许用户修改,且已经独立出来,放置在.\Libraries\STM32_USB-FS-Device_Driver文件夹中,直接拷贝出来备用(下文中,本文件夹以A代指)。
- High Layer层才是用户接口层,库中并未将其独立出来,而是糅合在例程中呈现。以Custom_HID为例,放置在.\Projects\Custom_HID文件夹下的inc和src中。
- .\Projects\Custom_HID\inc中包含内容较多,仅拷贝usb_xx.h格式的文件到A中的inc中即可;
- 同样,.\Projects\Custom_HID\src中文件,仅拷贝usb_xx.h格式的文件到A中的src中即可。
移植到工程
- 将A文件夹拷贝到工程目录中;
- 将src文件增加到工程目录中,如下图所示:
- 配置inc文件夹可被工程遍历,如下图所示:
- 此时编译工程,会报一大堆错误,如__IO未定义等,主要是usb库的各个文件需要操作寄存器,故需要core_cm3.h文件。在usb_lib.h中统一配置比较方便(当然,也可以每个报错的文件中都包含core_cm3.h文件),因涉及到其他库,这里导入"stm32f10x.h“更方便,示例代码如下:
#include "stm32f10x.h"
/*切记要将该代码放在最前面,因为预编译是按排序依次进行的,而其他头文件中需要该头文件内容*/
- 再次编译,报错LED0未定义等等。原因是官网的例程是基于开发板的,而这里不使用开发板环境,遇到地方直接屏蔽即可。
- 配置中断服务程序,本例中使用stm32f103ve芯片,在中断文件中增加usb中断服务程序,并包含“usb_istr.h",示例代码:
#include "usb_istr.h"
....
void USB_LP_CAN1_RX0_IRQHandler(void)
{
USB_Istr(); //库中专门用于处理USB中断的函数
}


再次编译,不再报错。
但此时,仅是将USB驱动库的文件放到了工程文件中,工程中并未使用USB驱动库的任何内容,要使用USB驱动库,需要在main函数中包含USB驱动库头文件。
将驱动库导入到工程中
- 在工程入口文件中包含USB驱动库头文件(本例中中在main.c文件中包含usb_core.h、usb_dese、usb_prop,一般情况下也就用到这三个文件),示意代码:
#include "usb_core.h"
#include "usb_desc.h"
#include "usb_prop.h"
- 为方便后续使用,这里对原有库进行二次开发,将USB的时钟配置,IO口配置、中断优先级配置集成到usb_init.c文件的USB_Init()函数中。
- USB的时钟配置
- STM32系列单片机的启动顺序
- 使用stdlib3.5库(包括其他库)时,启动程序首先调用SystemInit(),并将时钟配置为:HSE时钟->9倍频->PLL时钟->system clock;
- system clock 2分频作为apb1时钟,36Mhz;
- system clock不分频作为apb1时钟,72Mhz;
- USB时钟来自PLL(72MHz),而USB模块的基准时钟要求48MHz,所以需要经过1.5倍分频,配置如下:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB,ENABLE);
RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5);
- USB IO口配置
GPIO_InitTypeDef gpio_initstructure;
RCC_APB1PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,\
ENABLE);
gpio_initstructure.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_initstructure.GPIO_Pin = GPIO_Pin_11|GPIO_Pin_12;
gpio_initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&gpio_initstructure);
- 中断优先级配置
NVIC_InitTypeDef nvic_initstructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
nvic_initstructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
nvic_initstructure.NVIC_IRQChannelCmd = ENABLE;
nvic_initstructure.NVIC_IRQChannelPreemptionPriority = 0;
nvic_initstructure.NVIC_IRQChannelSubPriority = 6;
NVIC_Init(&nvic_initstructure);
接口
- 初始化函数:位于usb_init.c文件中,在使用USB功能前必须被调用,以初始化USB的硬件资源(移植后,USB_Init函数增加了初始化IO口、NVIC、CLK函数)。
- USB枚举过程:枚举过程中需要用到的各种描述符(设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符、其他描述符),在usb_desc.c文件中。
- USB中断处理:位于usb_istr.c文件中的USB_Istr函数负责处理USB底层中断,必须在中断服务程序中被调用。
- 回调函数:在usb_prop.c文件中配置(注意要使用端点回调函数,必须在usb_conf.h中配置)。
- 数据传输:usb_sil.c文件中包含端点的读、写函数,并启动数据传输。
bxCAN模块
基于Std3.5 lib的bxCan模块操作
开启时钟
一个老生常谈的话题,首先是开启时钟、配置对应的IO口。
bxCAN挂载与APB1上,使用的IO资源PA11->CAN_Rx、PA12->CAN_Tx,IO的工作方式在参考手册中有定义,截图如下:

示例代码:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1,ENABLE); //开启CAN1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE); //开启IO时钟
...
/*CAN_Rx*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_InitStructure);
/*CAN_TX*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA,&GPIO_InitStructure);
示例代码中,假定CAN总线的波特率是1M,IO口的速度要适当高一些(设定为10M)。
配置中断优先级(可选)
如工作于中断模式,则需要配置中断优先级。STM32F103VE单片机的的CAN中断服务程序在启动文件中定义USB_LP_CAN1_RX0_IRQHandler、USB_HP_CAN1_RX0_IRQHandler两个,二者没太大区别,中断通道同样定义了USB_LP_CAN1_RX0_IRQn、USB_HP_CAN1_RX0_IRQn两个分别对应不同的中断服务程序。示例代码如下:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);
NIVI_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;
NIVI_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NIVI_InitStructure.NVIC_IRQChannelSubPriority = 10;
NIVI_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NIVI_InitStructure);
配置CAN模块
使用std3.5 lib库,涉及到3部分,CAN工作模式、过滤器、接受/发送数据结构体。
- CAN工作模式
波特率的计算
CAN模块的时钟来自APB1总线桥,计算公式如下图所示:

默认启动,APB2的时钟为36MHz,要得到1MHz的波特率,可选BRT=5,BS1=2,BS2=1。
CAN_InitTypeDef结构体用于配置CAN的工作模式,示例代码如下:
CAN_InitTypeDef CAN_InitStructure;
/*开始配置CAN1**/
CAN_InitStructure.CAN_ABOM = DISABLE; //关闭Automatic bus-off
CAN_InitStructure.CAN_AWUM = DISABLE; //关闭Automatic wake-up
CAN_InitStructure.CAN_NART = DISABLE; //关闭non-automatic rt
CAN_InitStructure.CAN_RFLM = DISABLE; //关闭FIFO Locked
CAN_InitStructure.CAN_TTCM = DISABLE; //关闭time trigger
CAN_InitStructure.CAN_TXFP = ENABLE; //使能请求优先
CAN_InitStructure.CAN_Mode = CAN_Mode_Silent_LoopBack;//静默测试模式
//波特率1Mhz
CAN_InitStructure.CAN_BS1 = CAN_BS1_3tq;
CAN_InitStructure.CAN_BS2 = CAN_BS2_2tq;
CAN_InitStructure.CAN_SJW = CAN_SJW_1tq;
CAN_InitStructure.CAN_Prescaler = 6;
//can 初始化
CAN_Init(CAN1,&CAN_InitStructure);
- 配置过滤器
过滤器用于过滤总线上的广播数据,工作模式分为mask和list两个。
mask模式下,FiR1用作ID,FiR2用作Mask。Mask位为1的位置,必须与ID为完全一样,Mask位为0的位置,则忽略。
list模式下,FiR1、FiR2共同形成一个列表,通过过滤器的消息ID必须和FiR1或FiR2中的任一个一样。
示例代码中,激活F0Rx绑定FIFO0,Mask模式(配置为接受所有消息),如下:
CAN_FilterInitTypeDef CAN_FilterInitStructure;
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInitStructure.CAN_FilterIdHigh = 0;
CAN_FilterInitStructure.CAN_FilterIdLow = 0;
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0;
CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0;
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStructure.CAN_FilterNumber = 0;
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInit(&CAN_FilterInitStructure);
- 接受/发送数据结构体
- 接受消息结构体
CanRxMsg用于管理接受到的消息,结构体原型如下:
typedef struct
{
uint32_t StdId; //标准消息ID
uint32_t ExtId; //扩展消息ID
uint8_t IDE; //接到的消息类型:标准消息、扩展消息
uint8_t RTR; //远程控制指令标识
uint8_t DLC; //消息长度
uint8_t Data[8]; //消息本体
uint8_t FMI; //接到消息的过滤器号
} CanRxMsg;
- 发送消息结构体
CanTxMsg用于管理待发送的消息,结构体原型如下:
typedef struct
{
uint32_t StdId; //标准消息ID
uint32_t ExtId; //扩展消息ID
uint8_t IDE; //接到的消息类型:标准消息、扩展消息
uint8_t RTR; //远程控制指令标识
uint8_t DLC; //消息长度
uint8_t Data[8]; //消息本体
} CanTxMsg;
因消息结构体较简单,不再设置示例代码。
- 配置中断
- 开启中断
开启对应的中断,例程中仅开启FIFO0正确接受消息中断,示例代码如下:
CAN_ITConfig(CAN1,CAN_IT_FMP0,ENABLE);
- 中断服务程序
中断服务程序放在stm32f10x_it.c中,入口函数为USB_LP_CAN1_RX0_IRQHandler,示例代码如下:
void USB_LP_CAN1_RX0_IRQHandler(void)
{
if(CAN_GetITStatus(CAN1,CAN_IT_FMP0)==SET)
{
CAN_Receive(CAN1,CAN_FIFO0,&RxMsg); //读取消息
OSSemPost(sem); //通知系统有新到消息
}
}
bxCAN模块功能概述
工作原理
CAN总线上传输的消息,不是以目标地址区分,而是以消息ID作为区分。可以说,CAN总线网络内,消息在整个网络广播,各节点根据消息的ID自行判断是否需要接受。
消息ID有标准ID和扩展ID两种,以IED位区分:
- 标准ID:由11位组成,可对应16位过滤器
- 扩展ID:由29位组成,需对应32位的过滤器
bxCAN模块支持标准ID和扩展ID。
bxCAN模块的发送模块包括3个MailBOX,自动完成数据的发送及校检等。
bxCAN模块的接受模块包括过滤器和2个FIFO,过滤器决定是否接受总线的消息,并将接受到的消息绑定到制定的FIFO中。互联型有28个过滤器,普通型有14个过滤器,每个过滤器可单独绑定到FIFO中。
工作模式
bxCAN主要模式有复位模式、正常模式、休眠模式。
- 复位模式:模块复位后默认进入该模式,也可通过置位INRQ进入该模式。复位模式用于配置模块工作方式。注:进入复位模式不改变配置寄存器的状态,且配置模式下模块亦然接受总线数据。
- 正常模式:正常工作模式。
- 休眠模式:置位SLEEP,可进入该模式降低功耗。
测试模式
测试模式有3种,通过SILM和LBKM配置。
过滤器
过滤器的功能是根据总线上的信息ID判断是否接受信息。
bxCAN模块的每个过滤器包括2个32位的过滤器寄存器,可配置为MASK模式或List模式,并可配置为4个独立的16位List或2组16位的Mask,如下图所示:

过滤器优先级
当某条消息可通过多条过滤器时,遵循如下原则:
- List优先:List过滤器的优先级高于Mask过滤器;
CAN总线协议
关于CAN总线协议,请阅读E:\STM32F10X\CAN总线协议中文版_周立功.pdf。
SDIO模块
小技巧
- 编辑器内置的几个宏
C / C++编译器会内置几个宏,这些宏定义可以帮助我们完成跨平台的源码编写,也可以输出有用的调试信息。
ANSI C标准中有几个标准预定义宏(也是常用的):
- __DATE__:在源文件中插入当前的编译日期
- __TIME__:在源文件中插入当前编译时间;
- __FILE__:在源文件中插入当前源文件路径及文件名;
- __LINE__:在源代码中插入当前源代码行号;
- __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;