在Raspberry Pi上向Windows IoT添加丢失的实时时钟(译文)
By S.F.
本文链接 https://www.kyfws.com/news/adding-the-missing-real-time-clock-to-windows-iot/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 13 分钟阅读 - 6373 个词 阅读量 0在Raspberry Pi上向Windows IoT添加丢失的实时时钟(译文)
原文地址:https://www.codeproject.com/Articles/1113626/Adding-the-Missing-Real-Time-Clock-to-Windows-IoT
原文作者:Joel Ivory Johnson
译文由本站翻译
前言
Raspberry Pi上的Windows IoT尚未对硬件实时时钟提供本机支持.我已经创建了一种解决方案,以使Windows IoT在启动时从RTC初始化其时钟,这样就无需修改其他解决方案即可直接从RTC读取数据.
介绍
除其他设备外,Windows IoT在Raspberry Pi上运行.尽管Raspberry Pi没有内置的Windows实时时钟,但它将与互联网联系以获取当前时间并使用伪造的硬件时钟来设置时间.这足以满足许多目的.但是,当使用未连接到需要当前时间的Internet的解决方案时,这还不够好. Raspberry Pi的实时时钟价格低廉,但Windows IoT尚未在Raspberry Pi上对其提供本机支持.查看Internet上使用Raspberry Pi上的I2C实时时钟的其他解决方案,我见过的大多数解决方案都需要一个程序,该程序需要在需要时间的每个实例中直接从RTC检索时间,或者记住差异在系统时钟中的时间与连接的实时时钟之间.虽然这些解决方案有效,但我个人发现它们有些令人费解.我在这里介绍的解决方案旨在在每次引导时初始化系统时钟.与我看到的其他解决方案不同,设置完成后,无需更改现有程序即可使用它.可以使用通常用于获取时间的API,例如DateTime.Now
.
要求
我正在与一个已经在Windows IoT上进行开发的人写这篇文章.您将需要知道如何使用Visual Studio,C#编写程序,并熟悉C/C ++语言以了解此处编写的内容.您还需要已经知道如何使用电源外壳远程进入Windows IoT设备.
设定系统时间
Raspberry Pi上的Windows IoT运行UWP(通用Windows平台)应用程序. UWP应用程序运行沙箱;他们通常无法对系统进行任何会影响系统中其他程序的更改.如果浏览UWP类,将找不到任何可用于设置系统时间的类.尽管如此,您可能会在Internet上遇到一则帖子,指出您可以通过对本地Win32函数使用[DllImport]
来更改系统时间.这不是真的虽然可以编译和运行用于进行此Win32调用的代码,但它对系统没有影响.我仅在下面显示代码,以便您可以识别它.
[DllImport("kernelbase.dll", SetLastError = true)]
static extern bool SetSystemTime(ref SystemTime time);
该函数在" SystemTime"参数中使用时间和日期.时间必须是UTC时间.当填充要传递给此功能的信息时,请记住对UTC进行必要的调整.要使用此功能,将需要在UWP中定义此参数的布局.
[StructLayout(LayoutKind.Sequential)]
public struct SystemTime
{
[MarshalAs(UnmanagedType.U2)]
public short Year;
[MarshalAs(UnmanagedType.U2)]
public short Month;
[MarshalAs(UnmanagedType.U2)]
public short DayOfWeek;
[MarshalAs(UnmanagedType.U2)]
public short Day;
[MarshalAs(UnmanagedType.U2)]
public short Hour;
[MarshalAs(UnmanagedType.U2)]
public short Minute;
[MarshalAs(UnmanagedType.U2)]
public short Second;
[MarshalAs(UnmanagedType.U2)]
public short Milliseconds;
public SystemTime(DateTime dt)
{
Year = (short)dt.Year;
Month = (short)dt.Month;
DayOfWeek = (short)dt.DayOfWeek;
Day = (short)dt.Day;
Hour = (short)dt.Hour;
Minute = (short)dt.Minute;
Second = (short)dt.Second;
Milliseconds = (short)dt.Millisecond;
}
}
当我第一次编写使用此代码的代码时,我认为我的工作已经完成了一半.直到测试,我才发现该代码完全无效.在尝试了一些其他无效的方法后,我想到的最终可行解决方案是使用Power Shell脚本.您无需对电源外壳有太多了解即可遵循该解决方案. powershell脚本只有两行.
电源外壳中的set-date
命令可用于设置日期和时间.我设置时间的解决方案涉及到让程序将当前时间写入文件,然后让Power Shell脚本从该文件中提取时间并将其传递给set-date. UWP应用程序只能在文件系统的特定区域中进行写操作,这些区域大多数是应用程序特定的位置. (有关UWP上数据存储和限制的更多信息,请参见here).我没有处理写入文件的限制和要求,而是决定创建一个控制台应用程序,该应用程序将输出时间并将其输出捕获以转发到"设置日期".对此进行测试仅需要一个程序,该程序将以所需的格式输出日期.目前,它不需要是正确的日期
int main (Platform::Array<Platform::String^>^ args)
{
cout << "2020/03/04 01:23" << endl;
return 0;
}
作为优先事项,我在计算机上保留了一个名为`shares’的文件夹,出于各种目的,我在其中还有许多其他文件夹.我决定在Raspberry Pi上执行相同的操作.在我的台式计算机上,我打开文件浏览器并输入路径\ IPADDRESS \ c $ (其中IPADDRESS应该具有您的Raspberry Pi的名称或IP地址),并创建了文件夹路径c:\共享\启动.您可以根据需要创建其他文件路径,这只是我的选择.我将可执行文件(名为" SetTimeTool.exe")复制到此文件夹中,并启动了到设备的远程Power Shell会话.更改为该路径(使用与在命令提示符下使用的命令相同的命令),键入以下两行是将可执行文件输出的时间应用于系统时间所需要的全部.
$CurrentDate = ( c:\shares\boot\SetTimeTool.exe ) |Out-String
set-date $CurrentDate
现在,我们知道如何在命令提示符下更改时间,我们的工作已完成一半.以上命令将计划在启动时运行.我们待会儿再谈.该可执行文件需要更新,以便从实时时钟附件中提取实际时间.我要使用的时钟芯片是DS3231.如果您使用其他时钟,则需要相应地更改代码.
关于DS3231
DS3231的分线板约合10美元.我碰巧得到的是通过亚马逊收购的.我收到的装置将电池焊接到触点上,以在不使用时钟时备份时钟.我的设备上的电池是DOA,但更换电池后可以正常工作.本机以二进制编码的十进制(BCD)格式处理时间信息.这种格式的便利之处在于,如果我使用该系统来驱动LCD,则需要的电子设备更少,可以将时间转换为要显示的内容.我可以将信息按原样发送到硬件以显示十六进制数字,而无需进行任何转换.我没有用它来构建硬件时钟.我需要一个在二进制补码(普通)编码和BCD编码之间转换的函数. DS3231还可以将时间格式化为12小时制(AM和PM有点用)或24小时制.我只会使用24小时制. DS3231还具有温度传感器.这不是此职位感兴趣的功能.但是我将包含用于阅读的代码.如果需要准确读取温度,我建议您仔细考虑DS3231的放置或使用其他组件进行温度检测. Raspberry Pi本身在运行时会散发热量,如果芯片靠近Pi,则感测到的温度将高于其余环境中的温度.与DS3231的通信是通过I2C进行的.它的I2C地址是0x68.
BCD编码
BCD和二进制补码之间的转换很容易.以十六进制格式显示BCD时,很容易看到它代表的十进制数字.十六进制数字59表示为0x3B.如果我们想以一种人可以阅读某种转换的格式来显示它,那将是必要的.但是,如果要使用的值在BCD中,则当我们以十六进制显示59的BCD编码时,将得到0x59.对于值12,它将为0x12. 2将是0x02.给定一个数字的二进制补码编码,可以通过一些数学运算将其转换为2位数的BCD.取值mod 10的结果(“值%10”),然后将其值除以10(整数除法)再乘以16.BCD返回二进制补码的转换可以通过一些位运算和乘法.将BCD值的低半字节(4位)与高半字节乘以10.
static int BcdToInt(byte bcd)
{
int retVal = (bcd & 0xF) + ((bcd >> 4) * 10);
return retVal;
}
static byte IntToBcd(int v)
{
var retVal =(byte)( (v % 10) | (v / 10) << 0x4);
return retVal;
}
初始化I2C
为了通过I2C与时钟进行通信,我们需要获得对提供I2C控制器接口的对象的引用. DeviceInformation类可用于获取对控制其他硬件的对象的引用.在下面的代码中,我向I2C控制器查询ID,然后请求引用第一个I2C控制器上位于地址" 0x68"的设备.理论上,一个设备可以具有多个I2C控制器. Raspberry Pi有一个;第一个控制器将是唯一的控制器. _device对象可用于与时钟的所有通信.
async void Init(int address)
{
var advancedQuerySyntaxString = I2cDevice.GetDeviceSelector();
var controllerDeviceIds = await DeviceInformation.FindAllAsync(advancedQuerySyntaxString);
I2cConnectionSettings connectionSettings = new I2cConnectionSettings(address);
connectionSettings.BusSpeed = I2cBusSpeed.StandardMode;
_device = await I2cDevice.FromIdAsync(controllerDeviceIds[0].Id, connectionSettings);
_initComplete = true;
}
I2C总线上可能有多个设备.与I2C设备进行通讯时,我不会详细介绍电气方面的情况.从代码的角度来看,将以字节数组编码的消息发送到I2C设备,并以原子操作的形式接收其响应.操作之前必须在其中分配一个缓冲区来保存响应.对于读/写操作,我们将使用名为Read的方法Write,它带有两个参数.要发送到设备的信息的字节数组,以及将响应保存到的字节数组.设置时间时,我们还将执行只写操作.写功能接受包含要写入数据的字节数组.要知道我们可以写什么,有必要查看一下D3231中的寄存器.
DS3231寄存器的布局
DS3231包含一系列寄存器中的当前日期和时间,最后的温度读数以及其他一些信息的值.根据数据表(来自本页),寄存器布局如下. 我省略了上面简化的寄存器布局中的一些详细信息.该芯片确实支持警报;我不会使用的东西.如果要读取其中一个寄存器,则传递一个单字节数组.该字节的值将是开始读取的寄存器的偏移量.我们还需要将响应传递到其中的数组.如果为响应传递了多字节数组,则将使用跟随指定偏移量的寄存器填充该数组,直到该数组满为止.如果一个至少包含6个字节的字节数组,一次可以读取所有寄存器.如果要写入寄存器(以设置时间),则必须填充一个字节数组,其中第一个元素是要开始写入的寄存器偏移量,其后的所有字节都是要写入寄存器的值. 温度分布在两个寄存器中.第一个寄存器具有摄氏温度的整数部分.第二部分以0.25摄氏度的增量存储在寄存器的2位中.如果我们只想读取温度,可以按照以下步骤进行.
public float ReadTemperature()
{
byte[] buffer = new byte[2];
_device.WriteRead(new byte[] { 0x11 }, buffer);
float temperature = (float)buffer[0] + ((float)(buffer[1]>>6) / 4f);
return temperature;
}
这里分配了一个2字节的数组来接收温度.用包含011h的数组调用ReadWrite方法,这意味着我们希望在寄存器偏移量0x11(十进制为17)处获取信息. 2字节缓冲区将使用数组第一个字节中来自寄存器11h的整数温度进行填充,而寄存器12h中的小数部分将填充至第二个字节中.移位用于获取仅2个最高有效位,而除法用于将其缩小至小数部分. 读取时间需要更多的位操作,但与组件的交互是相同的.我们将填充一个字节数组,然后操纵这些位以所需的格式获取时间. DS3121的前7个字节包含时间分量.因此,我们将使用几行代码来获取前8个字节.
sbyte[] readBuffer = new byte[0x7h];
_device.WriteRead(new byte[] { 0x00 }, readBuffer);
在用单独的变量对日期和时间进行解码后,将其组装在一起成为单个" DateTime"对象.由于DS3121仅将年份记录为2位数字,因此必须在年份上加上2,000以获取实际年份.
public DateTime? ReadTime()
{
if (!_initComplete)
return null;
byte[] readBuffer = new byte[0x7h];
_device.WriteRead(new byte[] { 0x00 }, readBuffer);
int seconds = BcdToInt(readBuffer[0]);
int minutes = BcdToInt(readBuffer[1]);
bool is24HourCock = (readBuffer[2] >> 0x6) != 1;
int hours;
if (is24HourCock)
hours = (readBuffer[2] & 0xF) + ((readBuffer[2] >> 4) & 0x1) * 10 + ((readBuffer[2] >> 0x5) * 20);
else
hours = (readBuffer[2] & 0xF) + ((readBuffer[2] >> 4) & 0x1) * 10 + ((readBuffer[2] >> 0x5) * 12); ;
int day = BcdToInt(readBuffer[3]);
int date = BcdToInt(readBuffer[4]);
int months = BcdToInt((byte)(readBuffer[5]&(byte)0x3f));
int year = BcdToInt(readBuffer[6]);
return new DateTime(2000+year, months, date, hours, minutes, seconds);
}
设置时间与解码操作相反.我将时间的成分转换回BCD值,并将其填充到数组中.数组的第一个元素是要写入的第一个寄存器的偏移量.数组的其余值是要写入的数据.我只以24小时制写时间.
public void WriteTime(DateTime dateTime)
{
byte[] buffer = new byte[8];
int offset = 0;
buffer[offset++] = 0;
buffer[offset++] = IntToBcd(dateTime.Second);
buffer[offset++] = IntToBcd(dateTime.Minute);
buffer[offset++] = IntToBcd(dateTime.Hour);
buffer[offset++] = (byte)dateTime.DayOfWeek;
buffer[offset++] = IntToBcd(dateTime.Day);
buffer[offset++] = IntToBcd(dateTime.Month);
buffer[offset++] = IntToBcd(dateTime.Year % 100);
_device.Write(buffer);
}
设定时间
现在我们有了一个可以保留时间的时钟,我们需要对其进行设置.需要另一个初始时间来源.其他时间源可能是初始引导,因此可以使用NTP设置系统时钟.也可能来自用户或其他传感器(例如:GPS接收当前时间).时间的源头是现在,我们已经拥有设置实时时钟所需的所有操作.该演示应用程序将每秒从RTC和系统(伪硬件)时钟中读取一次,并显示它已读取的值.它还显示日期和时间选择器,以便用户可以在RTC中设置时间.如果要查看RTC是否确实在工作,可以将此应用程序设置为默认应用程序,并在断开网络连接的情况下重新启动Raspberry Pi.让我们将我们学到的知识应用到控制台模式应用程序中.
完成SetTimeTool程序
设置时间工具是控制台模式程序.迄今为止,控制台模式程序只能用C ++编写.我希望该程序能够做两件事.首先,该程序需要从实时时钟读取时间并将其打印到其输出.我还希望能够使用该程序在实时时钟中设置时间.为了简化设置实时时钟的过程,该程序将占用系统时间并将其复制到RTC中.该程序的主要方法如下.
int main (Platform::Array<Platform::String^>^ args)
{
//Check whether the user has specified the argument for setting the real time clock.
//otherwise assume that we are only outputting the time.
bool setTime = false;
if (args->Length > 1)
{
setTime = (args->get(1)->Equals(ref new String(L"set-time")));
}
//Get a reference to an I2C controller. If non is found have the program print
//an error message and exit immediately
String^ aqs = I2cDevice::GetDeviceSelector();
auto controllerList = concurrency::create_task(DeviceInformation::FindAllAsync(aqs)).get();
if (controllerList->Size < 1) {
cout << "no i2c controller found " << endl;
return -1;
}
//The DS3231 has an I2C address of 0x68. Create an I2cConnectionSettings object
//that contains this information
I2cConnectionSettings^ settings = ref new I2cConnectionSettings(0x68);
settings->BusSpeed = I2cBusSpeed::StandardMode;
//Create an I2cDevice object associated with the controller that we found for interacting
//with the real time clock
String^ controllerId = controllerList->GetAt(0)->Id;
auto realTimeClock = concurrency::create_task(I2cDevice::FromIdAsync(controllerId, settings)).get();
//If the time were being set then call the SetTime function. Otherwise call the ShowTime method
if (setTime) SetTime(realTimeClock);
else ShowTime(realTimeClock);
return 0;
}
我们仍然需要定义显示时间和设置时间以及移植BCD/Integer转换功能的方法.在C ++中,转换功能几乎与C#版本相同.
byte BcdToInt(byte bcd)
{
byte retVal = (bcd & 0xF) + ((bcd >> 4) * 10);
return retVal;
}
byte IntToBcd(int v)
{
byte retVal = (byte)((v % 10) | (v / 10) << 0x4);
cout << v << " - " << (int) retVal << endl;
return retVal;
}
void SetTime(I2cDevice^ realTimeClock)
{
SYSTEMTIME systemTime;
//Get the system time. This will be in UTC
GetSystemTime(&systemTime);
//Copy the UTC time into the Real Time chip
std::vector<BYTE> setTimeCommand;
setTimeCommand.push_back((BYTE)0x00);
setTimeCommand.push_back(IntToBcd(systemTime.wSecond));
setTimeCommand.push_back(IntToBcd(systemTime.wMinute));
setTimeCommand.push_back(IntToBcd(systemTime.wHour));
setTimeCommand.push_back(IntToBcd(systemTime.wDayOfWeek));
setTimeCommand.push_back(IntToBcd(systemTime.wDay));
setTimeCommand.push_back(IntToBcd(systemTime.wMonth));
setTimeCommand.push_back(IntToBcd(systemTime.wYear % 100));
for (int i = 0; i < 10; ++i)
setTimeCommand.push_back(0);
realTimeClock->Write(ArrayReference<BYTE>(setTimeCommand.data(), static_cast<unsigned int>(setTimeCommand.size())));
}
显示时间需要更多的思考.注意上面的内容,当我从系统请求时间时,我正在使用GetSystemTime()方法. T here也是一个" GetLocalTime()".使用系统时间更容易,因为我不必担心各个地区的夏令时规则的复杂性.但是,power shell的" set-date"命令适用于当地时间.因此,我需要将从芯片读取的UTC时间转换回本地时间. T here在Win32中没有直接将UTC时间转换为本地时间的功能.但是,如果使用了多种方法,我们可以进行转换.我在Microsoft页面here上找到了需要调用的一系列函数,该页面显示了使用3种函数进行此转换.
SystemTimeToFileTime(&time, &FileTime);
FileTimeToLocalFileTime(&FileTime, &LocalFileTime);
FileTimeToSystemTime(&LocalFileTime, &LocalTime);
有了掌握的知识,我们就可以完成显示时间所需的最后一个功能.该代码看起来类似于C#版本.
void ShowTime(I2cDevice^ realTimeClock)
{
std::vector<BYTE> readCommand;
Array<BYTE>^ resultBuffer = ref new Array<BYTE>(0x7);;
readCommand.push_back((BYTE)0x00);
realTimeClock->WriteRead(ArrayReference<BYTE>(readCommand.data(), static_cast<unsigned int>(readCommand.size())), resultBuffer);
SYSTEMTIME time;
ZeroMemory(&time, sizeof(time));
time.wSecond = BcdToInt(resultBuffer[0]);
time.wMinute = BcdToInt(resultBuffer[1]);
bool is24HourCock = (resultBuffer[2] >> 0x6) != 1;
if (is24HourCock)
time.wHour = (resultBuffer[2] & 0xF) + ((resultBuffer[2] >> 4) & 0x1) * 10 + ((resultBuffer[2] >> 0x5) * 20);
else
time.wHour = (resultBuffer[2] & 0xF) + ((resultBuffer[2] >> 4) & 0x1) * 10 + ((resultBuffer[2] >> 0x5) * 12); ;
time.wDayOfWeek = BcdToInt(resultBuffer[3]);
time.wDay = BcdToInt(resultBuffer[4]);
time.wMonth = BcdToInt((byte)(resultBuffer[5] & (byte)0x3f));
time.wYear = BcdToInt(resultBuffer[6]) + 2000;
//https://support.microsoft.com/en-us/kb/245786
FILETIME FileTime, LocalFileTime;
SYSTEMTIME LocalTime;
SystemTimeToFileTime(&time, &FileTime);
FileTimeToLocalFileTime(&FileTime, &LocalFileTime);
FileTimeToSystemTime(&LocalFileTime, &LocalTime);
std::cout << LocalTime.wMonth << "/" << LocalTime.wDay << "/" << LocalTime.wYear
<< " " << setfill('0') << setw(2) << LocalTime.wHour << ":" << LocalTime.wMinute << ":" << LocalTime.wSecond;
}
完成程序源代码后,剩下的就是确保我先前键入的Power Shell命令在每次引导时都能运行.我使用记事本将2行脚本的文本保存到名为" Update
Time.ps1"的文件中,并将其复制到路径为" c:\ shares \ boot"的Raspberry Pi中.为了测试它,我通过将系统时间设置为正确的时间,然后运行.\ SetTimeTool.exe set-time,来确保RTC具有正确的时间.然后,我有意输入" Set-
date"" 2010/01/02 15:16"(设置时间),然后将系统时钟设置为错误的" date"和时间,然后运行" date"安装系统显示其"日期".正如预期的那样,系统现在显示其日期不正确.我运行.`Update
Time.ps1,然后再次运行
date`,发现现在显示的是正确的时间.在上一个测试中,我制作了一个UWP应用程序,该应用程序无非是显示每秒更新一次系统时间并将其设置为默认程序.我使用以下Power Shell命令将Up.dateTime.ps1脚本安排为在启动时运行.
schtasks /create /tn "Update Time Script" /tr C:\shares\boot\UpdateTime.ps1 /sc onstart /ru SYSTEM
这是必要的,因为要确保程序正常运行,就需要从网络上引导(这将使我们无法远程调用程序),因此脚本是唯一更改时钟的东西.断开设备电源,稍等片刻,然后重新打开.您应该看到正确设置的时间.
对于其他时钟实现
还有其他几种芯片可用作实时时钟.一些GPS接收器具有备用电池,因此即使没有GPS定位器,只要有时间,它们就可以继续使用该时间.该代码无法直接与其他芯片配合使用,但是可以轻松进行修改,以便在启动时可以使用相同的技术来初始化来自实时源的时钟.
历史
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C++ C# Win32 Dev Intermediate WinRT Raspberry VS2013 IoT 新闻 翻译