sunsili 发表于 2023-8-2 08:40:39

STM32H5测评 | ADC采集和设置测试、串口通讯测试、TCP/IP UDP通...

本帖最后由 sunsili 于 2024-1-11 16:26 编辑

STM32H5测评 | ADC采集和设置测试、串口通讯测试、TCP/IP UDP通讯测试来源:EEWORLD论坛网友 bigbat 版权归原作者所有

ADC采集和设置测试、串口通讯测试要测试STM32H5的ADC,首先参考一下例程 ADC_SingleConversion_TriggerSW_IT,这是 cube 自带的例程,打开 cube 先看一下设置,例程使用了引脚 PC0,这个脚和板子的 A1 相连接。STM32H5 有两个 ADC 核心,设置为ADC1和CH10,引脚信号为单端信号 Single-ended。
引脚设置关键是时钟速率 Clock Prescaler 和 Resolution,速率为4分频,分辨率为12,需要中断开启。转换组设置 ADC_Regular_ConversionMode 为软件开启,而不是使用硬件联动触发的方式。/* Private variables ---------------------------------------------------------*/
ADC_HandleTypeDef hadc1;

/* USER CODE BEGIN PV */

/* Variables for ADC conversion data */
__IO uint16_t uhADCxConvertedData = VAR_CONVERTED_DATA_INIT_VALUE;; /* ADC group regular conversion data */

/* Variables for ADC conversion data computation to physical values */
uint16_t uhADCxConvertedData_Voltage_mVolt = 0;/* Value of voltage calculated from ADC conversion data (unit: mV) */

/* Variable to report status of ADC group regular unitary conversion          */
/*0: ADC group regular unitary conversion is not completed                  */
/*1: ADC group regular unitary conversion is completed                      */
/*2: ADC group regular unitary conversion has not been started yet          */
/*   (initial state)                                                      */
__IO uint8_t ubAdcGrpRegularUnitaryConvStatus = 2; /* Variable set into ADC interruption callback */

/* USER CODE END PV */
程序定义了一个 ADC 的转换变量 uhADCxConvertedData,注意:这里必须声明为 "__IO uint16_t",这个是为了让编译器不去优化该变量,直接去地址寄存器取值,在高速 MCU 中很关键,尤其在开启 ICACHE 的状态下。还有一个状态变量 ubAdcGrpRegularUnitaryConvStatus,用来做转换标准。转换程序的演示程序/* Perform ADC calibration *///校准开始
if (HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED) != HAL_OK)
{
   /* Calibration Error */
   Error_Handler();
}

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
   /* Start ADC group regular conversion */
   //转换开始
   if (HAL_ADC_Start_IT(&hadc1) != HAL_OK)
   {
   /* Error: ADC conversion start could not be performed */
   Error_Handler();
   }

   /* For this example purpose, wait until conversion is done */
   //等待转换结束,出结果
   while (ubAdcGrpRegularUnitaryConvStatus != 1);

   /* Reset status variable of ADC group regular unitary conversion */
   ubAdcGrpRegularUnitaryConvStatus = 0;

   /* Toggle LED at each ADC conversion */
   BSP_LED_On(LED1);
   HAL_Delay(LED_BLINK_SLOW);
   BSP_LED_Off(LED1);
   HAL_Delay(LED_BLINK_SLOW);

   /* Note: ADC group regular conversions data are stored into array         */
   /*       "uhADCxConvertedData"                                          */
   /*       (for debug: see variable content into watch window).             */

   /* Note: ADC conversion data are computed to physical values            */
   /*       into array "uhADCxConvertedData_Voltage_mVolt" using             */
   /*       ADC LL driver helper macro "__LL_ADC_CALC_DATA_TO_VOLTAGE()"   */
   /*       (for debug: see variable content with debugger)                  */
   /*       in IRQ handler callback function.                              */

   /* USER CODE END WHILE */

   /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
