- 2024-10-19
-
发表了主题帖:
【STM32H7S78-DK】USB虚拟 U盘
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 虚拟U盘简介
USB(Universal Serial BUS)通用串行总线,是一个外部总线标准,用于规范电脑与外部设备的连接和通讯。是应用在 PC 领域的接口技术。USB 接口支持设备的即插即用和热插拔功能。USB 是在 1994 年底由英特尔、康柏、IBM、Microsoft 等多家公司联合提出的。
USB 发展到现在已经有 USB1.0/1.1/2.0/3.0 等多个版本。目前用的最多的就是 USB1.1 和 USB2.0,USB3.0 目前已经开始普及。
标准 USB 共四根线组成,除 VCC/GND 外,另外为 D+,D-; 这两根数据线采用的是差分电压的方式进行数据传输的。在 USB 主机上,D-和 D+都是接了 15K 的电阻到低的,所以在没有设备接入的时候,D+、D-均是低电平。而在 USB 设备中,如果是高速设备,则会在 D+上接一个 1.5K 的电阻到 VCC,而如果是低速设备,则会在 D-上接一个 1.5K 的电阻到 VCC。这样当设备接入主机的时候,主机就可以判断是否有设备接入,并能判断设备是高速设备还是低速设备。
USB大容量存储设备类(The USB mass storage device class)是一种计算机和移动设备之间的传输协议,它允许一个通用串行总线(USB)设备来访问主机的计算设备,使两者之间进行文件传输。通过这个标准的计算机连接到的设备包括:移动硬盘、移动光驱、U盘、SD、TF等储存卡读卡器、数码相机、各种数字音频播放器和便携式媒体播放器、智能卡阅读器、掌上电脑和手机。
MSC的通用性和操作简单使他成为移动设备上最常见的文件系统,USB MSC并不需要任何特定的文件系统, 相反,它提供了一个简单的界面来读写接口用于访问任何硬盘驱动器。操作系统可以把MSC像本地硬盘一样格式化,并可以与他们喜欢的任何文件系统格式它,当然也可以创建多个分区。
2 USB硬件电路
USB的硬件电路如下:
Figure 2-1 USB的硬件电路
USB接口PM11和PM12。
3 利用SD Card实现U盘功能
SD Card实现U盘功能,利用文件系统对SD Card进行读写操作,同时PC端也可对U盘内文件进行操作。其外设SD Card的硬件电路如下。
Figure 6-1 SD卡硬件电路
STM32 控制器可以控制使用单线或 4 线传输,本开发板设计使用 4 线传输。
3.1 利用SD Card实现U盘
3.1.1 STM32CubeMX配置工程
STM32H7S的USB还有个MDC能够实现MSD(Mass Stroage Device)功能,也就是我们常说的U盘。于是就直接用STM32CubeMX配置工程,添加相应代码,实现U盘功能。
首先打开USB使能。
USB有主机(Host)和设备(Device)之分。一般电脑的USB接口为主机接口,而键盘、鼠标、U盘等则为设备。
STM32H7S系列的有2个USB接口,既可以作为HOST,又可以作为Device,还可以作为OTG接口。
然后选择中间件USB_DEVICE,如下图所示,USB IP选择Mass Storage Class,下方的配置参数只改一个MSC_MEDIA_PACKET,默认是512。这项参数要大于或等于存储器的块(页)大小,否则将出现请插入U盘或者无法读写U盘的情况。
MSC_MEDIA_PACKET (Media I/O buffer Size)(读写缓冲区大小): 512(默认为512,这个的大小对于USB读写速度会有一些影响,最好和存储介质的最小存储单元一致)
本实验板使用的SD卡(三种卡的统称)的存储空间是由一个一个扇区组成的,SD卡的扇区大小是固定的,为512byte(这一点很重要) ,若干个扇区又可以组成一个分配单元(也被成为簇),分配单元常见的大小为4K、8K、16K、32K、64K。
还可以配置设备信息。
配置USB的时钟为48MHz。
在 Connetivity 中选择 SDIO 设置,并选择 SD 4 bits Wide bus 四线SD模式。
在 Parameter Settings 进行具体参数配置。
Clock transition on which the bit capture is made: Rising transition。主时钟 SDIOCLK 产生 CLK 引脚时钟有效沿选择,可选上升沿或下降沿,它设定 SDIO 时钟控制寄存器(SDIO_CLKCR)的 NEGEDGE 位的值,一般选择设置为上升沿。
SDIO Clock divider bypass: Disable。时钟分频旁路使用,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 BYPASS 位。如果使能旁路,SDIOCLK 直接驱动 CLK 线输出时钟;如果禁用,使用 SDIO_CLKCR 寄存器的 CLKDIV 位值分频 SDIOCLK,然后输出到 CLK 线。一般选择禁用时钟分频旁路。
SDIO Clock output enable when the bus is idle: Disable the power save for the clock。节能模式选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 PWRSAV 位的值。如果使能节能模式,CLK 线只有在总线激活时才有时钟输出;如果禁用节能模式,始终使能 CLK 线输出时钟。
SDIO hardware flow control: The hardware control flow is disabled。硬件流控制选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 HWFC_EN 位的值。硬件流控制功能可以避免 FIFO 发送上溢和下溢错误。
SDIOCLK clock divide factor: 6。时钟分频系数,它设定 SDIO_CLKCR 寄存器的 CLKDIV 位的值,设置 SDIOCLK 与 CLK 线输出时钟分频系数:CLK 线时钟频率=SDIOCLK/([CLKDIV+2])。
SDIO_CK 引脚的时钟信号在卡识别模式时要求不超过 400KHz,而在识别后的数据传输模式时则希望有更高的速度(最大不超过 25MHz),所以会针对这两种模式配置 SDIOCLK 的时钟。
这里参数描述建议将SDIOCLK clock divede factor 参数使用默认值为0,SDIOCLK为72MHz,可以得到最大频率36MHz,但请注意,有些型号的SD卡可能不支持36MHz这么高的频率,所以还是要以实际情况而定。
配置NVIC
当在Middleware and SoftwarePacks中配置了USB_DEVICE的模式不为Disable时,便会自动开启USB_OTG的全局中断,且不可关闭,用户配置合适的中断优先级即可,具体配置如下图所示
3.1.2 代码实现
修改存储读写函数。
/* Includes ------------------------------------------------------------------*/
#include "usbd_storage_if.h"
/* USER CODE BEGIN INCLUDE */
/* USER CODE END INCLUDE */
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
/* USER CODE END PV */
/** @addtogroup STM32_USB_OTG_DEVICE_LIBRARY
* [url=home.php?mod=space&uid=159083]@brief[/url] Usb device.
* @{
*/
/** @defgroup USBD_STORAGE
* @brief Usb mass storage device module
* @{
*/
/** @defgroup USBD_STORAGE_Private_TypesDefinitions
* @brief Private types.
* @{
*/
/* USER CODE BEGIN PRIVATE_TYPES */
/* USER CODE END PRIVATE_TYPES */
/**
* @}
*/
/** @defgroup USBD_STORAGE_Private_Defines
* @brief Private defines.
* @{
*/
#define STORAGE_LUN_NBR 1
#define STORAGE_BLK_NBR 0x10000
#define STORAGE_BLK_SIZ 0x200
/* USER CODE BEGIN PRIVATE_DEFINES */
/* USER CODE END PRIVATE_DEFINES */
/**
* @}
*/
/** @defgroup USBD_STORAGE_Private_Macros
* @brief Private macros.
* @{
*/
/* USER CODE BEGIN PRIVATE_MACRO */
/* USER CODE END PRIVATE_MACRO */
/**
* @}
*/
/** @defgroup USBD_STORAGE_Private_Variables
* @brief Private variables.
* @{
*/
/* USER CODE BEGIN INQUIRY_DATA_FS */
/** USB Mass storage Standard Inquiry Data. */
const int8_t STORAGE_Inquirydata_FS[] = {/* 36 */
/* LUN 0 */
0x00,
0x80,
0x02,
0x02,
(STANDARD_INQUIRY_DATA_LEN - 5),
0x00,
0x00,
0x00,
'S', 'T', 'M', ' ', ' ', ' ', ' ', ' ', /* Manufacturer : 8 bytes */
'P', 'r', 'o', 'd', 'u', 'c', 't', ' ', /* Product : 16 Bytes */
' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'0', '.', '0' ,'1' /* Version : 4 Bytes */
};
/* USER CODE END INQUIRY_DATA_FS */
/* USER CODE BEGIN PRIVATE_VARIABLES */
/* USER CODE END PRIVATE_VARIABLES */
/**
* @}
*/
/** @defgroup USBD_STORAGE_Exported_Variables
* @brief Public variables.
* @{
*/
extern USBD_HandleTypeDef hUsbDeviceFS;
/* USER CODE BEGIN EXPORTED_VARIABLES */
extern SD_HandleTypeDef hsd1;
/* USER CODE END EXPORTED_VARIABLES */
/**
* @}
*/
/** @defgroup USBD_STORAGE_Private_FunctionPrototypes
* @brief Private functions declaration.
* @{
*/
static int8_t STORAGE_Init_FS(uint8_t lun);
static int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size);
static int8_t STORAGE_IsReady_FS(uint8_t lun);
static int8_t STORAGE_IsWriteProtected_FS(uint8_t lun);
static int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
static int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len);
static int8_t STORAGE_GetMaxLun_FS(void);
/* USER CODE BEGIN PRIVATE_FUNCTIONS_DECLARATION */
/* USER CODE END PRIVATE_FUNCTIONS_DECLARATION */
/**
* @}
*/
USBD_StorageTypeDef USBD_Storage_Interface_fops_FS =
{
STORAGE_Init_FS,
STORAGE_GetCapacity_FS,
STORAGE_IsReady_FS,
STORAGE_IsWriteProtected_FS,
STORAGE_Read_FS,
STORAGE_Write_FS,
STORAGE_GetMaxLun_FS,
(int8_t *)STORAGE_Inquirydata_FS
};
/* Private functions ---------------------------------------------------------*/
/**
* @brief Initializes the storage unit (medium) over USB FS IP
* @param lun: Logical unit number.
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
int8_t STORAGE_Init_FS(uint8_t lun)
{
/* USER CODE BEGIN 2 */
UNUSED(lun);
return (USBD_OK);
/* USER CODE END 2 */
}
/**
* @brief Returns the medium capacity.
* @param lun: Logical unit number.
* @param block_num: Number of total block number.
* @param block_size: Block size.
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
/* USER CODE BEGIN 3 */
HAL_SD_CardInfoTypeDef info;
if(HAL_SD_GetCardState(&hsd1) == HAL_SD_CARD_TRANSFER)
{
HAL_SD_GetCardInfo(&hsd1, &info);
*block_num = info.LogBlockNbr;
*block_size = info.LogBlockSize;
return USBD_OK;
}
return USBD_FAIL;
/* USER CODE END 3 */
}
/**
* @brief Checks whether the medium is ready.
* @param lun: Logical unit number.
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
int8_t STORAGE_IsReady_FS(uint8_t lun)
{
/* USER CODE BEGIN 4 */
UNUSED(lun);
return (USBD_OK);
/* USER CODE END 4 */
}
/**
* @brief Checks whether the medium is write protected.
* @param lun: Logical unit number.
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
int8_t STORAGE_IsWriteProtected_FS(uint8_t lun)
{
/* USER CODE BEGIN 5 */
UNUSED(lun);
return (USBD_OK);
/* USER CODE END 5 */
}
/**
* @brief Reads data from the medium.
* @param lun: Logical unit number.
* @param buf: data buffer.
* @param blk_addr: Logical block address.
* @param blk_len: Blocks number.
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
/* USER CODE BEGIN 6 */
int8_t ret = USBD_FAIL;
if(HAL_SD_ReadBlocks(&hsd1, buf, blk_addr, blk_len, HAL_MAX_DELAY) == HAL_OK)
{
ret = USBD_OK;
while(HAL_SD_GetState(&hsd1) == HAL_SD_STATE_BUSY);
while(HAL_SD_GetCardState(&hsd1) != HAL_SD_CARD_TRANSFER);
}
return ret;
/* USER CODE END 6 */
}
/**
* @brief Writes data into the medium.
* @param lun: Logical unit number.
* @param buf: data buffer.
* @param blk_addr: Logical block address.
* @param blk_len: Blocks number.
* @retval USBD_OK if all operations are OK else USBD_FAIL
*/
int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
/* USER CODE BEGIN 7 */
int8_t ret = USBD_FAIL;
if(HAL_SD_WriteBlocks(&hsd1, buf, blk_addr, blk_len, HAL_MAX_DELAY) == HAL_OK)
{
ret = USBD_OK;
while(HAL_SD_GetState(&hsd1) == HAL_SD_STATE_BUSY);
while(HAL_SD_GetCardState(&hsd1) != HAL_SD_CARD_TRANSFER);
}
return ret;
/* USER CODE END 7 */
}
/**
* @brief Returns the Max Supported LUNs.
* @param None
* @retval Lun(s) number.
*/
int8_t STORAGE_GetMaxLun_FS(void)
{
/* USER CODE BEGIN 8 */
return (STORAGE_LUN_NBR - 1);
/* USER CODE END 8 */
}
/* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */
/* USER CODE END PRIVATE_FUNCTIONS_IMPLEMENTATION */
/**
* @}
*/
/**
* @}
*/
3.2 实验现象
编译代码,将程序下载至开发板,并通过USB连接至电脑,格式化后,如下图所示显示容量为2.58G的U盘,当然U盘的大小硬件和设定的大小决定的。
这样就可往U盘中存放数据。
-
发表了主题帖:
【STM32H7S78-DK】USB虚拟串口
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 USB 虚拟串口简介
USB 虚拟串口,简称 VCP,是 Virtual COM Port 的简写,它是利用 USB 的 CDC 类来实现的一种通信接口。
我们可以利用 STM32 自带的 USB 功能,来实现一个 USB 虚拟串口,从而通过 USB,实现电脑与 STM32 的数据互传。上位机无需编写专门的 USB 程序,只需要一个串口调试助手即可调试,非常实用。
2 USB硬件电路
USB的硬件电路如下:
Figure 2-1 USB的硬件电路
USB接口PM11和PM12。
3 USB实现虚拟串口(VPC)
3.1 STM32CubeMX配置工程
首先打开USB使能。配置USB设备,选择USB Device功能,速度为默认全速USB设备12MHZ。
使能USB_DEVICE库,选择Virtual Port Com (虚拟串口,VPC),使用默认配置。
配置USB FS的时钟为48MHz。
然后生成工程。
3.2 虚拟串口软件实现
数据收发的接口在usbd_cdc_if.c文件下。
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] Data received over USB OUT endpoint are sent over CDC interface
* through this function.
*
* @note
* This function will issue a NAK packet on any OUT packet received on
* USB endpoint until exiting this function. If you exit this function
* before transfer is complete on CDC interface (ie. using DMA controller)
* it will result in receiving more data while previous ones are still
* not sent.
*
* @param Buf: Buffer of data to be received
* @param Len: Number of data received (in bytes)
* @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL
*/
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
/* USER CODE BEGIN 6 */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);
/* USER CODE END 6 */
}
/**
* @brief CDC_Transmit_FS
* Data to send over USB IN endpoint are sent over CDC interface
* through this function.
* @note
*
*
* @param Buf: Buffer of data to be sent
* @param Len: Number of data to be sent (in bytes)
* @retval USBD_OK if all operations are OK else USBD_FAIL or USBD_BUSY
*/
uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len)
{
uint8_t result = USBD_OK;
/* USER CODE BEGIN 7 */
USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData;
if (hcdc->TxState != 0){
return USBD_BUSY;
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, Buf, Len);
result = USBD_CDC_TransmitPacket(&hUsbDeviceFS);
/* USER CODE END 7 */
return result;
}
打印输出
当然也可以把USB作为类似于uart的printf来输出当前需要的log信息。我们只需要把这个函数改就下可以使用USB,VCP来打印输出信息。
int fputc(int ch, FILE *f)
{
while(CDC_Transmit_FS((uint8_t *)&ch, 1) == USBD_BUSY);
return ch;
}
主函数代码如下:
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_UART4_Init();
MX_USB_DEVICE_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
printf("USB CDC, %s \r\n", UserRxBufferFS);
memset(UserRxBufferFS, 0, APP_RX_DATA_SIZE);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
3.3 实验现象
将USB插入电脑,在电脑设备管理器中将显示新的串口设备。如果没有安装VPC驱动需要先安装。
查看USB的硬件信息。
上面的VID和PID可以在文件usbd_desc.c中找到。
使用串口助手接上设备,即可打印如下信息。
发送数据后,虚拟串口则将接收的数据打印处理。
- 2024-10-14
-
发表了主题帖:
【STM32H7S78-DK】独立看门狗
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 独立看门狗工作原理
独立看门狗用通俗一点的话来解释就是一个 12 位的递减计数器,当计数器的值从某个值一直减到 0 的时候,系统就会产生一个复位信号,即 IWDG_RESET。如果在计数没减到 0 之前,刷新了计数器的值的话,那么就不会产生复位信号,这个动作就是我们经常说的喂狗。看门狗功能由 VDD 电压域供电,在停止模式和待机模式下仍能工作。
独立看门狗由内部专门的 32Khz 低速时钟驱动,即使主时钟发生故障,它也仍然有效。这里需要注意独立看门狗的时钟是一个内部 RC 时钟,所以并不是准确的32Khz,而是在 30~60Khz 之间的一个可变化的时钟,只是我们在估算的时候,以32Khz 的频率来计算,看门狗对时间的要求不是很精确,所以,时钟有些偏差,都是可以接受的。
Figure 1-1 独立看门狗框图
独立看门狗的架构是很简单的,本质就是一个递减计数器,和Systick有些类似,只是运行在低速时钟下,另外还有寄存器访问保护功能。
【注】看门狗功能处于VDD供电区,即在停机和待机模式时仍能正常工作。
IWDG最适合应用于那些需要看门狗作为一个在主程序之外,能够完全独立工作,并且对时间精度要求较低的场合。 WWDG最适合那些要求看门狗在精确计时窗口起作用的应用程序。
Table 1-1 看门狗超时时间(32kHz的输入时钟(LSI))
【注】这些时间是按照32kHz时钟给出。实际上,MCU内部的RC频率会在30kHz到60kHz之间变化。此外,即使RC振荡器的频率是精确的,确切的时序仍然依赖于APB接口时钟与RC振荡器时钟之间的相位差,因此总会有一个完整的RC周期是不确定的。通过对LSI进行校准可获得相对精确的看门狗超时时间。
2 独立看门狗的寄存器描述
首先是键值寄存器 IWDG_KR,该寄存器的各位描述如下图所示。
Figure 1-2 键寄存器(IWDG_KR)
在键值寄存器(IWDG_KR)中写入 0xCCCC,开始启用独立看门狗;此时计数器开始从其复位值 0xFFF 递减计数。当计数器计数到末尾 0x000 时,会产生一个复位信号(IWDG_RESET)。无论何时,只要键寄存器 IWDG_KR 中被写入 0xAAAA,IWDG_RLR 中的值就会被重新加载到计数器中从而避免产生看门狗复位。
IWDG_PR 和 IWDG_RLR 寄存器具有写保护功能。要修改这两个寄存器的值,必须先向IWDG_KR 寄存器中写入0x5555。将其他值写入这个寄存器将会打乱操作顺序,寄存器将重新被保护。重装载操作(即写入0xAAAA)也会启动写保护功能。预分频寄存器(IWDG_PR)用来设置看门狗时钟的分频系数。重装载寄存器用来保存重装载到计数器中的值,该寄存器也是一个 32位寄存器,但是只有低 12 位是有效的。
Figure 1-3 预分频寄存器(IWDG_PR)
Figure 1-4 重装载寄存器(IWDG_RLR)
3 独立看门狗的具体代码实现
3.1 STM32Cube生成工程
我们在串口的例子的基础上进行配置。IWDG时钟预分频系数64分频,计数器重装载值 800。
Figure 1-5 使能独立看门狗
IWDG counter clock prescaler:看门狗分频系数
IWDG window value:独立狗窗口阈值
IWDG down-counter reload value:独立狗复位阈值
IWDG Early Wakeup Interrupt:独立看门狗溢出的中断阈值,要比IWDG down-counter reload value小
即可得到相应超出(溢出)时间:
Tout=((4×2^PRER) ×RLR)/LSI时钟频率=800*64/32Khz=1.6s
如果配置了独立看门狗中断,可通过中断溢出喂狗。
独立看门狗的时钟由专用的低速时钟(LSI)驱动(40kHz),即使主时钟发生故障它仍有效。独立看门狗适合应用于需要看门狗作为一个在主程序之外 能够完全独立工作,并且对时间精度要求低的场合。默认配置即可。
Figure 1-6 配置时钟
生成工程即可。
3.2 独立看门狗的具体代码分析
独立看门狗一般用来检测和解决由程序引起的故障,比如一个程序正常运行的时间是50ms,在运行完这段程序之后紧接着进行喂狗,我们设置独立看门狗的定时溢出时间为60ms,比我们需要监控的程序 50ms 多一点,如果超过 60ms 还没有喂狗,那就说明我们监控的程序出故障了,跑飞了,那么就会产生系统复位,让程序重新运行。
这个程序很简单,只需要及时喂狗即可。
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_UART4_Init();
MX_IWDG_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
printf("独立看门狗\r\n");
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_IWDG_Refresh(&hiwdg); //喂狗
printf("喂狗\r\n");
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 1000 ms */
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
主函数很简单,初始化独立看门狗后,在主循环里不断喂狗即可。HAL_IWDG_Refresh()函数就是喂狗操作,值得注意的是,本程序溢出时间是1.6s,我们喂狗时间要小于1.6s,毕竟LSI精度不高。
3.3 独立看门狗实现现象
编译无误,打开串口,现象如下:
当注释掉喂狗语句后,板子就会不断重启,因为没有喂狗就导致板子不断复位。
-
回复了主题帖:
【STM32H7S78-DK】内部温度传感器读取
cc1989summer 发表于 2024-10-13 20:05
55度也太高了吧。
高性能的处理器也还好,和低功耗的没法比
- 2024-10-13
-
发表了主题帖:
【STM32H7S78-DK】CRC 校验
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 CRC的校验原理
循环冗余校验(CRC)计算单元是根据固定的生成多项式得到任一32位全字的CRC计算结果。在其他的应用中, CRC技术主要应用于核实数据传输或者数据存储的正确性和完整性。标准EN/IEC 60335-1即提供了一种核实闪存存储器完整性的方法。 CRC计算单元可以在程序运行时计算出软件的标识,之后与在连接时生成的参考标识比较,然后存放在指定的存储器空间。那么首先来看看CRC校验原理。
1.1 基本原理
CRC检验原理实际上就是在一个p位二进制数据序列之后附加一个r位二进制检验码(序列),从而构成一个总长为n=p+r位的二进制序列;附加在数据序列之后的这个检验码与数据序列的内容之间存在着某种特定的关系。如果因干扰等原因使数据序列中的某一位或某些位发生错误,这种特定关系就会被破坏。因此,通过检查这一关系,就可以实现对数据正确性的检验。
几个基本概念
1、帧检验序列FCS(Frame Check Sequence):为了进行差错检验而添加的冗余码。
2、多项式模2运行:实际上是按位异或(Exclusive OR)运算,即相同为0,相异为1,也就是不考虑进位、借位的二进制加减运算。如:10011011 + 11001010 = 01010001。
3、生成多项式(generator polynomial):当进行CRC检验时,发送方与接收方需要事先约定一个除数,即生成多项式,一般记作G(x)。生成多项式的最高位与最低位必须是1。常用的CRC码的生成多项式有:
每一个生成多项式都可以与一个代码相对应,如CRC8对应代码:100110001。
1.2 CRC检验码的计算
设信息字段为K位,校验字段为R位,则码字长度为N(N=K+R)。设双方事先约定了一个R次多项式g(x),则CRC码:
V(x)=A(x)g(x)=xRm(x)+r(x)
其中: m(x)为K次信息多项式, r(x)为R-1次校验多项式。
这里r(x)对应的代码即为冗余码,加在原信息字段后即形成CRC码。
r(x)的计算方法为:在K位信息字段的后面添加R个0,再除以g(x)对应的代码序列,得到的余数即为r(x)对应的代码(应为R-1位;若不足,而在高位补0)。
计算示例:
设需要发送的信息为M = 1010001101,产生多项式对应的代码为P = 110101,R=5。在M后加5个0,然后对P做模2除法运算,得余数r(x)对应的代码:01110。故实际需要发送的数据是101000110101110。
1.3 错误检测
当接收方收到数据后,用收到的数据对P(事先约定的)进行模2除法,若余数为0,则认为数据传输无差错;若余数不为0,则认为数据传输出现了错误,由于不知道错误发生在什么地方,因而不能进行自动纠正,一般的做法是丢弃接收的数据。
【注】几点说明:
1、CRC是一种常用的检错码,并不能用于自动纠错。
2、只要经过严格的挑选,并使用位数足够多的除数 P,那么出现检测不到的差错的概率就很小很小。
3、仅用循环冗余检验 CRC 差错检测技术只能做到无差错接受(只是非常近似的认为是无差错的),并不能保证可靠传输。
2 STM32中的CRC
所有的STM32芯片都内置了一个硬件的CRC计算模块,可以很方便地应用到需要进行通信的程序中。
Figure 2-1 CRC结构
这个CRC计算模块使用常见的、写成16进制就是:0x04C11DB7
使用这个内置CRC模块的方法非常简单,既首先复位CRC模块(设置CRC_CR=0x01),这个操作把CRC计算的余数初始化为0xFFFFFFFF;然后把要计算的数据按每32位分割为一组数据字,并逐个地把这组数据字写入CRC_DR寄存器(既下图中的绿色框),写完所有的数据字后,就可以从CRC_DR寄存器(既下图中的蓝色框)读出计算的结果。
Figure 2-2 CRC 计算单元框图
下面是用C语言描述的这个计算模块的算法,大家可以把它放在通信的另一端,对通信的正确性进行验证:
DWORD dwPolynomial = 0x04c11db7;
DWORD cal_crc(DWORD *ptr, int len)
{
DWORD xbit;
DWORD data;
DWORD CRC = 0xFFFFFFFF; // init
while (len--) {
xbit = 1 << 31;
data = *ptr++;
for (int bits = 0; bits < 32; bits++) {
if (CRC & 0x80000000) {
CRC <<= 1;
CRC ^= dwPolynomial;
}
else
CRC <<= 1;
if (data & xbit)
CRC ^= dwPolynomial;
xbit >>= 1;
}
}
return CRC;
}
有几点需要说明:
1)上述算法中变量CRC,在每次循环结束包含了计算的余数,它始终是向左移位(既从最低位向最高位移动),溢出的数据位被丢弃。
2)输入的数据始终是以32位为单位,如果原始数据少于32位,需要在低位补0,当然也可以高位补0。
3)假定输入的DWORD数组中每个分量是按小端存储。
4)输入数据是按照最高位最先计算,最低位最后计算的顺序进行。
例如:
如果输入0x44434241,内存中按字节存放的顺序是:0x41,0x42,0x43,0x44。计算的结果是:0xCF534AE1
如果输入0x41424344,内存中按字节存放的顺序是:0x44,0x43,0x42,0x41。计算的结果是:0xABCF9A63
3 CRC寄存器描述
数据寄存器(CRC_DR)
Figure 3-1 数据寄存器(CRC_DR)
独立数据寄存器(CRC_IDR)
Figure 3-2 独立数据寄存器(CRC_IDR)
注:此寄存器不参与CRC计算,可以存放任何数据。
控制寄存器(CRC_CR)
Figure 3-3 控制寄存器(CRC_CR)
4 CRC具体代码实现
4.1 STM32Cube生成工程
我们在串口的例子的基础上进行配置。CRC配置很简单,激活CRC模块即可。
Figure 4-1 激活CRC 计算模块
输入数据的格式配置为Words。
4.2 CRC具体代码实现
代码很简单。
static const uint32_t DataBuffer[BUFFER_SIZE] =
{
0x00001021, 0x20423063, 0x408450a5, 0x60c670e7
};
__IO uint32_t CRCValue = 0;
主函数代码如下所示。
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_UART4_Init();
MX_CRC_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
printf("CRC(循环冗余校验)实验 \n");
/* 进行冗余循环校验,获取校验码*/
//CRCValue = HAL_CRC_Accumulate(&hcrc, (uint32_t *)DataBuffer, BUFFER_SIZE);
CRCValue = HAL_CRC_Calculate(&hcrc, (uint32_t *)DataBuffer, BUFFER_SIZE);
printf("\r\n32-bit CRC 校验码为:0x%X\n", CRCValue);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
值得注意的是ST官方提供的CRC校验有两个函数,函数原型如下:
/**
* @brief Compute the 7, 8, 16 or 32-bit CRC value of an 8, 16 or 32-bit data buffer
* starting with the previously computed CRC as initialization value.
* @param hcrc CRC handle
* @param pBuffer pointer to the input data buffer, exact input data format is
* provided by hcrc->InputDataFormat.
* @param BufferLength input data buffer length (number of bytes if pBuffer
* type is * uint8_t, number of half-words if pBuffer type is * uint16_t,
* number of words if pBuffer type is * uint32_t).
* @note By default, the API expects a uint32_t pointer as input buffer parameter.
* Input buffer pointers with other types simply need to be cast in uint32_t
* and the API will internally adjust its input data processing based on the
* handle field hcrc->InputDataFormat.
* @retval uint32_t CRC (returned value LSBs for CRC shorter than 32 bits)
*/
uint32_t HAL_CRC_Accumulate(CRC_HandleTypeDef *hcrc, uint32_t pBuffer[], uint32_t BufferLength)
{
uint32_t index; /* CRC input data buffer index */
uint32_t temp = 0U; /* CRC output (read from hcrc->Instance->DR register) */
/* Change CRC peripheral state */
hcrc->State = HAL_CRC_STATE_BUSY;
switch (hcrc->InputDataFormat)
{
case CRC_INPUTDATA_FORMAT_WORDS:
/* Enter Data to the CRC calculator */
for (index = 0U; index < BufferLength; index++)
{
hcrc->Instance->DR = pBuffer[index];
}
temp = hcrc->Instance->DR;
break;
case CRC_INPUTDATA_FORMAT_BYTES:
temp = CRC_Handle_8(hcrc, (uint8_t *)pBuffer, BufferLength);
break;
case CRC_INPUTDATA_FORMAT_HALFWORDS:
temp = CRC_Handle_16(hcrc, (uint16_t *)(void *)pBuffer, BufferLength); /* Derogation MisraC2012 R.11.5 */
break;
default:
break;
}
/* Change CRC peripheral state */
hcrc->State = HAL_CRC_STATE_READY;
/* Return the CRC computed value */
return temp;
}
/**
* @brief Compute the 7, 8, 16 or 32-bit CRC value of an 8, 16 or 32-bit data buffer
* starting with hcrc->Instance->INIT as initialization value.
* @param hcrc CRC handle
* @param pBuffer pointer to the input data buffer, exact input data format is
* provided by hcrc->InputDataFormat.
* @param BufferLength input data buffer length (number of bytes if pBuffer
* type is * uint8_t, number of half-words if pBuffer type is * uint16_t,
* number of words if pBuffer type is * uint32_t).
* @note By default, the API expects a uint32_t pointer as input buffer parameter.
* Input buffer pointers with other types simply need to be cast in uint32_t
* and the API will internally adjust its input data processing based on the
* handle field hcrc->InputDataFormat.
* @retval uint32_t CRC (returned value LSBs for CRC shorter than 32 bits)
*/
uint32_t HAL_CRC_Calculate(CRC_HandleTypeDef *hcrc, uint32_t pBuffer[], uint32_t BufferLength)
{
uint32_t index; /* CRC input data buffer index */
uint32_t temp = 0U; /* CRC output (read from hcrc->Instance->DR register) */
/* Change CRC peripheral state */
hcrc->State = HAL_CRC_STATE_BUSY;
/* Reset CRC Calculation Unit (hcrc->Instance->INIT is
* written in hcrc->Instance->DR) */
__HAL_CRC_DR_RESET(hcrc);
switch (hcrc->InputDataFormat)
{
case CRC_INPUTDATA_FORMAT_WORDS:
/* Enter 32-bit input data to the CRC calculator */
for (index = 0U; index < BufferLength; index++)
{
hcrc->Instance->DR = pBuffer[index];
}
temp = hcrc->Instance->DR;
break;
case CRC_INPUTDATA_FORMAT_BYTES:
/* Specific 8-bit input data handling */
temp = CRC_Handle_8(hcrc, (uint8_t *)pBuffer, BufferLength);
break;
case CRC_INPUTDATA_FORMAT_HALFWORDS:
/* Specific 16-bit input data handling */
temp = CRC_Handle_16(hcrc, (uint16_t *)(void *)pBuffer, BufferLength); /* Derogation MisraC2012 R.11.5 */
break;
default:
break;
}
/* Change CRC peripheral state */
hcrc->State = HAL_CRC_STATE_READY;
/* Return the CRC computed value */
return temp;
}
咋一看好像很没啥区别,其实还是有区别的,HAL_CRC_Calculate()函数在每次计算时,对DR寄存器进行了复位,而HAL_CRC_Accumulate()函数没有,因此在使用时就要根据需求来选择相应的函数了。
5 实验现象
将编译好的程序下载到板子中,通过串口助手可以看到如下现象。
Figure 5-1 实验现象
然后使用CRC计算工具来计算。
可以看到和软件计算的一致。
值得注意的是,STM32的硬件CRC的结果异或值是0x00000000。
【注】关于CRC的更多内容可以自行查阅相关资料,笔者这里推荐一篇文章`A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS`,感兴趣的朋友自己去看看吧。
-
发表了主题帖:
【STM32H7S78-DK】内部温度传感器读取
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 内部温度传感器工作原理
STM32H7S有一个内部温度传感器,可以用来测量 CPU 及周围的温度。该温度传感器在内部VSENSE输入通道相连接,此通道把传感器输出的电压转换成数字值。STM32H7S的内部温度传感器支持的温度范围为:-40~125度。
Figure 1-1 Temperature sensor架构
温度传感器的输出电压随温度线性变化。由于工艺不同,该线性函数的偏移量取决于各个芯片(芯片之间的温度变化可达 45°C)。
内部温度传感器更适用于对温度变量而非绝对温度进行测量的应用情况。如果需要读取精确温度,则应使用外部温度传感器。
STM32 内部温度传感器的使用很简单,只要设置一下内部 ADC,并激活其内部通道就差不多了。接下来我们介绍一下和温度传感器设置相关的 2 个地方。
第一个地方,我们要使用 STM32 的内部温度传感器,必须先激活 ADC 的内部通道,这里通过 ADC_CCR 的 TSEN位(bit23)设置。设置该位为 1 则启用内部温度传感器。
第二个地方, STM32 的内部温度传感器固定的连接在 ADC 的通道VSENSE上,所以,我们在设置好 ADC 之后只要读取VSENSE的值,就是温度传感器返回来的电压值了。根据这个值,我们就可以计算出当前温度。STM32内置一个温度传感器,通过VSENSE通道可以读出温度传感器的电压。其中给出了一个计算公式:
TS_CAL1 是温度传感器在 30℃时的校准值,固定保存在芯片内部的: 0x08FF F814 - 0x08FF F815这两个地址( 16 位)。
TS_CAL2 是温度传感器在 130℃时的校准值,固定保存在芯片内部的:0x08FF F818 - 0x08FF F819这两个地址( 16 位)。
TS_DATA: ADC1通道VSENSE读取到的当前温度传感器转换值。
Table 1-1 Temperature sensor校准值
现在,我们就可以总结一下 STM32 内部温度传感器使用的步骤了,如下:
1)设置 ADC,开启内部温度传感器。
关于如何设置 ADC,上一节已经介绍了,我们采用与上一节相似的设置。 不同的是上一节温度传感器是读取外部通道的值,而内部温度传感器相当与把通道端口连接在内部温度传感器上。函数如下。
HAL_StatusTypeDef HAL_ADC_ConfigChannel(ADC_HandleTypeDef *hadc, const ADC_ChannelConfTypeDef *pConfig)
2)读取通道VSENSE的 AD 值,计算结果。
在设置完之后,我们就可以读取温度传感器的电压值了, 得到该值就可以用上面的公式计算温度值。
2 内部温度传感器读取实现
2.1 STM32Cube生成工程
STM32H7S通道VSENSE连接内部温度传感器,因此,只需配置相应的参数即可。我们在串口的例子的基础上进行配置。
打开工程,打开Analog选项,配置ADC参数。
使能连续转换模式(Continuous Conversion Mode)。设置转换周期。其他为默认设置。
然后生成工程即可。
2.2 内部温度传感器具体代码
内部温度数据采集和普通ADC采集数据一样,其编程流程:
1.硬件等初始化;
2.串口、ADC等参数配置;
3.校准ADC,处理ADC数据;
主函数很简单:
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
float ADC_ConvertedValueLocal;
uint32_t ADC_ConvertedValue;
uint16_t ts_cal1 = 0;
uint16_t ts_cal2 = 0;
float temp = 0;
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_UART4_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
HAL_ADC_Start(&hadc1); //开启
ts_cal1 = *(volatile uint16_t*)(0x08FFF814);
ts_cal2 = *(volatile uint16_t*)(0x08FFF818);
temp = (float)((130.0f - 30.0f) / (ts_cal2-ts_cal1));
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_ADC_PollForConversion(&hadc1,10); //等待转换完成
if(HAL_IS_BIT_SET(HAL_ADC_GetState(&hadc1), HAL_ADC_STATE_REG_EOC))
{
ADC_ConvertedValue = HAL_ADC_GetValue(&hadc1);
ADC_ConvertedValueLocal =(float) ADC_ConvertedValue/(float)4096*3.3; // 读取转换的AD值
printf("The current AD value = 0x%04X \r\n", ADC_ConvertedValue);
printf("The current AD value = %f V \r\n",ADC_ConvertedValueLocal); //实际电压值
printf("temperture =%f\r\n\r\n",((ADC_ConvertedValue - ts_cal1) * temp + 30));
}
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
HAL_Delay(500);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
上述是通过查询的方式读取温度,也可通过中断和DMA的方式获取。
3 实验现象
将程序编译好后下载到板子中,通过串口助手可以看到在接收区有温度值输出。
- 2024-09-24
-
发表了主题帖:
【 匠芯创D133CBS】基于LVGL的DHT11温湿度显示
1 DHT11驱动架构
RT-Thread包含DHT11驱动,这里就没有去造轮子了,但是作为学习者,还是要去了解DHT11的原理及具体实现。这部分内容在笔者博客的STM32系列的外设篇已详细阐述,下面就DHT11驱动在RTT中的实现做个总结。
DHT11是采用单总线通讯的传感器,有的设备没有硬件单总线,DHT11的支持包采用 GPIO 模拟单总线时序。DHT11 的一次完整读时序需要 20ms,时间过长,故无法使用关中断或者关调度的方式实现独占 CPU 以保证时序完整正确。因此可能出现读取数据失败的情况。
DHT11的典型电路如下图所示。
Figure 1-1 DHT11典型电路
DATA 用于微处理器与 DHT11之间的通讯和同步,采用单总线数据格式,一次通讯时间4ms左右,数据分小数部分和整数部分,具体格式在下面说明,当前小数部分用于以后扩展,现读出为零.操作流程如下:
一次完整的数据传输为40bit,高位先出。
数据格式:8bit湿度整数数据+8bit湿度小数数据
+8bi温度整数数据+8bit温度小数数据
+8bit校验和
数据传送正确时校验和数据等于“8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据” 所得结果的末8位。
用户MCU发送一次开始信号后,DHT11从低功耗模式转换到高速模式,等待主机开始信号结束后,DHT11发送响应信号,送出40bit的数据,并触发一次信号采集用户可选择读取部分数据。从模式下,DHT11接收到开始信号触发一次温湿度采集,如果没有接收到主机发送开始信号,DHT11不会主动进行温湿度采集.采集数据后转换到低速模式。通讯过程如下图所示:
Figure 1-2 DHT11通讯时序图
熟悉RT-Thread系统都知道,RT-Thread将各个模块进行抽象,为上层提供统一的操作接口,依次提高上层代码的可重用性,自然也提高了应用开发效率。RT-Thread的驱动框架:
Figure 1-3 RT-Thread的驱动框架
应用程序通过 I/O 设备管理接口获得正确的设备驱动,然后通过这个设备驱动与底层 I/O 硬件设备进行数据(或控制)交互。
I/O设备模型框架位于硬件和应用程序之间,共分成三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层。
I/O设备管理层实现了对设备驱动程序的封装。应用程序通过 I/O 设备层提供的标准接口访问底层设备,设备驱动程序的升级、更替不会对上层应用产生影响。这种方式使得设备的硬件操作相关的代码能够独立于应用程序而存在,双方只需关注各自的功能实现,从而降低了代码的耦合性、复杂性,提高了系统的可靠性。
设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册 I/O 设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到 I/O 设备管理器中。
更加详细的内容请参看RT-Thread官方手册。
这里主要讲解Sensor驱动,RT-Thread将各个Sensor进行抽象,将不同厂商的Sensor合并为Sensor设备,从而提高了代码的重用性,提高开发效率。既然是总结,这里就只说重点,先看看DHT11设备驱动的时序图:
Figure 1-4 DHT11设备驱动的时序图
传感器数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、FIFO 模式。在使用的时候,这 3 种模式只能选其一,若传感器的打开参数 oflags 没有指定使用中断模式或者 FIFO 模式,则默认使用轮询模式。
oflags 参数支持下列参数:
#define RT_DEVICE_FLAG_RDONLY 0x001 /* 标准设备的只读模式,对应传感器的轮询模式 */
#define RT_DEVICE_FLAG_INT_RX 0x100 /* 中断接收模式 */
#define RT_DEVICE_FLAG_FIFO_RX 0x200 /* FIFO 接收模式 */
DHT11设备比较简单,采用的是轮询模式,其设备注册函数如下:
result = rt_hw_sensor_register(sensor, name, RT_DEVICE_FLAG_RDONLY, RT_NULL);
硬件初始化如下:
static int rt_hw_dht11_port(void)
{
struct rt_sensor_config cfg;
cfg.intf.user_data = (void *)DHT11_DATA_PIN;
rt_hw_dht11_init("dht11", &cfg);
return RT_EOK;
}
INIT_COMPONENT_EXPORT(rt_hw_dht11_port);
注意INIT_COMPONENT_EXPORT表示组件初始化,初始化顺序为4。
好了,DHT11就讲到这里了。
自动初始化:https://blog.bruceou.cn/2021/02/5-api-pi-auto-initialization/603/
Sensor:https://www.rt-thread.org/document/site/programming-manual/device/sensor/sensor/
2 LVGL UI绘制与移植
2.1 SquareLine绘制UI
笔者这里使用的是SquareLine Studio。在Screen中添加完后控件后,可点击play按钮后如下图所示。
点击Export->Export File,导出UI文件,包括UI Layout的.c和.h文件,以及PNG图片编码后的.c文件。
2.2 移植UI文件到D133CBS
找到一个一直好LVGL的工程,新建UI文件件,将上面生成的.c和.h文件添加到文件夹中。值得注意的是,D133CBS的SDK的LVGL使用的版本是v8.3.1,因此使用SquareLine Studio导数UI需要切换到v8.3.x的版本。
具体移植过程参考上一章。
3 DHT11获取温湿度并显示
RT-Thread提供了DHT11的驱动软件包,配置如下:
Figure 2-1 添加DHT11驱动
DHT11默认使用的 GPIO是可以修改的,在 dht11_sample.c 中修改以下代码:
#define DHT11_DATA_PIN 3//PA3
笔者这里使用PA3。
温度获取的参考代码如下。
/* Modify this pin according to the actual wiring situation */
#define DHT11_DATA_PIN 3
static void read_temp_entry(void *parameter)
{
rt_device_t dev = RT_NULL;
struct rt_sensor_data sensor_data;
rt_size_t res;
rt_uint8_t get_data_freq = 1; /* 1Hz */
dev = rt_device_find("temp_dht11");
if (dev == RT_NULL)
{
return;
}
if (rt_device_open(dev, RT_DEVICE_FLAG_RDWR) != RT_EOK)
{
rt_kprintf("open device failed!\n");
return;
}
rt_device_control(dev, RT_SENSOR_CTRL_SET_ODR, (void *)(&get_data_freq));
while (1)
{
res = rt_device_read(dev, 0, &sensor_data, 1);
if (res != 1)
{
rt_kprintf("read data failed! result is %d\n", res);
rt_device_close(dev);
return;
}
else
{
if (sensor_data.data.temp >= 0)
{
uint8_t temp = (sensor_data.data.temp & 0xffff) >> 0; // get temp
uint8_t humi = (sensor_data.data.temp & 0xffff0000) >> 16; // get humi
rt_kprintf("temp:%d, humi:%d\n" ,temp, humi);
}
}
rt_thread_delay(1000);
}
}
static int dht11_read_temp_sample(void)
{
rt_thread_t dht11_thread;
dht11_thread = rt_thread_create("dht_tem",
read_temp_entry,
RT_NULL,
1024,
RT_THREAD_PRIORITY_MAX / 2,
20);
if (dht11_thread != RT_NULL)
{
rt_thread_startup(dht11_thread);
}
return RT_EOK;
}
将获取的温度显示到UI,核心代码如下。
#include "ui.h"
#include "ui_helpers.h"
#include "sensor.h"
#include "sensor_dallas_dht11.h"
#include "aic_hal_gpio.h"
extern lv_obj_t * ui_HumidityData;
extern lv_obj_t * ui_temperatureData;
static rt_thread_t lvgl_thread = RT_NULL;
static rt_device_t dev = RT_NULL;
struct rt_sensor_data sensor_data;
static void dht11_event(void)
{
char str_temp[16];
float humidity, temperature;
rt_size_t res;
res = rt_device_read(dev, 0, &sensor_data, 1);
if (res != 1)
{
rt_kprintf("read data failed! result is %d\n", res);
rt_device_close(dev);
return;
}
else
{
if (sensor_data.data.temp >= 0)
{
uint8_t temp = (sensor_data.data.temp & 0xffff) >> 0; // get temp
uint8_t humi = (sensor_data.data.temp & 0xffff0000) >> 16; // get humi
rt_kprintf("temp:%d, humi:%d\n" ,temp, humi);
memset(str_temp,0,sizeof(str_temp));
// 把浮点数转换为字符串,存放在str_temp中。
sprintf(str_temp,"%d.00 C", temp);
//sprintf(str_temp,"%d C",lv_rand( 25, 30));
lv_label_set_text(ui_temperatureData, str_temp);
sprintf(str_temp,"%d.00 %%", humi);
//sprintf(str_temp,"%d %%",lv_rand( 50, 55));
lv_label_set_text(ui_HumidityData, str_temp);
}
}
}
static void lvgl_thread_entry(void *parameter)
{
/* 等待传感器正常工作 */
rt_thread_mdelay(1000);
rt_uint8_t get_data_freq = 1; /* 1Hz */
dev = rt_device_find("temp_dht11");
if (dev == RT_NULL)
{
return;
}
if (rt_device_open(dev, RT_DEVICE_FLAG_RDWR) != RT_EOK)
{
rt_kprintf("open device failed!\n");
return;
}
rt_device_control(dev, RT_SENSOR_CTRL_SET_ODR, (void *)(&get_data_freq));
while (1)
{
dht11_event();
rt_thread_mdelay(500);
}
}
int lvgl_update_init(void)
{
/* 创建线程*/
lvgl_thread = rt_thread_create("lvgl thread", /* 线程的名称 */
lvgl_thread_entry, /* 线程入口函数 */
RT_NULL, /* 线程入口函数的参数 */
1024, /* 线程栈大小,单位是字节 */
20, /* 线程的优先级,数值越小优先级越高*/
10); /* 线程的时间片大小 */
/* 如果获得线程控制块,启动这个线程 */
if (lvgl_thread != RT_NULL)
rt_thread_startup(lvgl_thread);
else
rt_kprintf("lvgl thread create failure !!! \n");
return RT_EOK;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(lvgl_update_init, lvgl data thread);
//INIT_APP_EXPORT(lvgl_update_init);
4 实验现象
程序编译下载到板卡后,会在串口中每 1s 打印一次温湿度数据。
[localvideo]1638eb0cf8c0eabccd4fb5c24152a318[/localvideo]
- 2024-09-23
-
发表了主题帖:
【 匠芯创D133CBS】基于SquareLine开发LVGL UI
SquareLine Studio做为LVGL官方推出的PC端开发工具,采用所见即所得的开发方式,大大减少了敲代码方式开发UI的时间,最早的名字是EdgeLine。
1 SquareLine下载安装
下载地址:https://squareline.io/downloads
根据自己的系统下载相应的软件包。
SquareLine Studio如果商用需要购买License,费用如下:
根据自己的需求购买吧,笔者这里使用免费的。
下载最新的SquareLine Studio 1.4.2版本,我是在Windows系统下开发,所以选择第一个Windows版本下载,下载完成后直接安装。
登录成功可以选择一个Example打开运行一下。
我这里选择第三个demo。
接下来可以导出工程。左上角File->Project Settings设置文件输出格式及保存的位置。
单栏Export->Export File选择导出文件的格式。
提示“Export Succeed”表示导出成功,找到对应的文件夹。以下即为导出的.c和.h文件。
2 SquareLine创建工程与移植
2.1 创建工程
打开SquareLine Studio,点击Creat,选择Desktop,右下角根据显示屏分辨率以及Bit数,填写好参数,点击右下角CREAT,创建工程。
在Screen中添加一个Label控件,编辑Label的字体颜色以及字体大小。
在Screen中添加完后控件后,可点击play按钮。
点击Export->Export File,导出UI文件,包括UI Layout的.c和.h文件,以及PNG图片编码后的.c文件。
2.2 移植UI文件到D133CBS
找到一个一直好LVGL的工程,新建UI文件件,将上面生成的.c和.h文件添加到文件夹中。值得注意的是,D133CBS的SDK的LVGL使用的版本是v8.3.1,因此使用SquareLine Studio导数UI需要切换到v8.3.x的版本。
然后将导出文件放入目录。
同时新建文件SConscript,文件内容如下:
from building import *
import os
cwd = GetCurrentDir()
group = []
src = []
CPPPATH = [cwd]
if GetDepend(['AIC_CHIP_D13X']) or GetDepend(['AIC_CHIP_G73X']):
src += Glob('./*.c')
list = os.listdir(cwd)
for d in list:
path = os.path.join(cwd, d)
if os.path.isfile(os.path.join(path, 'SConscript')):
group = group + SConscript(os.path.join(d, 'SConscript'))
group = group + DefineGroup('LVGL-port', src, depend = ['AIC_LVGL_USER_PROJECT'], CPPPATH = CPPPATH)
Return('group')
在application/Kconfig中增加配置。
然后使用scons --menuconfig配置。
然后保存退出。
再将ui.c中的ui_init()函数在aic_ui_init函数中调用即可。
最后编译工程。
烧写后现象如下。
-
发表了主题帖:
【STM32H7S78-DK】串口通信收发实现
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 串口简介
USART(Universal Synchronous Asynchronous Receiver and Transmitter,通用同步-异步接收发射器)提供了一种灵活的方法与使用工业标准NRZ异步串行数据格式的外部设备之间进行全双工数据交换。USART利用分数波特率发生器提供宽范围的波特率选择。它支持同步单向通信和半双工单线通信,也支持LIN(局部互连网),智能卡协议和IrDA(红外数据组织)SIR ENDEC规范,以及调制解调器(CTS/RTS)操作。它还允许多处理器通信。使用多缓冲器配置的DMA方式,可以实现高速数据通信。
虽然USART既可以同步又可以异步,但是常见的最常用的就是使用功能的异步功能,如果作为异步通信就是UART(Universal Asynchronous Receiver and Transmitter),可以说,UART是USART的子集,但是同步通信相比异步通信多了一根时钟同步信号线。
下面简单介绍下同步和异步。
在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调,同步数据,见下图。通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
Figure 1-1 同步通讯
在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包,以数据帧的格式传输数据,见下图,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。
Figure 1-2 异步通讯
在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小,而异步通讯双方的时钟允许误差较大。
从上面的介绍可以看出,USART以同步方式通信需要时钟同步信号,但不需要额外的起始、停止位,可以实现更快的传输速度。但USART控制起来更复杂,因此本文主要讲解以异步通信。
Figure 1-3 异步串口通信协议
异步串行通信以字符为单位,即一个字符一个字符地传送 。
串口外设的架构图看起来十分复杂,实际上对于软件开发人员来说,我们只需要大概了解串口发送的过程即可。从下至上,我们看到串口外设主要由三个部分组成,分别是波特率控制、收发控制和数据存储转移。
Figure 1-4 USART框图
波特率控制
波特率,即每秒传输的二进制位数,用 b/s (bps)表示,通过对时钟的控制可以改变波特率。在配置波特率时,我们向波特比率寄存器 USART_BRR 写入参数,修改了串口时钟的分频值USARTDIV。USART_BRR 寄存器包括两部分,分别是 DIV_Mantissa(USARTDIV 的整数部分)和 DIV_Fraction(USARTDIV 的小数)部分,最终,计算公式如下所示。
USARTDIV = DIV_Mantissa+( DIV_Fraction / ( 2- OVER8))。
USARTDIV 是对串口外设的时钟源进行分频的,对于USART1,由于它挂载在 APB2总线上,所以它的时钟源为 PCLK2 ;而 USART2、3 挂载在 APB1上,时钟源则为 PCLK1,串口的时钟源经过 USARTDIV 分频后分别输出作为发送器时钟及接收器时钟,控制发送和接收的时序。
对 USARTDIV 的尾数值和小数值进行编程时,接收器和发送器(Rx和Tx)的波特率均设置为相同值。
公式 1:适用于标准 USART(包括 SPI 模式)的波特率
公式 2:智能卡、 LIN 和 IrDA 模式下的波特率
收发控制
围绕着发送器和接收器控制部分,有好多个寄存器 :CR1、CR2、CR3 和 SR,即USART 的三个控制寄存器(Control Register)及一个状态寄存器(Status Register)。通过向寄存器写入 各种控制参数来控制发送和接收,如奇偶校验位、停止位等,还包括对USART 中断的控制 ;串口的状态在任何时候都可以从状态寄存器中查询得到。其中停止位的配置如下图所示。
Figure 1-5 停止位配置
发送配置步骤:
通过在USART_CR1寄存器上置位UE位来激活USART
编程USART_CR1的M位来定义字长。
在USART_CR2中编程停止位的位数。
如果采用多缓冲器通信,配置USART_CR3中的DMA使能位(DMAT)。按多缓冲器通信中的描述配置DMA寄存器。
利用USART_BRR寄存器选择要求的波特率。
设置USART_CR1中的TE位,发送一个空闲帧作为第一次数据发送。
把要发送的数据写进USART_DR寄存器(此动作清除TXE位)。在只有一个缓冲器的情况下,对每个待发送的数据重复步骤7。
在USART_DR寄存器中写入最后一个数据字后,要等待TC=1,它表示最后一个数据帧的传输结束。当需要关闭USART或需要进入停机模式之前,需要确认传输结束,避免破坏最后一次传输。
接收配置步骤:
将USART_CR1寄存器的UE置1来激活USART。
编程USART_CR1的M位定义字长
在USART_CR2中编写停止位的个数
如果需多缓冲器通信,选择USART_CR3中的DMA使能位(DMAR)。按多缓冲器通信所要求的配置DMA寄存器。
利用波特率寄存器USART_BRR选择希望的波特率。
设置USART_CR1的RE位。激活接收器,使它开始寻找起始位。
数据存储转移
收发控制器根据我们的寄存器配置,对数据存储转移部分的移位寄存器进行控制。当我们需要发送数据时,内核或 DMA 外设(一种数据传输方式,在后面介绍)把数据从内存(变量)写入到发送数据寄存器 TDR 后,发送控制器将适时地自动把数据从 TDR 加载到发送移位寄存器,然后通过串口线 Tx,把数据一位一位地发送出去,当数据从 TDR转移到移位寄存器时,会产生发送寄存器 TDR 已空事件 TXE,当数据从移位寄存器全部发送出去时,会产生数据发送完成事件 TC,这些事件可以在状态寄存器中查询到。而接收数据则是一个逆过程,数据从串口线 Rx 一位一位地输入到接收移位寄存器,然后自动地转移到接收数据寄存器 RDR,最后用内核指令或 DMA 读取到内存(变量)中。
以上对串口通信进行了简单介绍,为了方便各位读者朋友更好的理解,在这里笔者将引入一个新的思想--系统分层思想。既然各位对着有意于嵌入式,那么必须得有对整个系统的架构要有一定的认知。对STM32裸机开发,我们可以将分为三层:物理层、协议层和应用层。前文讲了这么多也是对串口协议进行分析,常用的物理层的串口通信标准有232和485。
【注】UART和USART的区别
USART(universal synchronous asynchronous receiver and transmitte): 通用同步异步收发器,USART是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。
UART(universal asynchronous receiver and transmitter): 通用异步收发器,异步串行通信口(UART)就是我们在嵌入式中常说的串口,它还是一种通用的数据通信议。从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型。
当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。如STM32的USART可以提供时钟支持ISO7816的智能卡接口。
USART是指单片机的一个端口模块,可以根据需要配置成同步模式(SPI,I2C),也可以将其配置为异步模式,后者就是UART。所以说UART姑且可以称之为一个与SPI,I2C对等的“协议”,而USART则不是一个协议,而是更应该理解为一个实体。相比于同步通讯,UART不需要统一的时钟线,接线更加方便。但是,为了正常的对信号进行解码,使用UART通讯的双方必须事先约定好波特率,即每个码元的长度。
关于串口的深入理解,请参看笔者文章:
https://blog.bruceou.cn/2021/01/detailed-explanation-of-stm32-serial-communication/555/
https://bruceou.blog.csdn.net/article/details/109431870
2 串口通信的寄存器描述
串口常用的寄存器有控制寄存器 (USART_CR)、数据发送寄存器(USART_TDR)、波特比率寄存器(USART_BRR)。
Figure 2-1 控制寄存器1
Figure 2-2 数据发送寄存器
Figure 2-3 波特比率寄存器
3 串口硬件
串口的接口通过三个引脚与其他设备连接在一起。任何USART双向通信至少需要两个脚:接收数据输入(RX)和发送数据输出(TX)。
Figure 3-1 USB转串口
RX:接收数据串行输入。通过采样技术来区别数据和噪音,从而恢复数据。
TX :发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,并且不发送数据时,TX引脚处于高电平。在单线和智能卡模式里,此I/O 口被同时用于数据的发送和接收。
这里使用USART4,接到ST-LINK的虚拟串口。这样USB转串口不仅可以为开发板供电,还可以与PC串口通信。
4 串口发送(重定向printf)
4.1 STM32Cube生成工程
点击USATR4,设置MODE为异步通信(Asynchronous) ,波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 ,16 倍过采样速度,接收和发送都使能。
Figure 4-1 基础参数配置
GPIO引脚设置 USART_RX->PD0/USART_TX->PD1,默认即可。
Figure 4-2 USART的GPIO配置
然后生成工程即可。
4.2 串口发送代码讲解
下面笔者还要介绍一种常用的串口打印方式I/O重定向,也就是使用printf打印数据到终端,但是我们的裸机系统没有终端,因此如果想让printf / scanf向USART1发送、获取数据,需要通过代码指定C标准库输入/输出函数的控制终端设备,也就是使用功能I/O重定向。
在stdio.h有相应的接口。
/*
* dynamically allocates a buffer of the right size for the
* formatted string, and returns it in (*strp). Formal return value
* is the same as any other printf variant, except that it returns
* -1 if the buffer could not be allocated.
*
* (The functions with __ARM_ prefixed names are identical to the
* ones without, but are available in all compilation modes without
* violating user namespace.)
*/
extern _ARMABI int fgetc(FILE * /*stream*/) __attribute__((__nonnull__(1)));
/*
* reads at most one less than the number of characters specified by n from
* the stream pointed to by stream into the array pointed to by s. No
* additional characters are read after a new-line character (which is
* retained) or after end-of-file. A null character is written immediately
* after the last character read into the array.
* Returns: s if successful. If end-of-file is encountered and no characters
* have been read into the array, the contents of the array remain
* unchanged and a null pointer is returned. If a read error occurs
* during the operation, the array contents are indeterminate and a
* null pointer is returned.
*/
extern _ARMABI int fputc(int /*c*/, FILE * /*stream*/) __attribute__((__nonnull__(2)));
下面我们以实现printf打印数据到USART(即重定义fputc函数)的实现过程。
我们先看如何实现的,再讲解具体的代码。先实现printf重定向函数,在main.c中添加如下函数。
/**
* @brief 重定向c库函数printf到USARTx
* @retval None
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart4, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/**
* @brief 重定向c库函数getchar,scanf到USARTx
* @retval None
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart4, &ch, 1, 0xffff);
return ch;
}
另外还需添加微库以便支持printf。具体设置参看本节后文的小贴士部分。
这里只讲解串口的参数初始化,代码如下:
/**
* @brief UART4 Initialization Function
* @param None
* @retval None
*/
static void MX_UART4_Init(void)
{
/* USER CODE BEGIN UART4_Init 0 */
/* USER CODE END UART4_Init 0 */
/* USER CODE BEGIN UART4_Init 1 */
/* USER CODE END UART4_Init 1 */
huart4.Instance = UART4;
huart4.Init.BaudRate = 115200;
huart4.Init.WordLength = UART_WORDLENGTH_8B;
huart4.Init.StopBits = UART_STOPBITS_1;
huart4.Init.Parity = UART_PARITY_NONE;
huart4.Init.Mode = UART_MODE_TX_RX;
huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart4.Init.OverSampling = UART_OVERSAMPLING_16;
huart4.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart4.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart4.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart4) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart4, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart4, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&huart4) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN UART4_Init 2 */
/* USER CODE END UART4_Init 2 */
}
这是STM32cudeMX自动生成的代码,我们要注意UART_HandleTypeDef结构体,这个结构体就是用来配串口参数的,原型如下:
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /*!< UART registers base address */
UART_InitTypeDef Init; /*!< UART communication parameters */
uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */
uint16_t TxXferSize; /*!< UART Tx Transfer size */
__IO uint16_t TxXferCount; /*!< UART Tx Transfer Counter */
uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */
uint16_t RxXferSize; /*!< UART Rx Transfer size */
__IO uint16_t RxXferCount; /*!< UART Rx Transfer Counter */
DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */
DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_UART_StateTypeDef gState; /*!< UART state information related to global Handle management
and also related to Tx operations.
This parameter can be a value of [url=home.php?mod=space&uid=1064992]@ref[/url] HAL_UART_StateTypeDef */
__IO HAL_UART_StateTypeDef RxState; /*!< UART state information related to Rx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO uint32_t ErrorCode; /*!< UART Error code */
} UART_HandleTypeDef;
这个结构体很简单,也有英文注释,笔者就不在赘述了。当然啦,除了使用普通方式发送,还可使用中断方式和DMA方式发送数据。这里中断发送数据就不讲了,下面会将中断接收,有兴趣的朋友请自行去看参考手册自行实现,DMA方式会在介绍DMA的时候讲解。
最后在main函数调用printf函数,代码如下:
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
char str[20];
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_UART4_Init();
/* USER CODE BEGIN 2 */
printf("串口普通方式实验 \r\n");
/* sprintf函数把格式化的数据写入某个字符串 */
sprintf(str,"20%02d-%02d-%02d",24,9,23);
/* 调用格式化输出函数打印输出数据 */
printf("%s\n",str);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
好了,这就是串口发送代码实现。
完整代码请查看配套程序,另外还需添加Use MicroLIB以便支持printf。具体设置参看本节后文的小贴士部分。
笔者在此给出输出格式的说明,请读者朋友参考。
Table 4-1 输出格式说明
格式
说明
%d
按照十进制整型数打印
%6d
按照十进制整型数打印,至少6个字符宽
%f
按照浮点数打印
%6f
按照浮点数打印,至少6个字符宽
%.2f
按照浮点数打印,小数点后有2位小数
%6.2f
按照浮点数打印,至少6个字符宽,小数点后有2位小数
%x
按照十六进制打印
%c
打印字符
%s
打印字符串
我们来总结下串口发送的流程:
1.初始化硬件,时钟;
2.USART 的GPIO初始化,USART参数初始化;
3.重定向printf
4.打印输出
4.3 实验现象
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
Figure 4-3 串口发送实验结果
5 串口接收数据(中断方式)
5.1 STM32Cube生成工程
和串口发送的大体一致,主要要配置中断。NVIC Settings 一栏使能接收中断,默认没打开。
Figure 5-1 USART中断使能
另外,还需配置中断优先级,默认即可。
Figure 5-2 中断有优先级
5.2 串口接收代码讲解
和串口发送数据一样,先看如何实现的,然后再进行代码讲解,
在main.c中添加下列定义:
#define RXBUFFERSIZE 256 //最大接收字节数
char TxBuffer[RXBUFFERSIZE]; //发送缓冲
uint8_t RxBuffer; //接收中断缓冲
uint8_t Uart1_Rx_Cnt = 0; //接收缓冲计数
在main()主函数中,调用一次接收中断函数
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
在main.c下方添加中断回调函数
/**
* @brief Rx Transfer completed callbacks.
* @param huart Pointer to a UART_HandleTypeDef structure that contains
* the configuration information for the specified UART module.
* @retval None
*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
/* NOTE: This function Should not be modified, when the callback is needed,
the HAL_UART_TxCpltCallback could be implemented in the user file
*/
if(Uart1_Rx_Cnt >= 255) //溢出判断
{
Uart1_Rx_Cnt = 0;
memset(TxBuffer,0x00,sizeof(TxBuffer));
HAL_UART_Transmit(&huart4, (uint8_t *)"数据溢出", 10,0xFFFF);
}
else
{
TxBuffer[Uart1_Rx_Cnt++] = RxBuffer; //接收数据转存
if((TxBuffer[Uart1_Rx_Cnt-1] == 0x0A)&&(TxBuffer[Uart1_Rx_Cnt-2] == 0x0D)) //判断结束位
{
HAL_UART_Transmit(&huart4, (uint8_t *)&TxBuffer, Uart1_Rx_Cnt,0xFFFF); //将收到的信息发送出去
while(HAL_UART_GetState(&huart4) == HAL_UART_STATE_BUSY_TX);//检测UART发送结束
Uart1_Rx_Cnt = 0;
memset(TxBuffer,0x00,sizeof(TxBuffer)); //清空数组
}
}
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1); //开启接收中断
}
好了。接下来看看主函数代码:
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_UART4_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart4, (uint8_t *)&RxBuffer, 1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
总结下串口接收的编程流程:
1.硬件初始化,时钟初始化;
2.串口GPIO初始化,串口参数配置;
3.在main()函数中使能中断接收;
4.编写HAL_UART_RxCpltCallback中断回调函数,处理接收的数据,
【注】中断接收函数只能触发一次接收中断,所以我们需要在中断回调函数中再次调用中断接收函数。这里可以对比下标准库的流程。
5.3 实验现象
将程序编译好下载到板子中,打开串口助手,按下图设置相应参数,按下板子的复位按键,在接收区可以看到如下信息。
Figure 5-3 串口接收实验结果
-
发表了主题帖:
【STM32H7S78-DK】PWM输出波形
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 PWM输出的工作原理
脉冲宽度调制(PWM),是英文“Pulse Width Modulation” 的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。
STM32 的定时器除了 TIM6 和 7(基本定时器)。其他的定时器都可以用来产生 PWM 输出。其中高级定时器 TIM1可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4路的 PWM 输出。
每个定时器有四个通道,每一个通道都有一个捕获比较寄存器,,将寄存器值和计数器值比较,通过比较结果输出高低电平,便可以实现脉冲宽度调制模式(PWM信号)。
在上一节,讲解了定时器的相关寄存器即基本原理,本节将不再赘述。下面谈谈如何使用定时器的寄存器进行PWM输出的。若配置脉冲计数器TIMx_CNT为向上计数,而重载寄存器TIMx_ARR配置为N,即TIMx_CNT的当前计数值数值X在TIMxCLK时钟源的驱动下不断累加,当TIMx_CNT的数值X大于N时,会重置TIMx_CNT数值为0重新计数。而在TIMxCNT计数的同时,TIMxCNT的计数值X会与比较寄存器TIMx_CCR预先存储了的数值A进行比较,当脉冲计数器TIMx_CNT的数值X小于比较寄存器TIMx_CCR的值A时,输出高电平(或低电平),相反地,当脉冲计数器的数值X大于或等于比较寄存器的值A时,输出低电平(或高电平)。如此循环,得到的输出脉冲周期就为重载寄存器TIMx_ARR存储的数值(N+1)乘以触发脉冲的时钟周期,其脉冲宽度则为比较寄存器TIMx_CCR的值A乘以触发脉冲的时钟周期,即输出PWM的占空比为A/(N+1)。
估计很多初学者看了上面的一段话都很蒙圈,没关系,下面以向上计数模式为例进行讲解。
Figure 1-1 向上计数模式
在PWM输出模式下,除了CNT(计数器当前值)、ARR(自动重装载值)之外,还多了一个值CCRx(捕获/比较寄存器值)。当CNT小于CCRx时,TIMx_CHx通道输出低电平;当CNT等于或大于CCRx时,TIMx_CHx通道输出高电平。因此得到PWM的一个周期如下:
1.定时器从0开始向上计数;
2.当0-t1段,定时器计数器TIMx_CNT值小于CCRx值,输出低电平;
3.t1-t2段,定时器计数器TIMx_CNT值大于CCRx值,输出高电平;
4.当TIMx_CNT值达到ARR时,定时器溢出,重新向上计数...循环此过程。
至此一个PWM周期完成。针对PWM重点关注两个寄存器,TIMx_ARR寄存器确定PWM频率,TIMx_CCRx寄存器确定占空比。
上文提到了PWM的输出模式,下面讲解PWM的工作模式:
PWM模式1(向上计数) :计数器从0计数加到自动重装载值(TIMx_ARR),然后重新从0开始计数,并且产生一个计数器溢出事。
PWM模式2(向下计数) :计数器从自动重装载值(TIMx_ARR)减到0,然后重新从重装载值(TIMx_ARR)开始递减,并且产生一个计数器溢出事件。
这里我们仅利用 TIM3产生多路 PWM 输出。如果要产生多路输出,大家可以根据我们的代码稍作修改即可。具体不同定时器对应引脚在对应芯片数据手册的引脚说明(pin description) 中查看。
[ps] 本文以H7S系列为例进行讲解,ST不同系列其定时器个数不同
2 PWM输出的寄存器描述
同样,我们首先通过对 PWM 相关的寄存器进行讲解,大家了解了定时器 TIM3 的 PWM原理之后,我们再讲解怎么使用库函数产生 PWM 输出。
要使 STM32 的通用定时器 TIMx 产生 PWM 输出,除了上一章介绍的寄存器外,我们还会用到 3 个寄存器,来控制 PWM 的。这三个寄存器分别是:捕获 /比较模式寄存器( TIMx_CCMR1/2)、捕获/比较使能寄存器( TIMx_CCER)、捕获/比较寄存器( TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。
首先是捕获/比较模式寄存器( TIMx_CCMR1/2),该寄存器总共有 2 个, TIMx _CCMR1和 TIMx _CCMR2。 TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。该寄存器的各位描述如图所示。
Figure 2-1 TIMx_CCMR1 寄存器各位描述
该寄存器的有些位在不同模式下,功能不一样,所以在上图中,我们把寄存器分了 2层,上面一层对应输出而下面的则对应输入。关于该寄存器的详细说明,请参考《 STM32 参考手册》。这里我们需要说明的是模式设置位 OCxM,此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,所以这 3 位必须设置为 110/111。这两种PWM 模式的区别就是输出电平的极性相反。
接下来,我们介绍捕获/比较使能寄存器( TIMx_CCER),该寄存器控制着各个输入输出通道的开关。该寄存器的各位描述如下图。
Figure 2-2 TIMx_ CCER 寄存器各位描述
该寄存器比较简单, 我们这里只用到了 CC2E 位,该位是输入/捕获 2 输出使能位,要想PWM 从 IO 口输出,这个位必须设置为 1,所以我们需要设置该位为 1。
最后,我们介绍一下捕获/比较寄存器( TIMx_CCR1~4),该寄存器总共有 4 个,对应 4 个输通道 CH1~4。因为这 4 个寄存器都差不多,我们仅以 TIMx_CCR1 为例介绍,该寄存器的各位描述如下图。
Figure 2-3 寄存器 TIMx_ CCR1 各位描述
在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。
假如我们要利用 TIM3 的 CH2 输出 PWM 来控制 DS0 的亮度,但是 TIM3_CH2 默认是接在 PA7上面的,这就可以通过映射功能,把 TIM3_CH2映射到 PB5 上。
Figure 2-4 寄存器 GPIOx_AFRx各位描述
STM32 的重映射控制是由复用重映射和调试 IO 配置寄存器(GPIOx_AFR)控制的,该寄存器的各位描述如上图,该寄存器分为GPIOx_AFRL和GPIOx_AFRH。
下面总结下PWM的工作过程:
Figure 2-5 PWM工作过程
1.CCR1寄存器:捕获/比较值寄存器:设置比较值;
计数器值TIMx_CNT与通道1捕获比较寄存器CCR1进行比较,通过比较结果输出有效电平和无效电平
OC1REF=0 无效电平
OC1REF=1 无效电平
2.TIMx_CCMR1寄存器:OC1M[2:0]位:用于设置PWM模式
110:PWM模式1
111:PWM模式2
3.CCER寄存器:CC1P位:输入/捕获1输出极性。
0:高电平为有效电平
1:低电平为有效电平
4.CCER寄存器:CC1E位:输入/捕获1输出使能。
0:关闭使能
1:打开使能
5.输出电平信号
TIM定时器的四路通道TIMx_CHx输出PWM
Figure 2-6 通用定时器框图
3 PWM周期、占空比分析
根据前面的参数配置,我们可以算出PWM的输出周期:
这里我们 arr=999 psc=0 Tclk=299Mhz ,
因此PWM的输出频率300KHz,周期是3.3us。
PWM的占空比为:
PWM自动重装值为999,两个通道的跳变值分别为500,375。因此,两个通道的占空比分别为50%,37.5%。
4 PWM输出实现
4.1 STM32Cube生成工程
本文介绍在STM32CubeMX进行定时器的配置,这里我们仅利用 TIM4的2路通道输出,方便我们比较波形。具体不同定时器对应引脚在对应芯片数据手册的引脚说明(pin description) 中查看。
1.时钟配置
笔者的板子使用的外部晶振为24MHz,选择外部时钟HSE 24MHz ,PLL锁相环后为600MHz,系统时钟来源选择为PLL,设置APB1分频器为 /2,这时候定时器的时钟频率为300Mhz。本文笔者使用的定时器是TIM4,TIM4挂在APB1上,不同的定时器挂在不同总线上的。
Figure 4-1 时钟配置
2.Times配置
选择TIM,使能TIM4,指定时钟源。
Figure 4-2 使能TIM4时钟源
【注】TIM4的时钟源有两个选项
选项1 :Internal Clock 内部时钟
选项2 : ETR2 外部触发输入(ETR)(仅适用TIM2,3,4)
本文要使用TIM4的2个通道,因此需要将其使能。每个通道有很多模式,这里选择PWM输出。当对应的通道打开后,对应的GPIO也会被使能。
Figure 4-3 使能TIM4的通道
【注】如果使能通道前通道中GPIO使用过,STM32CubeMX会自动将GPIO配置为重映射的GPIO。举个例子,当PB0被占用了,那么四个GPIO会重映射到PC6-PC9。
PWM参数配置如下:
Counter setting
Prtscaler (定时器分频系数) : 0
Counter Mode(计数模式) :Up(向上计数模式)
Counter Period(自动重装载值) : 999
CKD(时钟分频因子) : No Division 不分频
选项: 可以选择二分频和四分频
auto-reload-preload(自动重装载) : Enable 使能
TRGO Output (TRGO) Parameters
Master/Slave Mode(MSM bit):Disable
TRGO:定时器的触发信号输出 在定时器的定时时间到达的时候输出一个信号(如:定时器更新产生TRGO信号来触发ADC的同步转换,)
PWM Generation Channel (2个CH)
Mode(定时模式):PWM mode 1
Pulse(计数比较值):四个通道分别为500,375
CH Polarity(输出极性):High
Figure 4-4 PWM输出参数配置
根据前面的参数配置,我们可以算出PWM的输出周期,这里我们 arr=999 psc=0 Tclk=300Mhz ,
本文选择的是PWM模式1,在向上计数时,一旦TIMx_CNT < TIMx_CCR1(计数比较值)。时通道1为有效电平,否则为无效电平;在向下计数时,一旦TIMx_CNT>TIMx_CCR1时通道1为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。输出比较极性的指的是你在比较匹配之后输出口输出的极性,也就是设置比较输出的有效电平。你可以设置为高电平有效或者低电平有效。如果设置为高电平有效,那么当定时器比较匹配之后,输出口输出高电平,否则就反一下。
如果是PWM模式1,且向上计数,如果极性设置为低,那么 TIMx_CNT < TIMx_CCR1 时,输出低电平,更简单就是占空比为1 -TIMx_CCR1/(ARR+1). 如果极性为高,占空比就是TIMx_CCR1/(ARR+1)。
好了,到这里,配置就完成了,生成工程就行了。
4.2 PWM输出的具体代码分析
我们先看看主函数,其代码如下:
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM4_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim4);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
在主循环前面,需要对TIM4进行初始化配置:
HAL_TIM_Base_Start_IT(&htim4);
然后再开启四路通道的PWM:
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
PWM输出最重要就是MX_TIM4_Init()函数,这个函数包含了TIM4的PWM配置。
5 PWM输出的实验现象
在前面我们输出了TIM4 的通道 1(PD12)、2(PD13)不同占空比的 PWM 信号。接下来就看看PWM的输出,PWM 信号可以通过示波器看到。下面笔者就是用逻辑分析仪查看波形。
首先笔者使用的逻辑分析仪是Kingst LA5016,当然啦,其他的也可以,关于逻辑分析仪的相关使用笔者这里就不介绍了,可以查看官方资料。
首先将通道1(PD12)、2(PD13)分别接到逻辑分析仪的CH0 – CH1,然后下载程序到板子中,打开Kingst VIS,然后进行采样。
我们就可以看到不同通道的实际周期,占空比等信息。从上图可以看到,实际测量的频率和占空比和理论是相符的。
- 2024-09-21
-
发表了主题帖:
【STM32H7S78-DK】跳动的心脏-Systick
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
Cortex-M的内核中包含Systick定时器了,只要是Cortex-M系列的MCU就会有Systick,因此这是通用的,下面详细分析。
1 Systick工作原理分析
SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常(异常号 :15)。在以前,操作系统和所有使用了时基的系统都必须有一个硬件定时器来产生需要的“滴答”中断,作为整个系统的时基。滴答中断对操作系统尤其重要。例如,操作系统可以为多个任务分配不同数目的时间片,确保没有一个任务能霸占系统 ;或者将每个定时器周期的某个时间范围赐予特定的任务等,操作系统提供的各种定时功能都与这个滴答定时器有关。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
Cortex-M7在内核部分包含了一个简单的定时器——SysTick。因为所有的Cortex-M7芯片都带有这个定时器,软件在不同芯片生产厂商的 CM3 器件间的移植工作就得以简化。该定时器的时钟源可以是内部时钟(FCLK,Cortex-M7上的自由运行时钟),或者是外部时钟(Cortex-M7处理器上的 STCLK 信号)。不过,STCLK 的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能大不相同。因此,需要阅读芯片的使用手册来确定选择什么作为时钟源。在 STM32 中 SysTick 以 HCLK(AHB 时钟)或 HCLK/8 作为运行时钟。
SysTick 定时器能产生中断,Cortex-M7为它专门开出一个异常类型,并且在向量表中有它的一席之地。它使操作系统和其他系统软件在 Cortex-M7器件间的移植变得简单多了,因为在所有 Cortex-M7产品间,SysTick 的处理方式都是相同的。SysTick 定时器除了能服务于操作系统之外,还能用于其他目的,如作为一个闹铃、用于测量时间等。Systick 定时器属于Cortex 内核部件。
2 Systick寄存器分析
在传统的嵌入式系统软件按中通常实现 Delay(N) 函数的方法为:
for(i = 0; i <= x; i ++);
x --- ;
对于STM32系列微处理器来说,执行一条指令只有几十个 ns,进行 for 循环时,要实现 N 毫秒的 x 值非常大,而且由于系统频率的宽广,很难计算出延时 N 毫秒的精确值。针对 STM32 微处理器,需要重新设计一个新的方法去实现该功能,以实现在程序中使用 Delay(N)。
Cortex-M的内核中包含一个 SysTick 时钟。SysTick 为一个 24 位递减计数器,SysTick 设定初值并使能后,每经过 1 个系统时钟周期,计数值就减 1。计数到 0 时,SysTick 计数器自动重装初值并继续计数,同时内部的 COUNTFLAG 标志会置位,触发中断 (如果中断使能情况下)。
在 STM32 的应用中,使用 Cortex-M内核的 SysTick 作为定时时钟,设定每一毫秒产生一次中断,在中断处理函数里对 N 减一,在Delay(N) 函数中循环检测 N 是否为 0,不为 0 则进行循环等待;若为 0 则关闭 SysTick 时钟,退出函数。
注: 全局变量 TimingDelay , 必须定义为 volatile 类型 , 延迟时间将不随系统时钟频率改变。
STM32中的Systick 部分内容属于NVIC控制部分,一共有4个寄存器,名称和地址分别是:
STK_CTRL, 0xE000E010--控制寄存器
Table 2-1 SysTick控制及状态寄存器
第0位:ENABLE,Systick 使能位
(0:关闭Systick功能;1:开启Systick功能)
第1位:TICKINT,Systick 中断使能位
(0:关闭Systick中断;1:开启Systick中断)
第2位:CLKSOURCE,Systick时钟源选择
(0:使用HCLK/8 作为Systick时钟;1:使用HCLK作为Systick时钟)
第16位:COUNTFLAG,Systick计数比较标志,如果在上次读取本寄存器后,SysTick 已经数到了0,则该位为1。如果读取该位,该位将自动清零
STK_LOAD, 0xE000E014--重载寄存器
Table 2-2 SysTick重装载数值寄存器
Systick是一个递减的定时器,当定时器递减至0时,重载寄存器中的值就会被重装载,继续开始递减。STK_LOAD 重载寄存器是个24位的寄存器最大计数0xFFFFFF。
STK_VAL, 0xE000E018--当前值寄存器
Table 2-3 SysTick当前数值寄存器
也是个24位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick 控制及状态寄存器中的COUNTFLAG标志。
STK_CALRB, 0xE000E01C--校准值寄存器
Table 2-4 SysTick校准数值寄存器
校准值寄存器提供了这样一个解决方案:它使系统即使在不同的Cortex-M产品上运行,也能产生恒定的SysTick中断频率。最简单的作法就是:直接把TENMS的值写入重装载寄存器,这样一来,只要没突破系统极限,就能做到每10ms来一次 SysTick异常。如果需要其它的SysTick异常周期,则可以根据TENMS的值加以比例计算。只不过,在少数情况下,Cortex-M芯片可能无法准确地提供TENMS的值(如,Cortex-M的校准输入信号被拉低),所以为保险起见,最好在使用TENMS前检查器件的参考手册。
SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。要注意的是,当处理器在调试期间被喊停( halt)时,则SysTick定时器亦将暂停运作。
3 Systick定时器实现
3.1 STM32Cube配置工程
关于如何使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。本文要实现流水灯,其实输出为初始化设置为高电平还是低电平都可以,因为流水灯需要不断反转。
这里需要注意sys配置,也就是滴答定时器的配置。
Figure 3-1 滴答定时器
以上配置和GPIO流水灯是一样的,本文只具体讲解Systick的内容。
3.2 Systick定时器具体代码分析
Systick属于内核部分,相关的寄存器定义与库函数都在内核相关的文件core_cm7.h中,在上标准库函数版本中已经分析过了。那么HAL库函数是如何初始化Systick的呢?在HAL_Init()函数中调用了HAL_InitTick()函数,这才是Systick初始化入口。
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] This function configures the source of the time base:
* The time source is configured to have 1ms time base with a dedicated
* Tick interrupt priority.
* @note This function is called automatically at the beginning of program after
* reset by HAL_Init() or at any time when clock is reconfigured by HAL_RCC_ClockConfig().
* @note In the default implementation, SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals.
* Care must be taken if HAL_Delay() is called from a peripheral ISR process,
* The SysTick interrupt must have higher priority (numerically lower)
* than the peripheral interrupt. Otherwise the caller ISR process will be blocked.
* The function is declared as __weak to be overwritten in case of other
* implementation in user file.
* @param TickPriority Tick interrupt priority.
* @retval HAL status
*/
__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
HAL_StatusTypeDef status = HAL_OK;
/* Check uwTickFreq for MisraC 2012 (even if uwTickFreq is a enum type that doesn't take the value zero)*/
if ((uint32_t)uwTickFreq != 0U)
{
/*Configure the SysTick to have interrupt in 1ms time basis*/
if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / (uint32_t)uwTickFreq)) == 0U)
{
/* Configure the SysTick IRQ priority */
if (TickPriority < (1UL << __NVIC_PRIO_BITS))
{
HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
uwTickPrio = TickPriority;
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
}
else
{
status = HAL_ERROR;
}
/* Return function status */
return status;
}
HAL_SYSTICK_Config()函数默认中断周期是1ms,HAL_TICK_FREQ_DEFAULT是一个宏定义表示计数的频率,默认是1,也就是1KHz,也就是1/1000,那么中断一次的时间为600000000/1000/1*(1/600000000)=1ms。那么我们要延时1s怎么做呢。我们在上一节流水灯使用了HAL_Delay()函数,函数原型如下。
/**
* @brief This function provides minimum delay (in milliseconds) based
* on variable incremented.
* @note In the default implementation , SysTick timer is the source of time base.
* It is used to generate interrupts at regular time intervals where uwTick
* is incremented.
* @note This function is declared as __weak to be overwritten in case of other
* implementations in user file.
* @param Delay specifies the delay time length, in milliseconds.
* @retval None
*/
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay;
/* Add a period to ensure minimum wait */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)uwTickFreq;
}
while ((HAL_GetTick() - tickstart) < wait)
{
}
}
在函数中HAL_Delay(),(HAL_GetTick() - tickstart) < wait用于延时的中断周期数,在Systick初始化函数中,中断间隔为1ms,HAL_Delay ()函数的传入参数Delay表示多少个中断周期,也就是我们最终的延时,我们传入Delay = 500,那么最终的延时就是500ms。
我们再来看看HAL_GetTick()函数。
__weak uint32_t HAL_GetTick(void)
{
return uwTick;
}
HAL_GetTick()函数很简单,不断获取uwTick得值,这是一个全局变量,可以发现在HAL_IncTick()函数中使用过。那么HAL_IncTick()函数被那个函数调用了呢?
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq;
}
不难发现,在stm32h7rsxx_it.c中间中的SysTick_Handler()函数中调用了HAL_IncTick()函数,SysTick_Handler()也就是滴答定时器的中断服务函数,也就是中断一次会调用一次,也就会uwTick变量累加一次,最终uwTick累加到Delay次,表示此次延时结束。
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
/* USER CODE END SysTick_IRQn 0 */
HAL_IncTick();
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
好了,使用STM32Cube配置SysTick定时器的延时就讲解完成了,在主函数是使用延时函数控制LED就是流水灯了。
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* Enable the CPU Cache */
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Update SystemCoreClock variable according to RCC registers values. */
SystemCoreClockUpdate();
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
4 实验现象
将编译好的程序下载到板子中,可以看到四个LED灯不同地闪烁。
-
发表了主题帖:
【STM32H7S78-DK】STM32H7S78启动流程详解(Keil版)
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
对于我们常用的桌面操作系统而言,我们在开发应用时,并不关心系统的初始化,绝大多数应用程序是在操作系统运行后才开始运行的,操作系统已经提供了一个合适的运行环境,然而对于嵌入式设备而言,在设备上电后,所有的一切都需要由开发者来设置,这里处理器是没有堆栈,没有中断,更没有外围设备,这些工作是需要软件来指定的,而且不同的CPU类型、不同大小的内存和不同种类的外设,其初始化工作都是不同的。本文将以STM32H7S7L8H6H (基于Cortex-M7)为例进行讲解。
在开始正式讲解之前,你需要了解ARM寄存器、汇编以及反编译相关的知识,这些可以参考笔者博文。
深入理解ARM寄存器:https://bruceou.blog.csdn.net/article/details/117866186
ARM汇编入门:https://bruceou.blog.csdn.net/article/details/117897496
Keil反编译入门(一):https://bruceou.blog.csdn.net/article/details/118314875
Keil反编译入门(二):https://bruceou.blog.csdn.net/article/details/118400368
下面我们就来具体看一下用户从Flash启动STM32的过程,主要讲解从上电复位到main函数的过程。主要有以下步骤:
1.初始化堆栈指针 SP=_initial_sp,初始化 PC 指针=Reset_Handler
2.初始化中断向量表
3.配置系统时钟
4.调用 C 库函数_main 初始化用户堆栈,然后进入 main 函数。
在开始讲解之前,我们需要了解STM32的启动模式。
1 STM32的启动模式
首先要讲一下STM32的启动模式,因为启动模式决定了向量表的位置,STM32有三种启动模式:
主闪存存储器(Main Flash)启动:从STM32内置的Flash启动(0x0800 0000-0x0807 FFFF),一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。以0x08000000 对应的内存为例,则该块内存既可以通过0x00000000 操作也可以通过0x08000000 操作,且都是操作的同一块内存。
系统存储器(System Memory)启动:从系统存储器启动(0x1FFFF000 - 0x1FFF F7FF),这种模式启动的程序功能是由厂家设置的。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的ISP程序中,提供了串口下载程序的固件,可以通过这个ISP程序将用户程序下载到系统的Flash中。以0x1FFFFFF0对应的内存为例,则该块内存既可以通过0x00000000 操作也可以通过0x1FFFFFF0操作,且都是操作的同一块内存。
片上SRAM启动:从内置SRAM启动(0x2000 0000-0x3FFFFFFF),既然是SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。SRAM 只能通过0x20000000进行操作,与上述两者不同。从SRAM 启动时,需要在应用程序初始化代码中重新设置向量表的位置。
用户可以通过设置BOOT0的引脚电平状态,来选择复位后的启动模式。如下表所示。
Table 1-1 BOOT启动模式
启动模式只决定程序烧录的位置,加载完程序之后会有一个重映射(映射到0x00000000地址位置);真正产生复位信号的时候,CPU还是从开始位置执行。
值得注意的是STM32上电复位以后,代码区都是从0x00000000开始的,三种启动模式只是将各自存储空间的地址映射到0x00000000中。
2 STM32的启动文件分析
因为启动过程主要是由汇编完成的,因此STM32的启动的大部分内容都是在启动文件里。笔者的启动文件是startup_stm32h7s7xx.s。
在分析之前,先看看需要用到的汇编指令。
指令
作用
EQU
取符号名(类似C #define),同义词 *
AREA
指示编译器汇编一个新段(代码段或数据段)
SPACE
分配内存空间并填零。[标号] SPACE [表达式], 同义词 %
PRESERVE8
按8字节对齐
EXPORT
声明全局,可被外部文件使用,同义词 GLOBAL
DCD
以字为单位分配内存,要求4字节对齐且初始化该内存
PROC
定义子程序,与ENDP成对使用,表示子程序结束 同义词 FUNCTION
WEAK
编译器特性。弱定义,优先使用外部文件定义的标号。
IMPORT
声明标号来自外部文件,类似于C extern
B
跳转到一个标号
ALIGN
编译器指令,对指令或数据存放地址进行对齐(一般跟一个立即数,缺省为4字节)
END
EOF,文件结束
IF,ELSE,ENDIF
条件分支
2.1 堆栈定义
Stack栈
栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。当程序较大时,需要修改栈的大小,不然可能会出现的HardFault的错误。
第32行:表示开辟栈的大小为 0X00000400(1KB),EQU是伪指令,相当于C 中的 define。
第34行:开辟一段可读可写数据空间,ARER 伪指令表示下面将开始定义一个代码段或者数据段。此处是定义数据段。ARER 后面的关键字表示这个段的属性。段名为STACK,可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写,ALIGN=3,表示按照 8 字节对齐。
第35行:SPACE 用于分配大小等于 Stack_Size连续内存空间,单位为字节。
第36行: __initial_sp表示栈顶地址。栈是由高向低生长的。
2.Heap堆
堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆中。
开辟堆的大小为 0X00000200(512 字节),名字为 HEAP,NOINIT 即不初始化,可读可写,8字节对齐。__heap_base 表示对的起始地址,__heap_limit 表示堆的结束地址。
2.2 向量表
向量表是一个WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包含一张向量表,用于初始时的异常分配。
值得注意的是这里有个另类:0号类型并不是什么入口地址,而是给出了复位后 MSP 的初值,后面会具体讲解。
……
第55行:定义一块代码段,段名字是RESET,READONLY 表示只读。
第56-58行:使用EXPORT将3个标识符申明为可被外部引用,声明 __Vectors、__Vectors_End 和__Vectors_Size 具有全局属性。这几个变量在Keil分散加载时会用到。
第60行:__Vectors 表示向量表起始地址,DCD 表示分配 1 个 4 字节的空间。每行 DCD 都会生成一个 4 字节的二进制代码,中断向量表 存放的实际上是中断服务程序的入口地址。当异常(也即是中断事件)发生时,CPU 的中断系统会将相应的入口地址赋值给 PC 程序计数器,之后就开始执行中断服务程序。在60行之后,依次定义了中断服务程序的入口地址。
第235行:__Vectors_End 为向量表结束地址。
第237行:__Vectors_Size则是向量表的大小,向量表的大小是通过__Vectors 和__Vectors_End 相减得到的。
上述向量表可以在《Reference manual》中找到的,笔者这里只截取了部分。
2.3 复位程序
复位程序是系统上电后执行的第一个程序,复位程序也是中断程序,只是这个程序比较特殊,因此单独提出来讲解。
第244行:定义了一个服务程序,PROC表示程序的开始。
第245行:使用EXPORT将Reset_Handler申明为可被外部引用,后面WEAK表示弱定义,如果外部文件定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位程序可以由用户在其他文件重新实现,这种写法在HAL库中是很常见的。
第246-247行:表示该标号来自外部文件,SystemInit()是一个库函数,在system_stm32h7rsxx.c中定义的,__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,这个是由编译器完成的,该函数最终会调用我们自己写的main函数,从而进入C世界中。
第248行:这是一条汇编指令,表示从存储器中加载SystemInit到一个寄存器R0的地址中。R0~R3 寄存器通常用于函数入参出参或子程序调用。
第249行:汇编指令,表示跳转到寄存器R0的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
第250行:和248行是一个意思,表示从存储器中加载__main到一个寄存器R0的地址中。
第251行:和249稍微不同,这里跳转到至指定寄存器的地址后,不会返回。
第252行:和PROC是对应的,表示程序的结束。
值得注意的是,这里的__main和C语言中的main()不是一样东西,__main是C lib中的函数,也就是在Keil中自带的;而main()函数是C的入口,main()会被__main调用。
2.4 中断服务程序
我们平时要使用哪个中断,就需要编写相应的中断服务程序,只是启动文件把这些函数留出来了,但是内容都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置罢了。
这部分没啥好说的,和服务程序类似的,只需要注意‘B .’语句,B表示跳转,这里跳转到一个‘.’,即表示无线循环。
2.5 堆栈初始化
堆栈初始化是由一个IF条件来实现的,MICROLIB的定义与否决定了堆栈的初始化方式。
这个定义是在Options->Target中设置的。
如果没有定义__MICROLIB , 则会使用双段存储器模式,且声明了__user_initial_stackheap 具有全局属性,这需要开发者自己来初始化堆栈。
这部分也没啥讲的,需要注意的是,ALIGN表示对指令或者数据存放的地址进行对齐,缺省表示4字节对齐。
2.6 其他
第50行:PRESERVE8 用于指定当前文件的堆栈按照 8 字节对齐。
第51行:THUMB 表示后面指令兼容 THUMB 指令。现在 Cortex-M 系列的都使用 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超集。
3 STM32的启动流程实例分析
有了前面的分析,接下来就来具体看看STM32启动流程的具体内容。
3.1 初始化SP、PC、向量表
当系统复位后,处理器首先读取向量表中的前两个字(8 个字节),第一个字存入 MSP,第二个字为复位向量,也就是程序执行的起始地址。
这里通过J-Flash打开hex文件。
硬件这时自动从0x0800 0000位置处读取数据赋给栈指针SP,然后自动从0x0800 0004位置处读取数据赋给PC,完成了复位操作,SP= 0x0200 0400,PC = 0x0800 02C5。
初始化SP、PC紧接着就初始化向量表,如果感觉看HEX文件抽象,我们看看反汇编文件吧。
是不是更容易些,是不是和《Reference manual》中的向量表对应起来了。其实看反汇编文件更好理解STM32的启动流程,只是有些抽象。
生成反汇编的方法如下。
在KEIL的User选项中,如下图添加这两项:
fromelf --bin --output=STM32H7S78-DK_Appli.bin STM32H7S78-DK_Appli/STM32H7S78-DK_Appli.axf
fromelf --text -a -c --output=STM32H7S78-DK_Appli.dis STM32H7S78-DK_Appli/STM32H7S78-DK_Appli.axf
然后重新编译,即可得到二进制文件STM32H7S78-DK_Appli.bin(以后会分析)、反汇编文件STM32F429.dis。
如下图所示:
3.2 设置系统时钟
细心的朋友可能发现,PC= 0x0800 02C5这里表明MCU运行的是THUMB模式,最低位为1表示为THUMB状态。然后在反汇编文件中却是这样的:
当然也可通过硬件调试来确认上面的分析:
接下来就会进入SystemInit函数中。
SystemInit函数内容如下:
/**
* @brief Setup the microcontroller system.
* @retval None
*/
void SystemInit(void)
{
/* Configure the Vector Table location -------------------------------------*/
SCB->VTOR = INTVECT_START;
/* FPU settings ------------------------------------------------------------*/
#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
SCB->CPACR |= ((3UL << 20U)|(3UL << 22U)); /* set CP10 and CP11 Full Access */
#endif
}
这里主要初始化向量表。
3.3 初始化堆栈并进入main
执行指令LDR R0, =__main,然后就跳转到__main程序段运行,当然这里指标准库的__main函数。
最后就进入C文件的main函数中,至此,启动过程到此结束。
最后,总结下STM32 从flash的启动流程。
MCU上电后从0x0800 0000处读取栈顶地址并保存,然后从0x0800 0004读取中断向量表的起始地址,这就是复位程序的入口地址,接着跳转到复位程序入口处,初始向量表,然后设置时钟,设置堆栈,最后跳转到C空间的main函数,即进入用户程序。
-
发表了主题帖:
【STM32H7S78-DK】GPIO流水灯
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
1 GPIO工作原理
熟悉单片机的朋友都知道,学习的第一个例程就是流水灯,要想实现流水灯,首先必须了解GPIO的工作原理。GPIO的基本结构如图所示。
Figure 1-1 GPIO的基本结构
STM32 的 IO 口可以由软件配置成如下 8 种模式:
输入模式
浮空输入:浮空(floating)就是逻辑器件的输入引脚即不接高电平,也不接低电平。由于逻辑器件的内部结构,当它输入引脚悬空时,相当于该引脚接了高电平。一般实际运用时,引脚不建议悬空,易受干扰。 通俗讲就是让管脚什么都不接,浮空着。信号进入芯片内部后,既没有接上拉电阻也没有接下拉电阻,经由触发器输入。配置成这个模式后,用电压变量引脚电压为1点几伏,这是个不确定值。由于其输入阻抗比较大,一般把这种模式用于标准的通讯协议,比如IIC、USART的等。该模式是STM32复位之后的默认模式。
Figure 1-2 输入浮空模式
上拉输入:上拉就是把电位拉高,比如拉到Vcc。上拉就是将不确定的信号通过一个电阻嵌位在高电平,电阻同时起限流作用,弱强只是上拉电阻的阻值不同,没有什么严格区分。上拉输入就是信号进入芯片后加了一个上拉电阻,再经过施密特触发器转换成0、1信号,读取此时的引脚电平为高电平;
Figure 1-3 输入上拉模式
下拉输入:就是把电压拉低,拉到GND。与上拉原理相似。下拉输入就是信号进入芯片后加了一个下拉电阻,再经过施密特触发器转换成0、1信号,读取此时的引脚电平为低电平;
Figure 1-4 输入下拉模式
模拟输入:信号进入后不经过上拉电阻或者下拉电阻,关闭施密特触发器,经由另一线路把电压信号传送到片上外设模块。模拟输入是指传统方式的输入,数字输入是输入PCM数字信号,即0、1的二进制数字信号,通过数模转换,转换成模拟信号,经前级放大进入功率放大器,功率放大器还是模拟的。比如传送给ADC模块,由ADC采集电压信号。所以可以理解为模拟输入的信号是未经处理的信号,是原汁原味的信号。
Figure 1-5 模拟输入模式
输出模式
开漏输出:一般用在电平不匹配的场合,如需要输出5V的高电平。输出端相当于三极管的集电极,要得到高电平状态需要上拉电阻才行。适合于做电流型的驱动,其吸收电流的能力相对强(一般20mA以内)。
Figure 1-6 开漏输出模式
复用开漏输出:可以理解为GPIO口被用作第二功能时的配置情况(即并非作为通用IO口使用)。端口必须配置成复用开漏功能输出模式。
Figure 1-7 开漏复用功能
推挽式输出:可以输出高、低电平,连接数字器件;推挽结构一般是指两个三极管分别受两个互补信号的控制,总是在一个三极管导通的时候另一个截止。高低电平由IC的电源决定。推挽电路是两个参数相同的三极管或MOSFET,以推挽方式存在于电路中,各负责正负半周的波形放大任务,电路工作时,两只对称的功率开关管每次只有一个导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载抽取电流。推拉式输出级既提高电路的负载能力,又提高开关速度。
Figure 1-8 推挽式输出
推挽式复用输出
Figure 1-9 推挽式复用功能
2 I/O复用和重映射
2.1 I/O复用
STM32 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。当I/O端口被配置为复用功能时:
● 在开漏或推挽式配置中,输出缓冲器被打开
● 内置外设的信号驱动输出缓冲器(复用功能输出)
● 施密特触发输入被激活
● 弱上拉和下拉电阻被禁止
● 在每个APB2时钟周期,出现在I/O脚上的数据被采样到输入数据寄存器
● 开漏模式时,读输入数据寄存器时可得到I/O口状态
● 在推挽模式时,读输出数据寄存器时可得到最后一次写的值
Figure 2-1 复用功能配置
大家都知道,MCU都有串口,STM32H7S7L8H6H有好几个串口。我们可以查手册知道,串口 1 的引脚对应的 IO 为PA9、PA10。PA9、PA10 默认功能是 GPIO,所以当PA9、PA10 引脚作为串口 1 的 TX,RX引脚使用的时候,那就是端口复用。
2.2 I/O重映射
为了使不同器件封装的外设 IO 功能数量达到最优,可以把一些复用功能重新映射到其他一些引脚上。 STM32 中有很多内置外设的输入输出引脚都具有重映射(remap)的功能。 我们知道每个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的,为了让设计工程师可以更好地安排引脚的走向和功能,在 STM32 中引入了外设引脚重映射的概念,即一个外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚映射到其它的端口。
Table 2-1 USART1重映像
复用功能
USART1_REMAP = 0
USART1_REMAP = 1
USART1_TX
PA9
PB6
USART1_RX
PA10
PB7
从表中可以看出,默认情况下,串口 1 复用的时候的引脚位 PA9、PA10,同时我们可以将 TX 和 RX 重新映射到管脚 PB6 和 PB7 上面去。所以重映射我们同样要使能复用功能的时候讲解的 2 个时钟外,还要使能 AFIO 功能时钟,然后要调用重映射函数。
3 GPIO流水灯硬件电路分析
发光二极管是属于二极管的一种,具有二级管单向导电特性,即只有在正向电压(二极管的正极接正,负极接负)下才能导通发光。PO1引脚接发光二极管(LED1)的负极,所以PO1引脚输出低电平LED1亮,PO1引脚输出低电平LED1熄灭,,LED2/LED3/LED4同理。
Figure 3-1 LED电路图
值得注意的,不同的开发板,LED连接的GPIO一般是不同的,请注意修改。
4 GPIO流水灯寄存器分析
要想真正掌握一款单片机,分析寄存器是必不可少,但是对于STM32来再说,ST已经将寄存器操作封装成库函数,开发者只需要调用库函数即可,对于初学者来说,只需学会使用函数即可,对于没有基础的读者朋友就不必细究每个寄存器,当学到一定程度,再来一探究竟吧,笔者在这里只是给出GPIO的寄存配置相关配置表,在后面的章节也是如此。好了,继续进入正题吧。
每个GPIO端口都有一个32位模式寄存器 (GPIOx_MODER),一个32位输出类型寄存器 (GPIOx_ OTYPER),一个32位输出速度寄存器 (GPIOx_OSPEEDR),一个32位输出上拉/下拉寄存器 (GPIOx_PUPDR),两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),一个32位置位/ 复位寄存器(GPIOx_BSRR),一个32位锁定寄存器(GPIOx_LCKR),两个复用功能寄存器(GPIOx_AFRL和GPIOx_AFRH)。每个I/O端口位可以自由编程,I/O端口寄存器可通过字节( 8 位)、半字( 16 位)或字( 32 位)对 GPIO 寄存器进行访问。
点亮LED,基本步骤是:配置寄存器;控制寄存器。库开发只是将传统的配置方式编程函数,是的单片机开发变得简单方便快捷。
我们常用的IO端口寄存器只有几个:MODER、OTYPER、OSPEEDR、PUPDR、IDR、ODR。其中MODER、OTYPER、OSPEEDR、PUPDR控制着每个IO口的模式及输出速率。
Table 4-1 端口位配置表
Table 4-2 输出模式位
GPIO 端口模式寄存器 (GPIOx_MODER)描述如下图所示。
Figure 4-1 模式寄存器(GPIOx_MODER)
GPIO 端口输出类型寄存器 (GPIOx_OTYPER)如下图所示。
Figure 4-2 输出类型寄存器 (GPIOx_OTYPER)
GPIO 端口输出速度寄存器 (GPIOx_OSPEEDR)如下图所示。
Figure 4-3 输出速度寄存器 (GPIOx_OSPEEDR)
GPIO 端口上拉/下拉寄存器 (GPIOx_PUPDR) 如下图示所示。
Figure 4-4 上拉/下拉寄存器 (GPIOx_PUPDR)
5 GPIO 流水灯实现流程
笔者在上文已经分析了GPIO的原理及操作步骤,现在我们就来写代码吧。本书是用库来对STM32来开发的,这是本书的第一个实例,笔者为了读者比较直接配置寄存和库开发的区别,笔者在此用了两种方式进行开发,希望读者能自己体会两种方式的优劣。这里笔者会使用两种库:标准库和HAL库。
不管使用何种实现方式,其流程都是一样的。
GPIO是开发STM32最基本的配置,所以掌握GPIO的配置显得尤为重要。要实现流水灯,一般步骤可以总结为如下:
GPIO 时钟使能;
GPIO 端口模式设置;
初始化IO口;
编写处理函数;
6 GPIO 流水灯实现
6.1 STM32Cube新建工程
关于如何使用STM32Cube新建工程在前文已经讲解过了,这里直说配置GPIO部分内容。本文要实现流水灯,其实输出为初始化设置为高电平还是低电平都可以,因为流水灯需要不断反转。
1.首先进行系统配置,debug模式选择可以选择其他模式,默认选择Disable,Timebase Source选择SysTick,关于Timebase Source在后文会详细讲解。
Figure 6-1 系统配置
2.时钟配置,外部高速时钟。
Figure 6-2 时钟选择
STM32CubeMX配置时钟的界面非常方便,有很多提示(比如最大时钟),很多地方直接点击选择就行了。默认高速时钟是使用内部(HSI),而且CPU时钟配置的比较低。以我选择的STM32H7S,外部24M晶振为例(如下图)。配置系统时钟配置600MHz,APB1总线配置150MHz。
Figure 6-3 时钟配置
3. GPIO初始化配置
我们将PO1、PO5、PM2、PM3配置输出模式(高电平、低电平均可)、输出速率、上/下拉等,默认即可。
Figure 6-4 GPIO初始化
4.工程生成设置
Figure 6-5 工程配置
其他默认即可。最后点击“生成代码”即可生成代码。
6.2 GPIO流水灯实现
我们要实现流水灯,需要在主函数添加以下代码。
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
6.3 流水灯代码分析
在分析代码前,先看看流水灯编程流程:
1>使能GPIO端口时钟;
2>初始化GPIO引脚,即为GPIO初始化结构体赋值,并调用相应初始化函数完成初始化配置;
3>根据实际需求控制流水灯。
STM32H7S HAL库开发,代码比较多,因此,我们在看一个实际工程时,只需从主函数开始,好了,接下来,笔者就带领大家一步一步看看流水灯是怎么实现的。笔者先贴出主函数代码。
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* Enable the CPU Cache */
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* MCU Configuration--------------------------------------------------------*/
/* Update SystemCoreClock variable according to RCC registers values. */
SystemCoreClockUpdate();
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
注释部分不用管,我们只看代码。
1.复位所有硬件
HAL_Init()函数为复位所有硬件,这个就不细说了。
/**
* @brief Configure the time base source, NVIC and any required global low level hardware
* by calling the HAL_MspInit() callback function to be optionally defined in user file
* stm32h7rsxx_hal_msp.c.
*
* @note HAL_Init() function is called at the beginning of program after reset and before
* the clock configuration.
*
* @note In the default implementation the System Timer (Systick) is used as source of time base.
* The Systick configuration is based on HSI clock, as HSI is the clock
* used after a system Reset and the NVIC configuration is set to Priority group 4.
* Once done, time base tick starts incrementing: the tick variable counter is incremented
* each 1ms in the SysTick_Handler() interrupt handler.
*
* @retval HAL status
*/
HAL_StatusTypeDef HAL_Init(void)
{
HAL_StatusTypeDef status = HAL_OK;
/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* Use SysTick as time base source and configure 1ms tick */
if (HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
status = HAL_ERROR;
}
else
{
/* Init the low level hardware */
HAL_MspInit();
}
/* Return function status */
return status;
}
2.时钟初始化函数
SystemCoreClockUpdate ()函数用于时钟初始化,也就是通过STM32Cube实现。
时钟初始化函数如下:
/**
* @brief Update SystemCoreClock variable according to RCC registers values.
* The SystemCoreClock variable contains the core clock (sys_cpu_ck), it can
* be used by the user application to setup the SysTick timer or configure
* other parameters.
*
* @note Each time the core clock changes, this function must be called
* to update SystemCoreClock variable value. Otherwise, any configuration
* based on this variable will be incorrect.
*
* @note - The system frequency computed by this function is not the real
* frequency in the chip. It is calculated based on the predefined
* constant and the selected clock source:
*
* - If SYSCLK source is HSI, SystemCoreClock will contain the HSI_VALUE(*)
*
* - If SYSCLK source is CSI, SystemCoreClock will contain the CSI_VALUE(**)
*
* - If SYSCLK source is HSE, SystemCoreClock will contain the HSE_VALUE(***)
*
* - If SYSCLK source is PLL, SystemCoreClock will contain the HSI_VALUE(*)
* or CSI_VALUE(**) or HSE_VALUE(***) multiplied/divided by the PLL factors.
*
* (*) HSI_VALUE is a constant defined in stm32h7rsxx_hal.h file (default value
* 64 MHz) but the real value may vary depending on the variations
* in voltage and temperature.
*
* (**) CSI_VALUE is a constant defined in stm32h7rsxx_hal.h file (default value
* 4 MHz) but the real value may vary depending on the variations
* in voltage and temperature.
*
* (***) HSE_VALUE is a constant defined in stm32h7rsxx_hal.h file (default value
* 24 MHz), user has to ensure that HSE_VALUE is same as the real
* frequency of the crystal used. Otherwise, this function may
* have wrong result.
*
* - The result of this function could be not correct when using fractional
* value for HSE crystal.
*
* @retval None
*/
void SystemCoreClockUpdate(void)
{
uint32_t sysclk, hsivalue, pllsource, pllm, pllp, core_presc;
float_t pllfracn, pllvco;
/* Get SYSCLK source -------------------------------------------------------*/
switch (RCC->CFGR & RCC_CFGR_SWS)
{
case 0x00: /* HSI used as system clock source (default after reset) */
sysclk = (HSI_VALUE >> ((RCC->CR & RCC_CR_HSIDIV) >> RCC_CR_HSIDIV_Pos));
break;
case 0x08: /* CSI used as system clock source */
sysclk = CSI_VALUE;
break;
case 0x10: /* HSE used as system clock source */
sysclk = HSE_VALUE;
break;
case 0x18: /* PLL1 used as system clock source */
/* PLL1_VCO = (HSE_VALUE or HSI_VALUE or CSI_VALUE/ PLLM) * PLLN
SYSCLK = PLL1_VCO / PLL1R
*/
pllsource = (RCC->PLLCKSELR & RCC_PLLCKSELR_PLLSRC);
pllm = ((RCC->PLLCKSELR & RCC_PLLCKSELR_DIVM1) >> RCC_PLLCKSELR_DIVM1_Pos) ;
if ((RCC->PLLCFGR & RCC_PLLCFGR_PLL1FRACEN) != 0U)
{
pllfracn = (float_t)(uint32_t)(((RCC->PLL1FRACR & RCC_PLL1FRACR_FRACN)>> RCC_PLL1FRACR_FRACN_Pos));
}
else
{
pllfracn = (float_t)0U;
}
if (pllm != 0U)
{
switch (pllsource)
{
case 0x02: /* HSE used as PLL1 clock source */
pllvco = ((float_t)HSE_VALUE / (float_t)pllm) * ((float_t)(uint32_t)(RCC->PLL1DIVR1 & RCC_PLL1DIVR1_DIVN) + (pllfracn/(float_t)0x2000) +(float_t)1 );
break;
case 0x01: /* CSI used as PLL1 clock source */
pllvco = ((float_t)CSI_VALUE / (float_t)pllm) * ((float_t)(uint32_t)(RCC->PLL1DIVR1 & RCC_PLL1DIVR1_DIVN) + (pllfracn/(float_t)0x2000) +(float_t)1 );
break;
case 0x00: /* HSI used as PLL1 clock source */
default:
hsivalue = (HSI_VALUE >> ((RCC->CR & RCC_CR_HSIDIV) >> RCC_CR_HSIDIV_Pos));
pllvco = ( (float_t)hsivalue / (float_t)pllm) * ((float_t)(uint32_t)(RCC->PLL1DIVR1 & RCC_PLL1DIVR1_DIVN) + (pllfracn/(float_t)0x2000) +(float_t)1 );
break;
}
pllp = (((RCC->PLL1DIVR1 & RCC_PLL1DIVR1_DIVP) >> RCC_PLL1DIVR1_DIVP_Pos) + 1U ) ;
sysclk = (uint32_t)(float_t)(pllvco/(float_t)pllp);
}
else
{
sysclk = 0U;
}
break;
default: /* Unexpected, default to HSI used as system clock source (default after reset) */
sysclk = (HSI_VALUE >> ((RCC->CR & RCC_CR_HSIDIV) >> RCC_CR_HSIDIV_Pos));
break;
}
/* system clock frequency : CM7 CPU frequency */
core_presc = (RCC->CDCFGR & RCC_CDCFGR_CPRE);
if (core_presc >= 8U)
{
SystemCoreClock = (sysclk >> (core_presc - RCC_CDCFGR_CPRE_3 + 1U));
}
else
{
SystemCoreClock = sysclk;
}
}
RCC_TypeDef结构体类型定义时钟来源和系统时钟生成配置,本例程使用外部24MHz晶振,通过PLL锁相环后得到600M系统时钟。一般选择使能系统时钟、AHB、APB1和APB2总线时钟。
值得注意的是,这是使用的App部分工程,Boot工程的时钟配置为SystemClock_Config()。
3.GPIO初始化函数
MX_GPIO_Init()函数用于LED的GPIO初始化,代码如下:
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* USER CODE BEGIN MX_GPIO_Init_1 */
/* USER CODE END MX_GPIO_Init_1 */
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOM_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOO_CLK_ENABLE();
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOM, LED4_Pin|LED3_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOO, LED1_Pin|LED2_Pin, GPIO_PIN_RESET);
/*Configure GPIO pins : LED4_Pin LED3_Pin */
GPIO_InitStruct.Pin = LED4_Pin|LED3_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOM, &GPIO_InitStruct);
/*Configure GPIO pin : PD0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF8_UART4;
HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
/*Configure GPIO pins : LED1_Pin LED2_Pin */
GPIO_InitStruct.Pin = LED1_Pin|LED2_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOO, &GPIO_InitStruct);
/* USER CODE BEGIN MX_GPIO_Init_2 */
/* USER CODE END MX_GPIO_Init_2 */
}
以上就是LED的初始化函数,初始化GPIO有个重要的结构体GPIO_InitTypeDef,先看看该结构体。
/**
* @brief GPIO Init structure definition
*/
typedef struct
{
uint32_t Pin; /*!< Specifies the GPIO pins to be configured.
This parameter can be any value of [url=home.php?mod=space&uid=1064992]@ref[/url] GPIO_pins_define */
uint32_t Mode; /*!< Specifies the operating mode for the selected pins.
This parameter can be a value of @ref GPIO_mode_define */
uint32_t Pull; /*!< Specifies the Pull-up or Pull-Down activation for the selected pins.
This parameter can be a value of @ref GPIO_pull_define */
uint32_t Speed; /*!< Specifies the speed for the selected pins.
This parameter can be a value of @ref GPIO_speed_define */
uint32_t Alternate; /*!< Peripheral to be connected to the selected pins.
This parameter can be a value of @ref GPIO_Alternate_function_selection */
}GPIO_InitTypeDef;
Pin:引脚编号选择,一个GPIO有16个引脚可选,参数可选有:GPIO_PIN_0、….、GPIO_PIN_15和GPIO_PIN_ALL。很多时候我们可以使用或运算进行选择:GPIO_PIN_0| GPIO_PIN_4。
Mode:引脚模式选择,在前文笔者已经介绍了引脚有8中工作模式,根据外设选择相应模式。
Table 6-1 GPIO引脚工作模式
Pull:上下拉,用于输入模式,可选:GPIO_NOPULL不上下拉;GPIO_PULLUP:使能下拉;GPIO_PULLDOWN使能下拉;
Speed:引脚速度,可选:GPIO_SPEED_FREQ_LOW:低速(2MHz);中速(10MHz);高速(50MHz)。
在初始化代码中,还有一个重要的函数HAL_GPIO_WritePin()。HAL_GPIO_WritePin()函数为2个LED灯时钟初始化状态,这里设置为低电平,所以初始化状态2个LED都是暗的。
4.流水灯实现
在前文已经贴出了流水灯的相应代码。
while (1)
{
HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED3_GPIO_Port, LED3_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
HAL_GPIO_TogglePin(LED4_GPIO_Port, LED4_Pin);
/* Insert delay 200 ms */
HAL_Delay(200);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
HAL_GPIO_TogglePin函数用于GPIO电平反转,HAL_Delay用于延时,单位是毫秒,以此不断将LED1、LED2、LED3、LED4关灭,这就是实现流水灯的效果。
7 实验现象
将编译好的程序下载到板子中,可以看到四个LED灯不同地闪烁。
本章虽然用不同的方式实现了流水灯,也就是不断控制GPIO的高低电平,我想大家会有很多疑问,寄存器、表混库、HAL的区别是什么,流水灯是怎么一步步调用等等,后面的章节会一一解答,尽请期待!
- 2024-09-18
-
发表了主题帖:
【STM32H7S78-DK】基于MDK和STM32CubeMX的开发环境搭建
开发环境:
IDE:MKD 5.38a
STM32CubeMX: V6.12.0
开发板:STM32H7S78-DK开发板
MCU:STM32H7S7L8H6H
STM32的开发工具有很多,笔者习惯使用MDK开发,因此本文将基于MDK+STM32CubeMX搭建开发环境。
1 STM32H7S78-DK简介
STM32H7S78-DK探索套件是一个完整的演示和开发平,基于Arm® Cortex®‑M7内核的STM32H7S7L8H6H微控制器。STM32H7S78-DK探索套件包含丰富的硬件功能,可帮助用户评估许多外设,如USB Type-C® 、Octo‑SPI Flash存储器和Hexadeca‑SPI PSRAM设备、音频编解码器、数字麦克风、ADC、灵活的扩展连接器和用户按钮。凭借四个灵活的扩展连接器,可轻松实现无限制的扩展,适用于无线连接、模拟应用和传感器等特定应用。
Figure 1-1 STM32H7S78-DK探索套件
STM32H7S78-DK探索套件集成了面向STM32 MCU的STLINK-V3EC嵌入式在线调试器和编程器,配有USB虚拟通信端口桥以及全套MCU软件包。
STM32H7RS系列配备Cortex-M7内核,通过600 MHz bootflash MCU提供卓越的性能,非常适合高速实时应用。其外部存储器接口支持在线执行 (XiP) 应用,同时2.5D GPU可以最小的CPU负载支持最高WSVGA的分辨率。这款MCU提供ST-iRoT、存储器保护、加密和认证等各种高级安全功能,是一款安全极具性价比的选择,非常适合打造面向未来的应用。
STM32H7S7L8H6H微控制器具有三个I2C总线、六个SPI端口、三个USART端口、两个SDMMC端口、两个CAN端口、一个以太网端口、两个SAI端口、两个12位ADC、一个嵌入式降压转换器、两个Octo-SPI存储器接口、一个Hexadeca-SPI接口、支持功率传输的USB OTG HS端口、LCD-TFT控制器、灵活存储控制器 (FMC)、8至14位DCMI接口,支持JTAG和SWD调试。
Figure 1-2 STM32H7S7L8H6H简介
从上图可以看出,STM32H7S7L8H6H的性能和资源都是非常丰富,可玩性很强。
2 MDK环境准备
目前市面通用的MDK for ARM版本有Keil 4和Keil 5:使用Keil 5建议安装5.38以上版本。笔者的MDK是5.38a。
从MDK的官网可以下载得到MDK的安装包,然后安装即可,关于的MDK安装请看笔者的教程。
MDK安装教程:https://blog.csdn.net/bruceoxl/article/details/108548573
MDK下载地址:https://www.keil.com/download/product/
Figure 2-1 MDK ARM下载
安装完成后会自动打开,我们将其关闭。
接下来我们下载STM32H7S的软件支持包。
下载地址:https://www.keil.arm.com/packs/
Figure 2-2 STN32H7S Pack包下载
下载好后双击Keil.STM32H7RSxx_DFP.1.0.0.pack运行即可:
Figure 2-3 STN32H7S Pack包安装
点击[Next]即可安装完成。
Figure 2-4 STN32H7S Pack包安装完成
安装成功后,重新打开Keil,则可以在File->Device Database中出现STM32H7S7L8HxH的下拉选项,点击可以查看到相应的型号。
Figure 2-5 STM32H7S7L8HxH型号
3 STM32CubeMX安装
首先去ST官网下载STM32CubeMX。
下载地址:https://www.st.com.cn/zh/development-tools/stm32cubemx.html
更具自己的系统下载对应的版本,笔者使用的Windows 10系统,不管任何系统,除了初始化安装方式不一样,安装过程都是一样的。这里就不再赘述了。安装好后,还需要安装HAL库。
4 HAL库安装
HAL库可以在线安装也可以离线安装。
4.1 HAL库在线安装
打开安装好的 STM32CubeMX 软件, 进入库管理界面(Help -> Manage embedded...)。
会有一个软件列表, 勾选上你要安装的HAL库, 点击“Install Now” 直到安装成功。 如下图:
这种方式比较简单,需要联网在线下载,可能需要等待一段时间。如果刷新之后,可以看到最新版本,以及一些老版本HAL库。等待安装完成即可。
4.2 HAL库离线安装
当然还可以离线安装。
离线安装的前提是需要下载好离线包,方法有两种:1.从库管理界面直接导入离线包。2.直接解压离线包到指定路径下。
1.导入离线包
2.直接解压
先在Help -> Updater Settings设置HAL库路径
直接解压HAL库离线包,放在前面设置的库路径下。
5 新建工程
接下来从选择自己型号MCU,到生成代码并成功运行的创建过程。
1.打开STM32CubeMX,进入MCU选择器
进入选择器时会联网更新数据库,可能需要等待一下。
2.选择对应型号MCU
【注】第一次使用某单片机,在点击生成代码以后,如果没有相应的固件,STM32CubeMX软件会提示需要下载固件,点击确定即可在线下载。保持联网,在线安装很快,当然也可先安装HAL库,参见上一节内容。
3.时钟源选择
如果选择使用外部高速时钟(HSE),则需要在System Core中配置RCC(如下图)。如果使用默认内部时钟(HSI),这一步可以略过。
4.选择GPIO(LED)引脚
然后将对应的GPIO配置为输出模式。对于以上四个LED控制引脚,需要将Pin Context Assignment修改为Boot,User Label修改为和板子丝印对应的: LED1、LED2、LED3、LED4,如下图:
Pin Context Assignment修改为Boot。
5.配置时钟Clock
STM32CubeMX配置时钟的界面非常方便,有很多提示(比如最大时钟),很多地方直接点击选择就行了。默认高速时钟是使用内部(HSI),而且CPU时钟配置的比较低。外部晶振24M,将CPU Clocks修改为600。
6.工程管理
工程管理器的内容比较多,大部分都默认即可。
值得注意的是,如果想使用GCC或者其他编译工具,直接在‘Toolchain/IDE’选择即可,然后就可进行应用开发,不需要考虑工程搭建,非常方便。
7.生成代码
以上操作就基本配置完成了一个简单的示例工程,只需要点击“生成代码”即可。
8.添加代码
当然,以上生成的代码还不够完善,需要添加自己的应用代码。添加在主函数添加一下应用代码:
编译。这里使用Boot过程。
下载。
LED会不断闪烁。
- 2024-09-10
-
回复了主题帖:
测评入围名单:STM32H7R/S 高性能MCU 开启全新的创新机遇!纵享ST 2024高性能新品!
个人信息无误,确认可以完成评测计划
-
加入了学习《计算机组成与设计:RISC-V (浙江大学)》,观看 操作系统和虚拟存储
-
加入了学习《计算机组成与设计:RISC-V (浙江大学)》,观看 数据冒险
- 2024-08-30
-
发表了主题帖:
【匠芯创D133CBS】基于RT-Thread的DHT11温湿度获取
1 DHT11驱动架构
RT-Thread包含DHT11驱动,这里就没有去造轮子了,但是作为学习者,还是要去了解DHT11的原理及具体实现。这部分内容在笔者博客的STM32系列的外设篇已详细阐述,下面就DHT11驱动在RTT中的实现做个总结。
DHT11是采用单总线通讯的传感器,有的设备没有硬件单总线,DHT11的支持包采用 GPIO 模拟单总线时序。DHT11 的一次完整读时序需要 20ms,时间过长,故无法使用关中断或者关调度的方式实现独占 CPU 以保证时序完整正确。因此可能出现读取数据失败的情况。
DHT11的典型电路如下图所示。
Figure 1-1 DHT11典型电路
DATA 用于微处理器与 DHT11之间的通讯和同步,采用单总线数据格式,一次通讯时间4ms左右,数据分小数部分和整数部分,具体格式在下面说明,当前小数部分用于以后扩展,现读出为零.操作流程如下:
一次完整的数据传输为40bit,高位先出。
数据格式:8bit湿度整数数据+8bit湿度小数数据
+8bi温度整数数据+8bit温度小数数据
+8bit校验和
数据传送正确时校验和数据等于“8bit湿度整数数据+8bit湿度小数数据+8bi温度整数数据+8bit温度小数数据” 所得结果的末8位。
用户MCU发送一次开始信号后,DHT11从低功耗模式转换到高速模式,等待主机开始信号结束后,DHT11发送响应信号,送出40bit的数据,并触发一次信号采集用户可选择读取部分数据。从模式下,DHT11接收到开始信号触发一次温湿度采集,如果没有接收到主机发送开始信号,DHT11不会主动进行温湿度采集.采集数据后转换到低速模式。通讯过程如下图所示:
Figure 1-2 DHT11通讯时序图
熟悉RT-Thread系统都知道,RT-Thread将各个模块进行抽象,为上层提供统一的操作接口,依次提高上层代码的可重用性,自然也提高了应用开发效率。RT-Thread的驱动框架:
Figure 1-3 RT-Thread的驱动框架
应用程序通过 I/O 设备管理接口获得正确的设备驱动,然后通过这个设备驱动与底层 I/O 硬件设备进行数据(或控制)交互。
I/O设备模型框架位于硬件和应用程序之间,共分成三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层。
I/O设备管理层实现了对设备驱动程序的封装。应用程序通过 I/O 设备层提供的标准接口访问底层设备,设备驱动程序的升级、更替不会对上层应用产生影响。这种方式使得设备的硬件操作相关的代码能够独立于应用程序而存在,双方只需关注各自的功能实现,从而降低了代码的耦合性、复杂性,提高了系统的可靠性。
设备驱动框架层是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现。
设备驱动层是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册 I/O 设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到 I/O 设备管理器中。
更加详细的内容请参看RT-Thread官方手册。
这里主要讲解Sensor驱动,RT-Thread将各个Sensor进行抽象,将不同厂商的Sensor合并为Sensor设备,从而提高了代码的重用性,提高开发效率。既然是总结,这里就只说重点,先看看DHT11设备驱动的时序图:
Figure 1-4 DHT11设备驱动的时序图
传感器数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、FIFO 模式。在使用的时候,这 3 种模式只能选其一,若传感器的打开参数 oflags 没有指定使用中断模式或者 FIFO 模式,则默认使用轮询模式。
oflags 参数支持下列参数:
#define RT_DEVICE_FLAG_RDONLY 0x001 /* 标准设备的只读模式,对应传感器的轮询模式 */
#define RT_DEVICE_FLAG_INT_RX 0x100 /* 中断接收模式 */
#define RT_DEVICE_FLAG_FIFO_RX 0x200 /* FIFO 接收模式 */
DHT11设备比较简单,采用的是轮询模式,其设备注册函数如下:
result = rt_hw_sensor_register(sensor, name, RT_DEVICE_FLAG_RDONLY, RT_NULL);
硬件初始化如下:
static int rt_hw_dht11_port(void)
{
struct rt_sensor_config cfg;
cfg.intf.user_data = (void *)DHT11_DATA_PIN;
rt_hw_dht11_init("dht11", &cfg);
return RT_EOK;
}
INIT_COMPONENT_EXPORT(rt_hw_dht11_port);
注意INIT_COMPONENT_EXPORT表示组件初始化,初始化顺序为4。
好了,DHT11就讲到这里了。
自动初始化:https://blog.bruceou.cn/2021/02/5-api-pi-auto-initialization/603/
Sensor:https://www.rt-thread.org/document/site/programming-manual/device/sensor/sensor/
2 DHT11获取温湿度
RT-Thread提供了DHT11的驱动软件包,配置如下:
Figure 2-1 添加DHT11驱动
DHT11默认使用的 GPIO是可以修改的,在 dht11_sample.c 中修改以下代码:
#define DHT11_DATA_PIN 3//PA3
笔者这里使用PA3。
既然驱动有现成的,那么只需要写个相应的应用代码即可。应用部分的代码如下:
#include "dht11_thread.h"
/* Modify this pin according to the actual wiring situation */
#define DHT11_DATA_PIN 3
rt_dht11 dht11;
static void read_temp_entry(void *parameter)
{
rt_device_t dev = RT_NULL;
struct rt_sensor_data sensor_data;
rt_size_t res;
rt_uint8_t get_data_freq = 1; /* 1Hz */
dev = rt_device_find("temp_dht11");
if (dev == RT_NULL)
{
return;
}
if (rt_device_open(dev, RT_DEVICE_FLAG_RDWR) != RT_EOK)
{
rt_kprintf("open device failed!\n");
return;
}
rt_device_control(dev, RT_SENSOR_CTRL_SET_ODR, (void *)(&get_data_freq));
while (1)
{
res = rt_device_read(dev, 0, &sensor_data, 1);
if (res != 1)
{
rt_kprintf("read data failed! result is %d\n", res);
rt_device_close(dev);
return;
}
else
{
if (sensor_data.data.temp >= 0)
{
dht11.temp = (sensor_data.data.temp & 0xffff) >> 0; // get temp
dht11.humi = (sensor_data.data.temp & 0xffff0000) >> 16; // get humi
rt_kprintf("temp:%d, humi:%d\n" ,dht11.temp, dht11.humi);
}
}
rt_thread_delay(1000);
}
}
static int dht11_thread(void)
{
rt_thread_t dht11_thread;
dht11_thread = rt_thread_create("dht_tem",
read_temp_entry,
RT_NULL,
1024,
RT_THREAD_PRIORITY_MAX / 2,
20);
if (dht11_thread != RT_NULL)
{
rt_thread_startup(dht11_thread);
}
return RT_EOK;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(dht11_thread, dht11 thread);
static int rt_hw_dht11_port(void)
{
struct rt_sensor_config cfg;
cfg.intf.user_data = (void *)DHT11_DATA_PIN;
rt_hw_dht11_init("dht11", &cfg);
return RT_EOK;
}
MSH_CMD_EXPORT(dht11_read_temp_sample, dht11 device sample);
另外,值得注意的是DHT11的读取需要高精度延时函数。在sensor_dallas_dht11.c中有默认的函数,这里函数值针对Cortex-M处理器的。这里要注释,D133CBS的处理器是RISC-V的,对应Cortex-M 的SysTick 定时器是MTIMER,是一个64位的计数器。其延时函数如下:
/**
\brief get CORE timer counter high value
\return CORE timer counter value.
*/
__STATIC_INLINE uint32_t csi_coret_get_valueh(void)
{
return (CORET->MTIME >> 32) & 0xFFFFFFFF;
}
u64 aic_get_ticks(void)
{
return (((u64)csi_coret_get_valueh() << 32U) | csi_coret_get_value());
}
int32_t drv_get_sys_freq(void)
{
#ifdef QEMU_RUN
return g_system_clock;
#else
return CLOCK_4M;
#endif
}
void aic_udelay(u32 us)
{
u64 start = aic_get_ticks();
u32 cnt = us * (drv_get_sys_freq() / 1000000U);
while (1) {
u64 cur = aic_get_ticks();
if (start > cur) {
if ((start - cur) >= cnt)
break;
} else {
if (cur - start >= cnt)
break;
}
}
}
void rt_hw_us_delay(rt_uint32_t us)
{
aic_udelay(us);
}
3 实验现象
程序编译下载到板卡后,会在串口中每 1s 打印一次温湿度数据。
- 2024-08-27
-
发表了主题帖:
【匠芯创D133CBS】基于RT-Thread的I2C使用(OLED)
开发环境:
开发板:D133CBV-QFN88-V1-2 开发板
MCU:D133CBS
D133CBS有3路硬件I2C,但是笔者开发的是软件I2C,默认使用的I2C1的接口,对于应用而言,软件和硬件都是一样的。
1 RT-Thread 的I2C简介
I2C(Inter Integrated Circuit)总线是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。I2C 总线传输数据时只需两根信号线,一根是双向数据线 SDA(serial data),另一根是双向时钟线 SCL(serial clock)。
I2C 总线允许同时有多个主设备存在,每个连接到总线上的器件都有唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址,同一时刻只允许有一个主设备。如下图所示。
Figure 1-1 I2C 总线
一般情况下 MCU 的 I2C 器件都是作为主机和从机通讯,在 RT-Thread 中将 I2C 主机虚拟为 I2C总线设备,I2C 从机通过 I2C 设备接口和 I2C 总线通讯,相关接口如下所示:
Table 1-1 I2C相关接口
函数
描述
rt_device_find()
根据 I2C 总线设备名称查找设备获取设备句柄
rt_i2c_transfer()
传输数据
关于I2C更详细的内容,请参看官方手册:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/i2c/i2c
2 I2C硬件接口
本文将使用I2C点亮OELD,值得注意的是OLED接口一般有两种:I2C(4线)和SPI(7线或8线)。笔者这里使用的是I2C0。
I2C0的硬件接口如下。
I2C0所用的GPIO对应:PD0->SCL, PD1->SDA
3 I2C使用实例
I2C使用起来很简单。这里的从设备使用OLED。
1.首先根据 I2C 设备名称查找 I2C 名称,获取设备句柄,然后初始化设备。
2.其次就是与设备进行通信。
接下来就是配置相应的驱动。
首先配置软件I2C。
Figure 2-1 I2C配置
使能I2C接口,一般默认即可。
接下来就是配置OLED驱动。
Figure 2-3 配置OLED驱动
笔者这里使用的SSD1306的软件包。
Figure 2-4 SSD1306参数配置
以上参数默认即可,笔者这里还使用了SSD1306的sample。
最后保存更新软件包即可。
编译下载,然后在终端输入“ssd1306_TestAll”即可进行测试。
正常情况下,OLED将会点亮。
完整演示视频如下:
[localvideo]0c31188417305f3be15de3f85e2a49a4[/localvideo]
- 2024-08-18
-
发表了主题帖:
【匠芯创D133CBS】D133CBS 新建工程
1 RT-Thread 的UART简介
UART和其他设备一样,应用程序通过统一的设备管理接口来访问串口硬件,相关接口如下所示:
函数
描述
rt_device_find()
查找设备
rt_device_open()
打开设备
rt_device_read()
读取数据
rt_device_write()
写入数据
rt_device_control()
控制设备
rt_device_set_rx_indicate()
设置接收回调函数
rt_device_set_tx_complete()
设置发送完成回调函数
rt_device_close()
关闭设备
关于API的详细描述请参看官网手册:
https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/device/uart/uart_v1/uart
UART的主要步骤如下所示:
1.首先查找串口设备获取设备句柄。
2.配置串口参数。
3.初始化回调函数发送使用的信号量,然后以读写及中断接收方式打开串口设备。
4.设置串口设备的接收回调函数,之后发送字符串,并创建读取数据线程。
运行序列图如下图所示:
上述方式是基于中断实现的,当然也可使用DMA,目前UART驱动还不支持,待以后完善吧。
2 新建工程
首先在luban-lite\application\rt-thread目录下新建一个新的过程,这里命名为uart。
然后在luban-lite\target\configs目录下新建d13x_demo88-nor_rt-thread_uart_defconfig文件。
修改d13x_demo88-nor_rt-thread_uart_defconfig文件中的配置和应用名。
然后添加应用代码。核心代码如下:
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#define SAMPLE_UART_NAME "uart2" /* 串口设备名称 */
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT; /* 初始化配置参数 */
/* 用于接收消息的信号量 */
static struct rt_semaphore rx_sem;
static rt_device_t serial;
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] uart_input //接收数据回调函数
* @param dev
* size
* @retval RT_EOK
*/
static rt_err_t uart_input(rt_device_t dev, rt_size_t size)
{
/* 串口接收到数据后产生中断,调用此回调函数,然后发送接收信号量 */
rt_sem_release(&rx_sem);
return RT_EOK;
}
/**
* @brief serial_thread_entry
* @param parameter
* @retval None
*/
static void serial_thread_entry(void *parameter)
{
char ch;
while (1)
{
/* 从串口读取一个字节的数据,没有读取到则等待接收信号量 */
while (rt_device_read(serial, -1, &ch, 1) != 1)
{
/* 阻塞等待接收信号量,等到信号量后再次读取数据 */
rt_sem_take(&rx_sem, RT_WAITING_FOREVER);
}
/* 读取到的数据输出 */
rt_kprintf("%c",ch);
}
}
/**
* @brief thread_serial
* @param None
* @retval ret
*/
int thread_serial(void)
{
rt_err_t ret = RT_EOK;
char uart_name[RT_NAME_MAX];
char str[] = "hello RT-Thread!\r\n";
rt_strncpy(uart_name, SAMPLE_UART_NAME, RT_NAME_MAX);
/* 查找系统中的串口设备 */
serial = rt_device_find(uart_name);
if (!serial)
{
rt_kprintf("find %s failed!\n", uart_name);
return RT_ERROR;
}
/* 修改串口配置参数 */
config.baud_rate = BAUD_RATE_115200; //修改波特率为 115200
config.data_bits = DATA_BITS_8; //数据位 8
config.stop_bits = STOP_BITS_1; //停止位 1
config.bufsz = 64; //修改缓冲区 buff size 为 128
config.parity = PARITY_NONE; //无奇偶校验位
/* 控制串口设备。通过控制接口传入命令控制字,与控制参数 */
rt_device_control(serial, RT_DEVICE_CTRL_CONFIG, &config);
/* 初始化信号量 */
rt_sem_init(&rx_sem, "rx_sem", 0, RT_IPC_FLAG_FIFO);
/* 以中断接收及轮询发送模式打开串口设备 */
rt_device_open(serial, RT_DEVICE_FLAG_INT_RX);
/* 设置接收回调函数 */
rt_device_set_rx_indicate(serial, uart_input);
/* 发送字符串 */
rt_device_write(serial, 0, str, (sizeof(str) - 1));
/* 创建 serial 线程 */
rt_thread_t thread = rt_thread_create("serial", serial_thread_entry, RT_NULL, 1024, 25, 10);
/* 创建成功则启动线程 */
if (thread != RT_NULL)
{
rt_thread_startup(thread);
}
else
{
ret = RT_ERROR;
}
return ret;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(thread_serial, uart device sample);
3 编译 Luban-Lite
scons --list-def //查看有多少配置
D13x 针对不同的封装共开发了两套开发板供用户参考。
笔者这里使用的是 D133CBV-V1.0的封装。
方案的配置对应的是 target/config/d13x_demo88-nor_rt-thread_helloworld 工程
编译时选择 d13x_demo88-nor_rt-thread_helloworld
生成固件:output/d13x_demo88-nor_rt-thread_helloworld/images/d13x_demo88-nor_v1.0.0.img
scons --apply-def=12 //选择 12 号配置
scons --menuconfig//配置
在 BSP 目录下打开 Env,然后在使用 scons --menuconfig命令打开配置界面。menuconfig 常用快捷键如图所示:
配置UART2。
scons //编译
成功编译打印信息如下。
编译后的固件名称为d13x_demo88-nor_v1.0.0.img
4 固件烧写与调试
使用AiBrun下载固件,然后打开串口调试助手,打印信息如下。
调试信息如下:
从以上打印信息可以看出,串口2已经使能,然后使用MSH命令‘thread_serial’即可使能串口线程。
使能串口线程后,串口2将打印‘hello RT-Thread’,用户也可通过串口2发送数据到开发板,发送信息后,调试终端即可看到串口2发送的数据。