概述
應用中經常會有使用單片機進行模數轉換的需求。PY32F003 具有 1 個 12 位的模擬數字轉換器(ADC),今天我們一起來使用一下這個 ADC。
數據手冊中對 ADC 簡介如下。
SAR ADC:逐次逼近式 ADC,原理參見“參考鏈接:什么是SAR ADC? - 知乎”。12位采樣值的最大值4095。數據手冊上標明的最大可用通道數量是 8 個外部通道,但對照 PY32F003F18P 的管腳復用表,如果應用中還要使用 GPIO,LED,定時器 和 UART 的話,可使用的外部 ADC 通道數最多不超過 6 個。對比于 PY32F003F18P 的 20 腳封裝和低廉的芯片價格,這樣的 MCU 可以在應用中采樣 6 個外部模擬量通道也是相當可觀的數量了。
PY32F003 可以在不使用外部晶振的情況下完成數模轉換,但其采樣精度還需要驗證。今天先嘗試著把 ADC 的功能跑通先。
實現代碼
參考在 STM32F103 上實現 ADC 的思路,在 PY32F003 上完成一下看。大致的步驟如下:
- 為 ADC1 指定 GPIO 管腳,并設置其復用功能
- 對 ADC1 進行初始化
- 在主循環中進行采樣和打印輸出
在 main.h 中增加和 ADC 相關的函數聲明
/** ----------------------------------------------------------------------------
* @name : void ADC_Init(void)
* @brief : ADC 初始化
* @param : [in] None
* @retval : [out] void
* @remark :
*** ----------------------------------------------------------------------------
*/
void ADC_Init(void);/** ----------------------------------------------------------------------------
* @name : HAL_StatusTypeDef ADC_Sample(char * sampleResult)
* @brief : 獲取 ADC 的采樣結果,結果存放在 sampleResult 字符串中
* @param : [in] None
* @retval : [out] HAL_HandleTypeDef. 操作成功返回 HAL_OK, 錯誤返回錯誤碼。
* @remark : sampleResult 是格式化的字符串,需要解析
*** ----------------------------------------------------------------------------
*/
HAL_StatusTypeDef ADC_Sample(char * sampleResult);
在 app_adc.c 文件中實現函數功能
在 Application/User 組增加 app_adc.c 文件,完整代碼如下。
/********************************************************************************* @file app_adc.c* @brief Application level Analog-Digital Conveter codes.******************************************************************************* @attention** Copyright (c) 2023 CuteModem Intelligence.* All rights reserved.** This software is licensed under terms that can be found in the LICENSE file* in the root directory of this software component.* If no LICENSE file comes with this software, it is provided AS-IS.********************************************************************************/#include "main.h"ADC_HandleTypeDef hadc;
uint32_t adc_value[3];/********************************************************************************************************
* @name : HAL_StatusTypeDef ADC_Sample(char * sampleResult)
* @brief : 獲取 ADC 的采樣結果,結果存放在 sampleResult 字符串中
* @param : [in] None
* @retval : [out] HAL_HandleTypeDef. 操作成功返回 HAL_OK, 錯誤返回錯誤碼。
* @remark : sampleResult 是格式化的字符串,需要解析
********************************************************************************************************/
HAL_StatusTypeDef ADC_Sample(char * sampleResult)
{uint8_t i=0;if(HAL_ADCEx_Calibration_Start(&hadc) != HAL_OK) return HAL_ERROR;HAL_ADC_Start(&hadc); //開始采樣for (i = 0; i < 3; i++){HAL_ADC_PollForConversion(&hadc, 10000); //等待ADC轉換adc_value[i] = HAL_ADC_GetValue(&hadc); //獲取AD值}#if(1)// excel formatsprintf(sampleResult, "%d,%d,%d",(uint16_t)adc_value[0],(uint16_t)adc_value[1],(uint16_t)adc_value[2]);
#else// JSON formatsprintf(sampleResult, "[{\"C\":0,\"D\":%d}"",{\"C\":1,\"D\":%d}"",{\"C\":5,\"D\":%d}""]",(uint16_t)adc_value[0],(uint16_t)adc_value[1],(uint16_t)adc_value[2]);
#endif HAL_ADC_Stop(&hadc); // 停止采樣return HAL_OK;
}void ADC_Init(void)
{ADC_ChannelConfTypeDef sConfig = {0};__HAL_RCC_ADC_FORCE_RESET();__HAL_RCC_ADC_RELEASE_RESET();__HAL_RCC_ADC_CLK_ENABLE();hadc.Instance = ADC1;if (HAL_ADCEx_Calibration_Start(&hadc) != HAL_OK) //AD校準Error_Handler();/* Configure global features of the ADC1 */hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1; //ADC_CLOCK_SYNC_PCLK_DIV2/4,分頻系數hadc.Init.Resolution = ADC_RESOLUTION_12B; //設置采樣位數hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; //右對齊hadc.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD; //掃描方向設置hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; //ADC_EOC_SINGLE_CONV:單次采樣 ; ADC_EOC_SEQ_CONV:序列采樣hadc.Init.LowPowerAutoWait = ENABLE; //ENABLE:讀取ADC值后,開始下一次轉換; DISABLE:直接轉換hadc.Init.ContinuousConvMode = DISABLE; //ENABLE:連續模式, DISABLE:單次模式hadc.Init.DiscontinuousConvMode = DISABLE; //非連續轉換模式設置hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; //觸發模式設置hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; //外部觸發沿設置hadc.Init.DMAContinuousRequests = DISABLE; //DMA連續模式設置hadc.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; //ADC_OVR_DATA_OVERWRITTEN:過載時覆蓋,ADC_OVR_DATA_PRESERVED:保留舊值if (HAL_ADC_Init(&hadc) != HAL_OK) Error_Handler(); //初始化ADC/* Configure selected ADC channels */sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_RANK_CHANNEL_NUMBER; sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) Error_Handler();sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_RANK_CHANNEL_NUMBER; sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) Error_Handler();sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = ADC_RANK_CHANNEL_NUMBER; sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5; if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) Error_Handler();
}
在 app_adc.c 中定義了業務所需的變量,功能函數也在一個 .c 文件中全部實現。這樣做是參考了面向對象的編程模式,遵循代碼/變量和功能解耦的原則,ADC 所需的全局變量都在 app_adc.c 中定義,main.c 中就不用再引用 ADC 相關的變量,也不用關心實現的細節了。唯一的接口就是 ADC_Sample() 函數的 sampleResult,sampleResult 定義為一個字符串具有很好的通用性,并隱藏了實現的細節。這里例子中被注釋掉的 JSON 串返回結果的代碼,在實際應用中,在上一層的業務邏輯處理是很方便的。當然 MCU 編程,一般不會采用 JSON 這種富文本的格式,這里只作為一種示例。
ADC_Sample() 函數中每次采樣之前都對 ADC 進行了校準,校準完成后開始采樣,采樣完畢后停止 ADC。
在 py32f0xx_hal_msp.c 文件中指定 GPIO 及其復用功能
/*** -----------------------------------------------------------------------* @name : void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)* @brief : 初始化 ADC 相關 MSP* @param : [in] *hadc, ADC handler pointer* @retval : void* @remark :* -----------------------------------------------------------------------
*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{GPIO_InitTypeDef GPIO_InitStruct = {0};/*=============PA0/1/4初始化=============*/if (hadc->Instance == ADC1){__HAL_RCC_ADC_CLK_ENABLE(); /* Peripheral clock enable */__HAL_RCC_GPIOA_CLK_ENABLE(); /*ADC GPIO ConfigurationPA0 ------> ADC_IN0PA1 ------> ADC_IN1PA4 ------> ADC_IN5*/GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_4; // 指定 PA0/1/4GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; // 設置為模擬端口GPIO_InitStruct.Pull = GPIO_PULLDOWN; // 下拉:無輸入時采樣值接近零HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 執行初始化}
}
按照廠家例程的文件組織,所有的 HAL_xxx_MspInit() 集中在 py32_f0xx_hal_msp.c 文件中,由于在 ADC_Init() 函數中調用了 HAL_ADC_Init() 函數,要調用 HAL_ADC_MspInit(),這個函數在 HAL 庫中的原型是 weak 類型的,并且是一個空函數,因此需要在實用中重寫。
當然,把 HAL_ADC_MspInit() 函數在 app_adc.c 文件中實現也是可以的。
修改 DEBUG 口的管腳映射
PY32F003 ADC1 的通道 0/1/5 復用了 PA0/1/4,之前的實驗中,PA1/0 被用作了 DEBUG 口 UART2,和 ADC1 的通道是沖突的,所以需要把 DEBUG 口對應的管腳挪走。查了數據手冊,AF4 組的 PA2/3 可以用作 UART2,修改 UART_Config() 如下。
除了修改管腳映射以外,中斷優先級等的不做修改。
HAL_StatusTypeDef USART_Config(void)
{// Using PA2/PA3 (TX/RX)HAL_StatusTypeDef conf_res = HAL_OK;GPIO_InitTypeDef GPIO_InitStruct;gUartInited = 0; //====================// USART2初始化//====================__HAL_RCC_USART2_CLK_ENABLE();__HAL_RCC_GPIOA_CLK_ENABLE();UartHandle.Instance = USART2;UartHandle.Init.BaudRate = 115200;UartHandle.Init.WordLength = UART_WORDLENGTH_8B;UartHandle.Init.StopBits = UART_STOPBITS_1;UartHandle.Init.Parity = UART_PARITY_NONE;UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;UartHandle.Init.Mode = UART_MODE_TX_RX;conf_res = HAL_UART_Init(&UartHandle);if(conf_res != HAL_OK) return conf_res;/**USART2 GPIO ConfigurationPA2 ------> USART2_TXPA3 ------> USART2_RX*/GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3;GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;GPIO_InitStruct.Pull = GPIO_PULLUP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;GPIO_InitStruct.Alternate = GPIO_AF4_USART2;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);HAL_NVIC_SetPriority(USART2_IRQn, 0, 3); // 使能NVICHAL_NVIC_EnableIRQ(USART2_IRQn); // 使能USART2中斷gUartInited = 1;return conf_res;
}
在 main.c 的主循環中采樣
int main(void)
{HAL_Init(); // systick初始化SystemClock_Config(); // 配置系統時鐘GPIO_Config();if(USART_Config() != HAL_OK) Error_Handler(); printf("[SYS_INIT] Debug port initilaized.\r\n");ADC_Init();printf("[SYS_INIT] ADC initilaized.\r\n");printf("\r\n+---------------------------------------+""\r\n| PY32F003 MCU is ready. |""\r\n+---------------------------------------+""\r\n 10 digits sent to you! ""\r\n+---------------------------------------+""\r\n");if (DBG_UART_Start() != HAL_OK) Error_Handler();char sres[64]={0};uint8_t sIndex = 0;while (1){ BSP_LED_Toggle(LED3);if(sIndex % 2 == 0){if(ADC_Sample(sres) == HAL_OK){printf("%s\r\n", sres);}else{printf("Sample error.\r\n");}}sIndex ++;}HAL_Delay(500);
}
代碼中,主循環每 0.5s 翻轉一次 LED,每 1s 采樣一次。
實驗結果
初次跑通
按照上述步驟編寫好代碼,編譯燒錄,在 XCOM 上得到的結果如圖。初次運行,PA0/1/4 出于懸空狀態,得到的采樣值是隨機的。
注意到在 HAL_ADC_MspInt() 函數中,將 PA0/1/4 這三個管腳的 PULL 屬性都設置成了 PULLDOWN,本想著即使懸空的話仍可得到接近 0 的采樣值。但實驗結果中,PA0 的懸空狀態采樣值仍在 1480 多的值,折合成電壓為
? 1480/4096*3.3 = 1.192V
這個值挺高的,而 PA1/4 管腳換算得到的電壓值分別為 0.661/0.524V,這兩個值也不低。這說明 PY32F003 的內部下拉應該是“弱下拉”——或許,在 HAL_ADC_Init() 函數中又對這幾個管腳做了什么配置?這個問題留著以后關注。
基于此,在實際項目中用到 PY32F003 進行 ADC 時,在信號管腳接入前,要使用一個(或一組)運放做一下電壓跟隨才好。
采樣時長
在 HAL_ADC_ConfigChannel() 中,設置了采樣周期均為 71.5,加上轉換的耗費 12.5 周期,合計84 個時鐘周期,計算得到采樣時間為 3.5us 一次,也挺快了了。
對 GND 和 VCC 的采樣值
將 PA0 接地,然后再觀察其采樣值,得到了全“0”的采樣結果。
將 PA0 接 3.3V 管腳,50次采樣得到的平均值是 4087.22,換算得到 3.293V,也還好。
PULLUP 還是 PULLDOWN,還是 NOPULL?
把 PA0/1/4 都設置為內部上拉/下拉/無上下拉狀態時,PA0 接地,測得 PA1/4 的采用值分別是:
PULLUP:2.159/2.191V,PULLDOWN:0.242/0.322V,NOPULL:1.990/3.061V
PA1和PA4的特性略有不同。
PA0 得到的采樣值均為0,這說明管腳的 PULL 被初始化的狀態不會對采樣的測量值產生影響。
在 PA0 接 VCC 時,不論其 PULL 屬性如何,對采樣值也沒有影響。
總結
- 根據廠家例程移植,跑通 ADC 的輪詢式采樣是比較簡單的。如果熟悉對 STM32 的 ADC 配置,可以照搬 STM32 的步驟。
- 分配 ADC1 的采樣通道時,要把開發板默認的 UART2 管腳和 ADC1 的采樣通道管腳錯開。
- 當某一管腳配置為模擬信號時,其管腳的 PULL 屬性對測量結果無影響。
- 實用中,ADC1 的采樣輸入管腳最好使用運放做一個電壓跟隨器。
- 0~VCC 中間值的采樣精度如何,尚未驗證,留待后續實驗完成。
后續還會繼續嘗試使用 DMA 的 ADC,敬請期待。
謬誤之處,懇請指正。