过程很简单,先初始化 ADC,在测试前先校准 ADC,然后开始转换 ADC 测量,等待 ADC 转换结束,这个程序用了中断程序。/**
* @brief This function handles ADC1 global interrupt.
*/
void ADC1_IRQHandler(void)
{
/* USER CODE BEGIN ADC1_IRQn 0 */

/* Customize process using LL interface to improve the performance          */
/* (exhaustive feature management not handled).                           */

/* ########## Starting from this point HAL API must not be used ########### */

/* Check whether ADC group regular end of unitary conversion caused         */
/* the ADC interruption.                                                    */
if(LL_ADC_IsActiveFlag_EOC(ADC1) != 0)
{
   /* Clear flag ADC group regular end of unitary conversion */
   LL_ADC_ClearFlag_EOC(ADC1);

   /* Call interruption treatment function */
   AdcGrpRegularUnitaryConvComplete_Callback();
}

/* Check whether ADC group regular overrun caused the ADC interruption */
if(LL_ADC_IsActiveFlag_OVR(ADC1) != 0)
{
   /* Clear flag ADC group regular overrun */
   LL_ADC_ClearFlag_OVR(ADC1);

   /* Call interruption treatment function */
   AdcGrpRegularOverrunError_Callback();
}

/* USER CODE END ADC1_IRQn 0 */
/* USER CODE BEGIN ADC1_IRQn 1 */

/* USER CODE END ADC1_IRQn 1 */
}



中断处理也很简单,首先判断是不是 ADC1 的"测量规则组"转换是否结束是则清除,完成后调用 AdcGrpRegularUnitaryConvComplete_Callback 函数,该函数中对主函数中的标志变量 ubAdcGrpRegularUnitaryConvStatus 进行设置。判断是否是 ADC1 中断转换完成标志,是则清除。/**
* @briefADC group regular end of unitary conversion interruption callback
* @retval None
*/void AdcGrpRegularUnitaryConvComplete_Callback()
{
/* Retrieve ADC conversion data */
uhADCxConvertedData = LL_ADC_REG_ReadConversionData32(ADC1);

/* Computation of ADC conversions raw data to physical values         */
/* using helper macro.                                                */
uhADCxConvertedData_Voltage_mVolt = __LL_ADC_CALC_DATA_TO_VOLTAGE(VDDA_APPLI, uhADCxConvertedData, LL_ADC_RESOLUTION_12B);

/* Update status variable of ADC unitary conversion                     */
ubAdcGrpRegularUnitaryConvStatus = 1;
}

/**
* @briefADC group regular overrun interruption callback
* @note   This function is executed when ADC group regular
*         overrun error occurs.
* @retval None
*/
void AdcGrpRegularOverrunError_Callback(void)
{
/* Note: Disable ADC interruption that caused this error before entering in
          infinite loop below. */

/* In case of error due to overrun: Disable ADC group regular overrun interruption */
LL_ADC_DisableIT_OVR(ADC1);

/* Error reporting */
Error_Handler();
}

例程中没有输出变量,接下来的任务就是通过串口输出 ADC1 的值了。


根据板子的设计,将串口输出到 USART3_PD9,USART3_PD8。通过新建立的项目完成硬件配置,主要是将 ADC 的函数拷贝到项目中。/**
* @brief USART3 Initialization Function
* @param None
* @retval None
*/static void MX_USART3_UART_Init(void)
{

/* USER CODE BEGIN USART3_Init 0 */

/* USER CODE END USART3_Init 0 */

/* USER CODE BEGIN USART3_Init 1 */

/* USER CODE END USART3_Init 1 */
huart3.Instance = USART3;
huart3.Init.BaudRate = 115200;
huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE;
huart3.Init.Mode = UART_MODE_TX_RX;
huart3.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart3.Init.OverSampling = UART_OVERSAMPLING_16;
huart3.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart3.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart3.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart3) != HAL_OK)
{
   Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart3, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
   Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart3, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
   Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&huart3) != HAL_OK)
{
   Error_Handler();
}
/* USER CODE BEGIN USART3_Init 2 */

/* USER CODE END USART3_Init 2 */

}

将例程拷贝到例程中,然后是 printf 函数的设置,main.c 中包含 #include <stdio.h>#if defined(__ICCARM__)
/* New definition from EWARM V9, compatible with EWARM8 */
int iar_fputc(int ch);
#define PUTCHAR_PROTOTYPE int putchar(int ch)
#elif defined ( __CC_ARM ) || defined(__ARMCC_VERSION)
/* ARM Compiler 5/6*/
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#elif defined(__GNUC__)
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#endif /* __ICCARM__ */
还有就是声明 PUTCHAR_PROTOTYPE 宏。
/* USER CODE BEGIN 4 */
/**
* @briefRetargets the C library printf function to the USART.
* @paramNone
* @retval None
*/
PUTCHAR_PROTOTYPE
{
/* Place your implementation of fputc here */
/* e.g. write a character to the USART1 and Loop until the end of transmission */
HAL_UART_Transmit(&huart3, (uint8_t *)&ch, 1, 0xFFFF);

return ch;
}
/* USER CODE END 4 */
还需要一个 keil 的设置 MicroLib。


