最近在学习STM32F4系列的RTC时钟系统,同时也在学习4位7段数码管显示驱动。而市面上很多3641BS数码管都是集成了74HC595移位寄存器芯片的PCB板,故网上大多基于STM32驱动该型号数码管的驱动都是基于五引脚(VCC, DIO, RCLK, SCLK, GND)设计的。但我手头只有最原始的12引脚版本,因此在这篇博客里我会讲解最原始版本的,也当作是一个备忘录和对RTC时钟唤醒中断配置以及3641BS工作原理的认识吧。
1. 实验原理
如下图,四位七段数码管有12个引脚。除了显示"abcdefg"的7段和显示小数点的"h"之外,还有4个引脚D1、D2、D3和D4用作"位"引脚。当四个数码管的"位"引脚为高电平时,相应的数码管被点亮。 四个数码管的显示原理是连续扫描D1、D2、D3、D4,然后相应的八段管依次亮起。由于点亮速度很快,小于人眼分辨的时间差值极限,所以看起来就像四个数码管同时显示。
对于STM32实时时钟(RTC)来说,它是一个独立的BCD定时器/计数器,提供日历时钟、两个可编程报警中断和一个具有中断功能的可编程定期唤醒标志,还包含一个自动唤醒单元来管理低功耗模式。 两个32位的寄存器包含了秒、分、小时(12或24小时格式)、星期、日期、月份和年份的二进制十进制(BCD)格式。此外,亚秒值以二进制格式提供。 系统可以自动将每月的天数偏移到28、29(闰年)、30和31天。还可提供夏令时补偿。其他32位寄存器包含可编程的报警亚秒、秒、分钟、小时、星期和日期。此外,晶体精度的偏差可以通过数字校准功能进行补偿。在上电复位后,所有的RTC寄存器都受到保护,防止可能的异常写入。无论设备状态如何(工作模式、低功耗模式或重启),只要电源电压保持在工作范围内,RTC就不会停止工作。
?
2.实验材料
STM32F407ZG开发板一块 面包板及配套连接线(公对母) 四位共阳极数码管3641BS(最原始版本,不包含任何集成芯片) 4个220Ω限流电阻
3.实验电路连接图
?
数码管的a~h引脚分别接到单片机的PC0~PC7引脚,d0,d1,d2,d3引脚分别接到单片机的PF0~PF3引脚,并且在中间串联220Ω的限流电阻,以免永久损坏数码管LED。
4. CUBEMX配置以及代码编写
首先将A~H八端初始化为高电平,D0~D3初始化为低电平,每个引脚上下拉模式为浮空
?RCC系统时钟源的HSE, LSE都调为晶振源
?
打开RTC时钟源和日历,启用内部时钟唤醒,把时间调为24小时制,存储模式用二进制(这点很重要!!!等会读取时间的时候也要以二进制形式读取,否则数字会有偏差),设置好时分秒以及年月日、星期几,根据自己需要是否关闭夏令时,唤醒定时器的时钟输入设置为1Hz(即RTC_WAKEUPCLOCK_RTCCLK / 16),其他参数默认就好
?
?接下来配置唤醒中断以及优先级。在这里,我将中断优先级分组设置为2位抢占优先级、2位响应优先级,同时将唤醒中断激活并将抢占、响应优先级均设置为2,以保证不会干扰其他系统底层中断的运行。最后勾选生成中断请求处理函数和HAL调用处理函数。
?
为了在中断处理函数中通过printf输出日期和其他更详细的信息,需要启用串口。在SYS中将Debug模式调为Serial Wire,并且启用USART1,设置为异步传输模式,波特率115200,其他默认就好。
确认无误后生成基于HAL库的KEIL5 MDK工程文件
接下来到了实现关键代码部分的时候了!?
首先,为了保证RTC断电后时间不丢失、同时每次上电后时间日期不被重复设置,需要在RTC 后备寄存器中留下标记值,以标记是否为第一次设置时间。但后备寄存器需要启用写权限,于是在main()函数HAL_Init()初始化完成后插入:
HAL_PWR_EnableBkUpAccess();
然后在rtc.c文件中的MX_RTC_Init()函数初始化时间部分前后加上判断后备寄存器是否有标记的逻辑,若没有标记则初始化并写寄存器。
/* USER CODE BEGIN Check_RTC_BKUP */
if(HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0) != 0x5050) { // Add this line
/* USER CODE END Check_RTC_BKUP */
sTime.Hours = 14;
sTime.Minutes = 10;
sTime.Seconds = 12;
// ....
/* USER CODE BEGIN RTC_Init 2 */
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0x5050); // Add this line
} // Add this line
/* USER CODE END RTC_Init 2 */
/** Enable the WakeUp
*/
if (HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 0, RTC_WAKEUPCLOCK_CK_SPRE_16BITS) != HAL_OK)
{
Error_Handler();
}
添加中断回调处理函数
int finale = 0;
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc) {
char buf[40];
RTC_TimeTypeDef RTC_TimeStruct;
RTC_DateTypeDef RTC_DateStruct;
HAL_RTC_GetTime(hrtc,&RTC_TimeStruct,RTC_FORMAT_BIN);
sprintf((char*)buf,"Time: %02d:%02d:%02d",RTC_TimeStruct.Hours,RTC_TimeStruct.Minutes,RTC_TimeStruct.Seconds);
printf("%s\r\n",buf);
HAL_RTC_GetDate(hrtc,&RTC_DateStruct,RTC_FORMAT_BIN);
sprintf((char*)buf,"Date: 20%02d-%02d-%02d Day %d",RTC_DateStruct.Year,RTC_DateStruct.Month,RTC_DateStruct.Date,RTC_DateStruct.WeekDay);
printf("%s\r\n",buf);
finale = RTC_TimeStruct.Hours * 100 + RTC_TimeStruct.Minutes;
}
并且在头文件中添加显示小时和分钟的外部int值,供主函数while循环中扫描显示在数码管上
extern int finale;
最后在usart.c和usart.h中添加fputc重载函数,以保证将printf函数输出设备从控制台重定向到串口1
#include <stdio.h>
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart1,(uint8_t*)&ch, 1, 0xFFFF);
return ch;
}
可能有同学就在这里问了:为什么我加了printf函数以后的程序编译烧写进板子,其系统不能启动,出现死机的情况?这是因为在嵌入式的编程中HAL库并没有对IO函数的底层实现(比如_sys_exit(), _sys_open(), _sys_close()这些函数),使得设备运行时会进入软件中断BAEB处,即进入了半主机模式,这时就需要__use_no_semihosting_swi这个声明,同时用空壳实现这些函数(dummy函数),使程序遇到这些文件操作函数时不停在此中断处。将以下代码粘贴进新建的源文件并加入工程即可:
/* USER CODE BEGIN 0 */
#include <time.h>
#pragma import(__use_no_semihosting_swi)
#pragma import(_main_redirection)
const char __stdin_name[150];
const char __stdout_name[150];
const char __stderr_name[150];
typedef int FILEHANDLE;
void _sys_exit(int status)
{
while(1);
}
FILEHANDLE _sys_open(const char *name, int openmode)
{
return 0;
}
int _sys_close(FILEHANDLE fh)
{
return 0;
}
int _sys_write(FILEHANDLE fh, const unsigned char *buf, unsigned len, int mode)
{
return 0;
}
int _sys_read(FILEHANDLE fh, unsigned char*buf, unsigned len, int mode)
{
return 0;
}
int _sys_istty(FILEHANDLE fh)
{
return 0;
}
int _sys_seek(FILEHANDLE fh, long pos)
{
return 0;
}
int _sys_ensure(FILEHANDLE fh)
{
return 0;
}
long _sys_flen(FILEHANDLE fh)
{
return 0;
}
int _sys_tmpnam(char *name, int fileno, unsigned maxlength)
{
return 0;
}
void _ttywrch(int ch)
{
}
time_t time(time_t *t)
{
return 0;
}
int remove(const char *filename)
{
return 0;
}
char *_sys_command_string(char *cmd, int len)
{
return 0;
}
clock_t clock(void)
{
return 0;
}
/* USER CODE END 0 */
接下来的重中之重,就是实现四位七段数码管的驱动程序了。首先打出码表和对应的引脚数组
int dat[10][8] = {
// Last bit is reserved for H (i.e. point)
{0, 0, 0, 0, 0, 0, 1, 1}, //0
{1, 0, 0, 1, 1, 1, 1, 1}, //1
{0, 0, 1, 0, 0, 1, 0, 1}, //2
{0, 0, 0, 0, 1, 1, 0, 1}, //3
{1, 0, 0, 1, 1, 0, 0, 1}, //4
{0, 1, 0, 0, 1, 0, 0, 1}, //5
{0, 1, 0, 0, 0, 0, 0, 1}, //6
{0, 0, 0, 1, 1, 1, 1, 1}, //7
{0, 0, 0, 0, 0, 0, 0, 1}, //8
{0, 0, 0, 0, 1, 0, 0, 1} //9
};
uint16_t dbit[4] = {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3}; //使能位引脚(D0~D3), 高电平有效
uint16_t tbit[8] = {GPIO_PIN_0, GPIO_PIN_1, GPIO_PIN_2, GPIO_PIN_3, GPIO_PIN_4, GPIO_PIN_5, GPIO_PIN_6, GPIO_PIN_7}; //控制点亮哪根数码管(a~h), 低电平有效
然后是显示单个数字
void DisplayInt(int n, int nb, int punto) {
int i = 0;
if(n < 10) {
for(i = 0; i < 8; i++) {
HAL_GPIO_WritePin(GPIOC, tbit[i],
(nb == 1 && i == 7) ? (punto ? 0 : 1) : dat[n][i]);
}
}
}
接下来是四位数字同时显示,punto形参表示是否显示小时和分钟之间的小数点
void delay_us(uint32_t nus) {
uint32_t ticks;
uint32_t told,tnow,tcnt=0;
uint32_t reload=SysTick->LOAD;
uint32_t freq = HAL_RCC_GetSysClockFreq();
ticks=nus * freq / 1e6;
told=SysTick->VAL;
while(1) {
tnow=SysTick->VAL;
if(tnow!=told) {
if(tnow<told)tcnt+=told-tnow;
else tcnt+=reload-tnow+told;
told=tnow;
if(tcnt>=ticks) break;
}
}
}
void DisplayNumber(int x, int punto) {
int nb = 0;
int display_arr[5] = {0};
int bit_base = 1000;
for(nb = 0; nb < 4; nb++) {
if(x / bit_base != 0) {
display_arr[nb] = x/bit_base;
x = x%bit_base;
} else {
display_arr[nb] = 0;
}
bit_base = bit_base / 10;
}
for(nb = 0; nb < 4; nb++) {
DisplayInt(display_arr[nb], nb, punto);
HAL_GPIO_WritePin(GPIOF, dbit[nb], GPIO_PIN_SET); // 4位数字轮流显示200us后熄灭,形成循环,小于人眼分辨时间,看上去就是连续的
delay_us(200);
HAL_GPIO_WritePin(GPIOF, dbit[nb], GPIO_PIN_RESET);
}
}
最后是显示小时和分中的总函数,要保持一直显示而不熄灭,必须将DisplayNumber()置入该函数的定时循环之中,单位是毫秒,以下是代码:
void DisHHMM(int x, uint32_t ms, int punto) {
uint32_t tickstart = HAL_GetTick();
uint32_t wait = ms;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
while((HAL_GetTick() - tickstart) < wait)
{
DisplayNumber(x, punto);
}
}
至此,基础函数部分已经全部实现完毕,可以在main()函数中测试了。
int main(void)
{
// ...
MX_GPIO_Init();
MX_USART1_UART_Init();
MX_RTC_Init();
// ...
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(finale != 0) {
DisHHMM(finale, 500, 1);
DisHHMM(finale, 500, 0);
}
}
/* USER CODE END 3 */
}
5. 实验结果
?
6. 参考文章?
1.?stm32半主机模式 - chenguan - 博客园
2.?How to drive a 7 segment display directly on Raspberry Pi in Python – RasPi.TV
|