完成这些设置后就可以使用printf函数了。在主函数中,printf("mVolt=%d \n",uhADCxConvertedData_Voltage_mVolt);
while (1)
{
   /* USER CODE END WHILE */
/* Start ADC group regular conversion */
   if (HAL_ADC_Start_IT(&hadc1) != HAL_OK)
   {
   /* Error: ADC conversion start could not be performed */
   Error_Handler();
   }

   /* For this example purpose, wait until conversion is done */
   while (ubAdcGrpRegularUnitaryConvStatus != 1);
   printf("mVolt=%d \n",uhADCxConvertedData_Voltage_mVolt);
   /* Reset status variable of ADC group regular unitary conversion */
   ubAdcGrpRegularUnitaryConvStatus = 0;
HAL_Delay(500);
   /* USER CODE BEGIN 3 */
}
下面是输出的结果
测试中我使用了我的一个宝贝,信号发生器。ADC 的性能和以往的ST差不多。
TCP/IP UDP 通讯测试

我的项目在以前的应用主要是 FreeRTOS+lwIP 的组合,这个组合能用但不好用,其中问题也较多。例如,断线重连问题,从检测到重连的时间较长,往往中断50秒或更久,希望找到一个靠谱的 TCP/IP 协议栈。STM32H563ZI 板卡有 ThreadX 和 NETX 的组合支持,我们一起来测试一下。
首先从已有例程开始,选择 Nx_SNTP_Client,该例程是 NTP 服务器对时的应用。这个应用非常实用,在与实时时钟的相关应用中,多数都需要校时任务。首先打开 Nx_SNTP_Client.ioc 文件。
可以看到应用支持,ThreadX 和 NETX 的应用,应用层支持 DNS 和 SNTP 应用,相比以前使用过lwIP的DNS,步骤简洁很多。底层支持 DHCP、ICMP、TCP,比 lwIP 的 DHCP 稳定快捷,lwIP 的 DHCP 应用有时会出现长时间无法获得IP、需不停检测和重试的情况。硬件接口设置就很简单了接口选 RMII,PHY 设置 LAN8742,MAC 地址 00:80:E1:00:00:00 (这是测试地址,应用中最好修改一下)硬件开启全局中断。还有串口设置,还有 printf 设置,注意编译时选择 microLIB 库,要不无法收到串口信息。下面来分析应用,Main.C 基本上不需要动,主要是硬件程序化和 MX_ThreadX_Init() 函数。/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* 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_ICACHE_Init();
MX_ETH_Init();
MX_USART3_UART_Init();
MX_RTC_Init();
/* USER CODE BEGIN 2 */
/* USER CODE END 2 */
MX_ThreadX_Init();
/* We should never get here as control is now taken by the scheduler */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

可以看到程序非常简洁,微软的建议是将硬件的初始化尽可能的放在 main.C 中进行,这里放入 printf 的设置。不要放入太多的逻辑代码,应用的初始化尽可能在 tx_application_define 函数中进行。这里的初始化过程被封装到 status = MX_NetXDuo_Init(memory_ptr) 中了,在这个 MX_NetXDuo_Init 函数中在进行线程的初始化。主要有三个线程和一个回调函数。/* Private function prototypes -----------------------------------------------*/
static VOID App_Main_Thread_Entry (ULONG thread_input);
static VOID ip_address_change_notify_callback(NX_IP *ip_instance, VOID *ptr);
/* USER CODE BEGIN PFP */
static VOID App_SNTP_Thread_Entry(ULONG thread_input);
static VOID App_Link_Thread_Entry(ULONG thread_input);

对我启发较大的是 App_Link_Thread_Entry 线程,这个线程会定期检测网络物理连接。
接下来是 App_Main_Thread_Entry 线程,这个线程主要是完成联网的准备工作。还有就是对 App_Link_Thread_Entry 线程数据的初始化。/* Exported macro ------------------------------------------------------------*/
/* USER CODE BEGIN EM */
#define PRINT_IP_ADDRESS(addr)    do { \
             printf("STM32 %s: %lu.%lu.%lu.%lu \n", #addr, \
             (addr >> 24) & 0xff,                        \
             (addr >> 16) & 0xff,                        \
             (addr >> 8) & 0xff,                           \
             (addr & 0xff));                               \
             } while(0)

主要的工作线程是 App_SNTP_Thread_Entry,这个线程完成整个过程。/**
* @briefSNTP thread entry.
* @param thread_input: ULONG user argument used by the thread entry
* @retval none
*/
/* Define the client thread.*/
static void App_SNTP_Thread_Entry(ULONG info)
{
UINT ret;
RtcHandle.Instance = RTC;
ULONGseconds, fraction;
ULONGevents = 0;
UINT   server_status;
NXD_ADDRESS sntp_server_ip;
NX_PARAMETER_NOT_USED(info);
sntp_server_ip.nxd_ip_version = 4;
/* Create a DNS client */
ret = dns_create(&DnsClient);
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
/* Look up SNTP Server address. */
ret = nx_dns_host_by_name_get(&DnsClient, (UCHAR *)SNTP_SERVER_NAME,
                              &sntp_server_ip.nxd_ip_address.v4, NX_APP_DEFAULT_TIMEOUT);
    /* Check for error. */
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
   /* Create the SNTP Client */
ret =nx_sntp_client_create(&SntpClient, &NetXDuoEthIpInstance, iface_index, &NxAppPool, NULL, kiss_of_death_handler, NULL);
/* Check for error. */
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
/* Setup time update callback function. */
   nx_sntp_client_set_time_update_notify(&SntpClient, time_update_callback);
/* Use the IPv4 service to set up the Client and set the IPv4 SNTP server. */
ret = nx_sntp_client_initialize_unicast(&SntpClient, sntp_server_ip.nxd_ip_address.v4);
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
/* Run whichever service the client is configured for. */
ret = nx_sntp_client_run_unicast(&SntpClient);
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
else
{
    PRINT_CNX_SUCC();
}
/* Wait for a server update event. */
tx_event_flags_get(&SntpFlags, SNTP_UPDATE_EVENT, TX_OR_CLEAR, &events, PERIODIC_CHECK_INTERVAL);
if (events == SNTP_UPDATE_EVENT)
{
    /* Check for valid SNTP server status. */
    ret = nx_sntp_client_receiving_updates(&SntpClient, &server_status);
    if ((ret != NX_SUCCESS) || (server_status == NX_FALSE))
    {
      /* We do not have a valid update. */
      Error_Handler();
    }
    /* We have a valid update.Get the SNTP Client time.*/
    ret = nx_sntp_client_get_local_time_extended(&SntpClient, &seconds, &fraction, NX_NULL, 0);
    ret = nx_sntp_client_utility_display_date_time(&SntpClient,buffer,64);
    if (ret != NX_SUCCESS)
    {
      printf("Internal error with getting local time 0x%x\n", ret);
      Error_Handler();
    }
    else
    {
      printf("\nSNTP update :\n");
      printf("%s\n\n",buffer);
    }
}
else
{
    Error_Handler();
}
/* Set Current time from SNTP TO RTC */
rtc_time_update(&SntpClient);
/* We can stop the SNTP service if for example we think the SNTP server has stopped sending updates */
ret = nx_sntp_client_stop(&SntpClient);
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
/* When done with the SNTP Client, we delete it */
ret = nx_sntp_client_delete(&SntpClient);
if (ret != NX_SUCCESS)
{
    Error_Handler();
}
/* Toggling LED after a success Time update */
while(1)
{
    /* Display RTC time each second*/
    display_rtc_time(&RtcHandle);
    HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    /* Delay for 1s */
    tx_thread_sleep(100);
}
}

这个程序目的是完成 SNTP 获取,设置到操作系统参数和 RTC 设置中,就可以不断地获取时间了。可以看到时间显示出来了。但这里需要注意一个“坑”,就是发现时间好像和计算机显示的时间对不上,因为这个时间与UTC时间在不同时区,随即找到以下程序,/* EPOCH_TIME_DIFF is equivalent to 70 years in sec
calculated with www.epochconverter.com/date-difference
This constant is used to delete difference between :
Epoch converter (referenced to 1970) and SNTP (referenced to 1900) */
time_t timestamp = client_ptr->nx_sntp_current_server_time_message.receive_time.seconds - EPOCH_TIME_DIFF;

将程序修改成:/* EPOCH_TIME_DIFF is equivalent to 70 years in sec
calculated with www.epochconverter.com/date-difference
This constant is used to delete difference between :
Epoch converter (referenced to 1970) and SNTP (referenced to 1900) */
time_t timestamp = client_ptr->nx_sntp_current_server_time_message.receive_time.seconds - EPOCH_TIME_DIFF + 28800U;
这样转换成 UTC 0800 时区时间。
这次时间匹配成功。


后记


这个程序经长达3小时测试,未发现任何中断现象。反复的ping单片机地址,TTL 值一直很稳定,相比 lwIP 程序改善很多。
页: [1]
查看完整版本: STM32H5测评 | ADC采集和设置测试、串口通讯测试、TCP/IP UDP通...