2024年1月

背景:看了博主一抹浅笑的rest_framework认证模板,发现登录视图函数是基于APIView类封装。
优化:使用ModelViewSet类通过重写create方法编写登录函数。
环境:既然接触到rest_framework的使用,相信已经搭建好相关环境了。

1 建立模型

编写模型类

# models.py
from django.db import models
class User(models.Model):
    username = models.CharField(verbose_name='用户名称',unique=True,max_length=16)
    password = models.CharField(verbose_name='登陆密码',max_length=16)
class Token(models.Model):
    username = models.CharField(verbose_name='用户名称',unique=True,max_length=16)
    token = models.CharField(verbose_name='验证密钥',max_length=32)

生成迁移文件

python manage.py makemigrations

迁移数据模型

python manage.py migrate

2 确定需要重写的方法

查看ModelViewSet类源码

'''
class ModelViewSet(mixins.CreateModelMixin,
                   mixins.RetrieveModelMixin,
                   mixins.UpdateModelMixin,
                   mixins.DestroyModelMixin,
                   mixins.ListModelMixin,
                   GenericViewSet):
    """
    A viewset that provides default `create()`, `retrieve()`, `update()`,
    `partial_update()`, `destroy()` and `list()` actions.
    """
    pass
'''

最终目的是往Token模型对应的表添加数据,所以得选择CreateModelMixin模型的源码查看。

'''
class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}
'''

查看得知,CreateModelMixin类下的create方法调用了serializer类的save方法创建数据。继续查看save方法。
通过serializers.ModelSerializer定位到serializers.py文件,搜索'def save('定位到以下内容。

'''
    def save(self, **kwargs):
        assert hasattr(self, '_errors'), (
            'You must call `.is_valid()` before calling `.save()`.'
        )

        assert not self.errors, (
            'You cannot call `.save()` on a serializer with invalid data.'
        )

        # Guard against incorrect use of `serializer.save(commit=False)`
        assert 'commit' not in kwargs, (
            "'commit' is not a valid keyword argument to the 'save()' method. "
            "If you need to access data before committing to the database then "
            "inspect 'serializer.validated_data' instead. "
            "You can also pass additional keyword arguments to 'save()' if you "
            "need to set extra attributes on the saved model instance. "
            "For example: 'serializer.save(owner=request.user)'.'"
        )

        assert not hasattr(self, '_data'), (
            "You cannot call `.save()` after accessing `serializer.data`."
            "If you need to access data before committing to the database then "
            "inspect 'serializer.validated_data' instead. "
        )

        validated_data = {**self.validated_data, **kwargs}

        if self.instance is not None:
            self.instance = self.update(self.instance, validated_data)
            assert self.instance is not None, (
                '`update()` did not return an object instance.'
            )
        else:
            self.instance = self.create(validated_data)
            assert self.instance is not None, (
                '`create()` did not return an object instance.'
            )
'''

看最后这个if……else……语句中的self.instance = self.create(validated_data)。
说明这里调用了create方法,返回一个模型对象。于是查看ModelSerializer类的create方法。

'''
    def create(self, validated_data):
        """
        We have a bit of extra checking around this in order to provide
        descriptive messages when something goes wrong, but this method is
        essentially just:

            return ExampleModel.objects.create(**validated_data)

        If there are many to many fields present on the instance then they
        cannot be set until the model is instantiated, in which case the
        implementation is like so:

            example_relationship = validated_data.pop('example_relationship')
            instance = ExampleModel.objects.create(**validated_data)
            instance.example_relationship = example_relationship
            return instance

        The default implementation also does not handle nested relationships.
        If you want to support writable nested relationships you'll need
        to write an explicit `.create()` method.
        """
        raise_errors_on_nested_writes('create', self, validated_data)

        ModelClass = self.Meta.model

        # Remove many-to-many relationships from validated_data.
        # They are not valid arguments to the default `.create()` method,
        # as they require that the instance has already been saved.
        info = model_meta.get_field_info(ModelClass)
        many_to_many = {}
        for field_name, relation_info in info.relations.items():
            if relation_info.to_many and (field_name in validated_data):
                many_to_many[field_name] = validated_data.pop(field_name)

        try:
            instance = ModelClass._default_manager.create(**validated_data)
        except TypeError:
            tb = traceback.format_exc()
            msg = (
                'Got a `TypeError` when calling `%s.%s.create()`. '
                'This may be because you have a writable field on the '
                'serializer class that is not a valid argument to '
                '`%s.%s.create()`. You may need to make the field '
                'read-only, or override the %s.create() method to handle '
                'this correctly.\nOriginal exception was:\n %s' %
                (
                    ModelClass.__name__,
                    ModelClass._default_manager.name,
                    ModelClass.__name__,
                    ModelClass._default_manager.name,
                    self.__class__.__name__,
                    tb
                )
            )
            raise TypeError(msg)

        # Save many-to-many relationships after the instance is created.
        if many_to_many:
            for field_name, value in many_to_many.items():
                field = getattr(instance, field_name)
                field.set(value)

        return instance
'''

这逻辑我是没看懂,但是通过print、type、dir函数可以确定
接收对象validated_data是一个字典,
返回对象instance是一个模型对象。
于是可以把源码cv过来,简单测试是否能够通。

import time
import hashlib

from rest_framework import status
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from myapp import models as myapp_models

class TokenSerializer(serializers.ModelSerializer):
    class Meta:
        model = myapp_models.Token
        fields = '__all__'
    def create(self,validated_data):
        ######################################
        query_obj = myapp_models.Token.objects.update_or_create(
            username=validated_data['username'],
            defaults={"username":validated_data['username'],"token":validated_data['token']})[0]
        print(query_obj)
        return query_obj
        #------------------------------------#
class LoginView(ModelViewSet):
    queryset = myapp_models.Token.objects.all()
    serializer_class = TokenSerializer
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

3 重写create方法

3.1 编写登录逻辑

TokenSerializer
1.获取username和password。
2.验证username、password匹配性。
3.匹配错误:更新或创建模型中username对应的token为空字符串,返回模型对象。
4.匹配正确:通过md5加密生成token,更新或创建模型中username对应的token为密钥。
ModelViewSet
1.根据username查询token值。
2.将username、token值设置到session会话。

import time
import hashlib

from rest_framework import status
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from myapp import models as myapp_models

class TokenSerializer(serializers.ModelSerializer):
    class Meta:
        model = myapp_models.Token
        fields = '__all__'
    def create(self,validated_data):
        ######################################
        user_obj = myapp_models.User.objects.filter(
            username=validated_data['username'],
            password=validated_data['token'])
        user_dict = validated_data
        user_dict['token'] = ''
        if not user_obj.exists():
            query_obj = myapp_models.Token.objects.update_or_create(
                username=user_dict['username'],
                defaults={"username":user_dict['username'],"token":user_dict['token']})[0]
            return query_obj
        validated_data['token'] = hashlib.md5(
            ''.format(time.time(),''.join(validated_data.values())).encode()).hexdigest()
        query_obj = myapp_models.Token.objects.update_or_create(
            username=validated_data['username'],
            defaults={"username":validated_data['username'],"token":validated_data['token']})[0]
        print(query_obj)
        return query_obj
        #------------------------------------#
class LoginView(ModelViewSet):
    queryset = myapp_models.Token.objects.all()
    serializer_class = TokenSerializer
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        ######################################
        token_obj = myapp_models.Token.objects.filter(
            username=request.POST.get('username')).first()
        if token_obj.token == '':
            request.session['username'] = token_obj.username
            request.session['token'] = token_obj.token
            return Response('检查输入的账户和密码')
        request.session['username'] = token_obj.username
        request.session['token'] = token_obj.token
        #------------------------------------#
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

3.2 编写认证逻辑

1.从session中获取username,token。
2.判断username,token是否不存在、或token是否为空字符串。
3.判断正确:抛出异常。
4.判断错误:范围username和模型对象组成的元组。

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

from myapp import models as myapp_models

class Authentication(BaseAuthentication):
    def authenticate(self,request):
        ######################################
        username = request._request.session.get('username','')
        token = request._request.session.get('token','')
        token_obj = myapp_models.Token.objects.filter(
            username=username,token=token)
        if not token_obj.exists or token_obj.first().token == '':
            raise exceptions.AuthenticationFailed('认证失败')
        return (token_obj.first().username,token_obj.first())
        #------------------------------------#

3.3 添加路由

path('login/',myapp_views.LoginView.as_view({
        'post':'create'}),name='login')

什么是WSL
WSL:Windows subsystem for Linux,是用于Windows上的Linux的子系统
作用很简单,可以在Windows系统中获取Linux系统环境,并完全直连计算机硬件,无需通过虚拟机虚拟硬件,不会影响Windows系统本身

为什么使用WSL
WSL作为自Windows10以来的全新特性正逐步颠覆开发人员的选择
传统方式下,获取Linux操作系统环境,是安装完整的虚拟机,如VMware
但使用WSL,是可以以非常轻量化方式,得到Linux的系统环境

WSL部署(重点哦!)

WSL为Windows自带功能,我们只需要把它开启即可
在搜索里找到控制面板,点击程序,点击启用或关闭Windows功能,向下滑动找到适用于Linux的Windows子系统点击确定进行重启

在微软商店,搜索Ubuntu,点击下载即可,等待安装成功就可以去使用了

(额...失败了)

错误提示是WSL2的问题(抱歉,没有截图)

debug吧,通过微软商店里的评论,我们可以发现我们打开所有关于Hyper-V的项目和虚拟机相关项目

但是我用的是Windows11家庭版没有Hyper-V的选项啊!(该怎么办呢>-<)

通过查找,我找到了这篇文章:
Win10 家庭中文版安装Docker(Win10家庭版 +Hyper-V+Docker) - 柠檬草不孤单 - 博客园 (cnblogs.com)

pushd "%~dp0"

dir /b %SystemRoot%\servicing\Packages\*Hyper-V*.mum >hyper-v.txt

for /f %%i in ('findstr /i . hyper-v.txt 2^>nul') do dism /online /norestart /add-package:"%SystemRoot%\servicing\Packages\%%i"

del hyper-v.txt

Dism /online /enable-feature /featurename:Microsoft-Hyper-V-All /LimitAccess /ALL

鼠标右键选中“以管理员身份运行”,窗口运行执行代码,直到运行结束,显示提示是否重启,输入Y,重启电脑

再次打开(还是不行>_<++)(抱歉,我把这个错误忘记了)

(难道就这样放弃吗-------------------------------------------------------------当然不可能)

我查看了微软WSL文档:
旧版 WSL 的手动安装步骤 | Microsoft Learn

1.我们会发现当我们启用Windows下的Linux子系统时WSL就已经安装启用完成了(所以第一步安装跳过>v<)

2.对于第二步(想必看这篇文章的用户都知道自己的设备能否使用WSL2吧)

若要更新到 WSL 2,需要运行 Windows 10及以上。

  • 对于 x64 系统:版本 1903 或更高版本,内部版本为 18362.1049 或更高版本。
  • 对于 ARM64 系统:版本 2004 或更高版本,内部版本为 19041 或更高版本。

3.启用虚拟功能

安装 WSL 2 之前,必须启用“虚拟机平台”可选功能。 计算机需要
虚拟化功能
才能使用此功能。(从这可以看到,我们之前只启用了Windows下的Linux子系统)

以管理员身份打开 PowerShell 并运行:

dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

重新启动计算机,以完成 WSL 安装并更新到 WSL 2。

4.下载Linux内核更新安装包

Linux 内核更新包会安装最新版本的
WSL 2 Linux 内核
,以便在 Windows 操作系统映像中运行 WSL。 (若要运行
Microsoft Store 中的 WSL
并更频繁地推送更新,请使用
wsl.exe --install

wsl.exe --update
。)

下载最新包:(如果使用的是 ARM64 计算机,请下载ARM64包)

安装完成后,请继续执行下一步 - 在安装新的 Linux 分发时,将 WSL 2 设置为默认版本。 (如果希望将新的 Linux 安装设置为 WSL 1,请跳过此步骤。)

5.将将 WSL 2 设置为默认版本

打开 PowerShell,然后在安装新的 Linux 发行版时运行以下命令,将 WSL 2 设置为默认版本:

wsl --set-default-version 2

打开刚才下载好的Ubuntu,可以运行了(完结撒花)

配置Ubuntu

配置好用户名和密码

其它根据个人习惯进行配置

祝贺你!(你成功的安装了Windows上的Linux的子系统)

(希望我的文章能帮助到你,文章还有很多不足,请在评论区指证,以促进我们共同进步,谢谢)

版权说明: 本文章版权归本人及博客园共同所有,转载请在文章前标明原文出处( https://www.cnblogs.com/mikevictor07/p/17968696.html ),以下内容为个人理解,仅供参考。

一、背景

起因:
自监控应用凌晨告警:
Pod 内存使用率大于80%(规格为1c1G)
。内存缓慢增长,持续到早上内存使用率停止在81%左右。

疑点:
此模块是一个轻任务模块(基于go开发),请求量很低并且数据量非常少,平常内存占用一直以来都在100MB左右,出现内存不足的概率极小,而且运行了几个月无故障。

初步定位:
登录平常查看指标,确实有一个节点内存异常,但另一个节点正常(这模块有个特性是主备模式,
同一时间只有一个节点工作
,通过日志确定异常的节点正是工作节点)。

二、初步分析过程

登录k8s查看内存情况,通过 kubectl top pod 查看内存占用果然已经有800MB+,但理论上这模块不应该占用这么多内存(截图时间点不一样,有部分回收)。

继续登录pod内,通过 cat /sys/fs/cgroup/memory/ 查看内存统计 (注意,在pod中使用 free -m 等类似的命令只能统计到宿主机的内存信息,固无用)

# cd  /sys/fs/cgroup/memory/
# cat memory.usage_in_bytes
显示输出 962097152(即约917MB,即将超过1GB限额,超过则会激活OOM Kill)

# cat memory.stat 后输出如下图

其中的
rss
标识当前应用进程实际使用内存量,
55017472 = 约52MB
,此数据证实了一般的设定:这个应用一般占用都在100MB以内。

三、怀疑监控指标不准确?

通过了解到,激活自监控告警的指标是通过k8s的
container_memory_working_set_bytes
指标超过80%告警。

通过查阅k8s源码 promethus.go 的 Memory.WorkingSet 相关引用发现,此参数是通过计算
Memory.Usage - total_inactive_file
得出(即本案例是
962097152 - 666666620096 = 811MB

(其中的
Memory.Usage 即为memory.usage_in_bytes文件中的值:962097152

按照此情况看,数据取值确实没问题,同时,关注到一个指标
total_active_file
(795275264 = 758MB),此参数加上rss刚好与已用内存接近

,源码中未找到此指标的相关信息,通过查阅官方资料发现,此参数认为是一个不能被计算为可用内存的值。

也就是说 k8s 作者们认为
此active_file内存不认定为可用内存
(官方地址为:https://kubernetes.io/docs/concepts/scheduling-eviction/node-pressure-eviction/#active-file-memory-is-not-considered-as-available-memory )

此参数作为文件缓存是否要被计算进已用内存中,github上的讨论已经有了6年之久仍然是Open状态 (地址为:  https://github.com/kubernetes/kubernetes/issues/43916)。

四、应用分析

此应用只有日志才用到写文件的操作,是否是日志文件导致的file cache呢? 进入到日志文件目录 ,通过 > xxx.log 清理文件后,再次 cat memory.stat

其中的 total_active_file 立即缩小,在通过之前的命令查看内存占用,立即恢复正常,也就是
日志文件导致的 total_active_file 增长从而导致Pod内存使用量增大

五、回溯代码 & 修复措施

此应用使用了 zap日志框架,通过配置 MaxSize 设定日志轮转文件大小为1G,在故障时日志文件大小已经达到了 889M。

日志一直要达到1G才会激活轮转,此前系统将此cache住,但是k8s认为此内存无法被利用,就导致了内存一直在增长,直到产生告警。

解决方案:
为保证Pod 不被 OOM Kill,通过修改MaxSize 修改文件大小进行轮转(比如改为200-300M),file cache即可在日志轮转后释放。

模块 pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。 "pickling" 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 "unpickling" 是相反的操作,会将(来自一个 binary file 或者 bytes-like object 的)字节流转化回一个对象层次结构。 pickling(和 unpickling)也被称为“序列化”, “编组” 1 或者 “平面化”。而为了避免混乱,此处采用术语 “封存 (pickling)” 和 “解封 (unpickling)”。

pickle
模块 并不安全。 你只应该对你信任的数据进行 unpickle 操作。
构建恶意的 pickle 数据来 在解封时执行任意代码 是可能的。 绝对不要对不信任来源的数据和可能被篡改过的数据进行解封。
请考虑使用 hmac 来对数据进行签名,确保数据没有被篡改。
在你处理不信任数据时,更安全的序列化格式如 json 可能更为适合。

与 json 模块的比较

在 pickle 协议和 JSON (JavaScript Object Notation) 之间有着本质上的差异:

  • JSON 是一个文本序列化格式(它输出 unicode 文本,尽管在大多数时候它会接着以 utf-8 编码),而 pickle 是一个二进制序列化格式;
  • JSON 是我们可以直观阅读的,而 pickle 不是;
  • JSON是可互操作的,在Python系统之外广泛使用,而pickle则是Python专用的;
  • 默认情况下,JSON 只能表示 Python 内置类型的子集,不能表示自定义的类;但 pickle 可以表示大量的 Python 数据类型(可以合理使用 Python 的对象内省功能自动地表示大多数类型,复杂情况可以通过实现 specific object APIs 来解决)。
  • 不像pickle,对一个不信任的JSON进行反序列化的操作本身不会造成任意代码执行漏洞。

Pickle的基本用法

序列化(Pickling)

要将Python对象序列化为二进制数据,可以使用
pickle.dump()
函数。以下是一个简单的示例,将一个Python列表保存到文件中:

import pickle

data = [1, 2, 3, 4, 5]

# 打开一个文件以写入二进制数据
with open('data/data.pkl', 'wb') as file:
    pickle.dump(data, file)

在上述代码中,使用
pickle.dump()
函数将data列表序列化为二进制数据,并将其保存到名为data.pkl的文件中。参数'wb'表示以二进制写入模式打开文件。

反序列化(Unpickling)

要从文件中加载并反序列化二进制数据,可以使用pickle.load()函数。以下是加载data.pkl文件并还原Python对象的示例:

import pickle

# 打开文件以读取二进制数据
with open('data/data.pkl', 'rb') as file:
    loaded_data = pickle.load(file)

print("反序列化 %s" % loaded_data)

在上述代码中,使用
pickle.load()
函数从data.pkl文件中加载数据,并将其还原为Python对象。

Pickle的工作原理

pickle模块的工作原理涉及到将Python对象转换为一种可序列化的中间格式,然后再将该中间格式序列化为二进制数据。这个中间格式是一个自包含的表示对象的字典,其中包含了对象的数据和其类型信息。

当使用
pickle.dump()
序列化对象时,pickle 模块首先创建一个包含对象数据和类型信息的中间字典。然后,它将该字典转换为二进制数据。反序列化时,pickle模块将二进制数据还原为中间字典,然后再从字典中还原Python对象。

这种方法使pickle模块非常灵活,因为它可以序列化几乎所有Python对象,包括自定义对象,只要它们可以在中间字典中表示。

Pickle的适用场景

pickle模块在以下情况下非常有用:

  • 数据持久化:你可以使用pickle将Python对象保存到文件中,以便稍后读取。这对于保存模型、配置文件、数据缓存等非常有用。
  • 数据传输:你可以使用pickle将Python对象序列化并通过网络传输,以便不同的Python程序之间共享数据。
  • 对象复制:你可以使用pickle将Python对象进行深拷贝,以便创建对象的独立副本,而不是引用原始对象。
  • 测试和调试:pickle也用于创建模拟数据,以便进行测试和调试。

Pickle的注意事项

尽管pickle非常方便,但在使用它时需要注意一些事项:

  • 安全性:反序列化数据时要小心,因为pickle可以执行任意代码。不要从不受信任的来源加载pickle数据,以免遭受安全风险。
  • 版本兼容性:在不同版本的Python之间,pickle数据的兼容性可能会有问题。因此,确保在不同版本之间测试并验证pickle数据的兼容性。
  • 自定义对象:一些自定义对象的序列化和反序列化可能会受到限制,因此需要额外的配置。你可能需要实现特定的__reduce__方法来控制对象的序列化行为。

示例代码

以下是一个示例代码,演示如何使用pickle模块来序列化和反序列化一个自定义Python对象:

import pickle

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Person(name='{self.name}', age={self.age})"


# 创建一个自定义对象
person = Person("Alice", 30)

# 序列化并保存到文件
with open('data/person.pkl', 'wb') as file:
    pickle.dump(person, file)

# 从文件中加载并反序列化
with open('data/person.pkl', 'rb') as file:
    loaded_person = pickle.load(file)

print(loaded_person)  # 输出: Person(name='Alice', age=30)

在上述代码中,我们首先定义了一个自定义类Person,然后创建了一个Person对象。我们使用pickle将该对象序列化为二进制数据,然后再从二进制数据中反序列化还原对象。

1、准备材料

开发板(
正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

野火DAP仿真器

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的
DAC OUT1实现输出0-3.3V任意模拟电压
,然后用ADC1_IN5单通道采集DAC输出的电压,并利用USART1输出信息用于验证

3、实验流程

3.0、前提知识

STM32F407有一个DAC,该DAC拥有两个输出通道OUT1/2,每个通道均可以输出0~VREF+范围内电压、噪声波或三角波型

DAC集成了两个输出缓冲器,可用来降低输出阻抗并在不增加外部运算放大器的情况下直接驱动外部负载,该参数可以在STM32CubeMX中DAC参数配置页面配置,一般选择Enable

DAC输出的触发源一共有8个触发源,可以通过设置DAC控制寄存器DAC_CR的TSEL[2:0]位来决定触发源,其中外部引脚触发源在STM32CubeMX中需要勾选Mode中的External Trigger才可以选择,具体如下表所示
(注释1)

DAC的数字转模拟主要是利用片上的12位电压输出数模转换器来实现的,而这个12位电压输出数模转换器的输入数据为数据输出寄存器DORx中的内容,
但是用户不能直接将数据写入数据输出寄存器DORx中,而是需要将数据输入数据保持寄存器DHRx中,然后等待触发源到来/一个时钟周期后,数据将自动从DHRx中转移到DORx中

由于DHRx寄存器位32位寄存器,而我们写入的数据为8/12位的,因此存在数据对齐的问题
,采用不同的对齐方式需要将数据写入对应对齐方式的数据保存寄存器中,如下图所示为DAC单/双通道模式下的数据对齐模式,每种模式对应1/2个寄存器(注释1)

举个例子:

本实验采取DAC1单通道模式12位右对齐,因此笔者需要将数据写入 DAC_DHR12R1 寄存器中,而该寄存器的偏址从手册上可以看到为0x80

我们写入DAC寄存器数据时使用的函数为HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue),其中DAC_ALIGN_12B_R值为0,因此最后将DacValue值写入了地址为DAC基址+0x00000008UL偏址的 DAC_DHR12R1 寄存器,上述描述如下图所示

DAC输出引脚输出的电压值由DACoutput = VREF+ * DOR / 4095公式计算,通常 VREF+直接与VDDA短接,因此DAC通道输出的电压范围为0-3.3V
,如下图所示为DAC通道框图
(注释1)

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

本实验需要需要初始化开发板上KEY2和KEY0用户按键,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

本实验需要配置TIM3 100ms更新事件作为ADC1_IN5通道采集触发源的ADC采集,因此需要初始化TIM3和ADC1_IN5,具体配置步骤请阅读“
STM32CubeMX教程13 ADC - 单通道转换
”,如下图所示为配置简图

接下来配置DAC OUT1,在Pinout & Configuration页面左边功能分类栏目
Analog中单击其中DAC,在Mode中勾选OUT1 Configuration

在DAC Out1 Settings中使能
Output Buffer

Trigger
选择默认None
,这里不需要触发源,也就是说当DAC启动后DAC就会一直输出下去而不是在每次一触发源来到的时候才输出,具体配置如下图所示

3.1.3、外设中断配置

DAC只有两个关于DMA的下溢事件的中断源,本实验尚不设计DMA,因此这里无需开启DAC的任何中断

但是DAC的输出电压需要由ADC1_IN5来采集,因此这里勾选ADC的全局中断,并设置合适的中断优先级,具体配置如下图所示

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

在生成的工程代码主函数中增加了MX_DAC_Init()函数,该函数对启用的DAC触发方式、输出缓存进行了配置

然后调用HAL_DAC_Init()函数对DAC进行了初始化,并调用了HAL_DAC_MspInit()函数

在HAL_DAC_MspInit()函数中对DAC OUT1的输出引脚PA4做了引脚复用配置,并且使能了DAC的时钟,如果配置了中断,在该函数中还会出现中断优先级及中断使能相关代码

上述DAC初始化调用流程如下图所示

3.2.2、外设中断调用流程

本实验只开启了ADC的全局中断,ADC全局中断调用流程请阅读“
STM32CubeMX教程13 ADC - 单通道转换
”4.2.2小节

3.2.3、添加其他必要代码

在adc.c中重新实现ADC采集完毕中断回调函数HAL_ADC_ConvCpltCallback()
,与”
STM32CubeMX教程13 ADC - 单通道转换
”实验内容一致,目的就是获取ADC1_IN5通道采集值并通过USART1输出,具体代码如下所示

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if(hadc->Instance == ADC1)
    {
        uint32_t val=HAL_ADC_GetValue(&hadc1);
        uint32_t Volt=(3300*val)>>12;
        printf("val:%d, Volt:%d\r\n",val,Volt);
    }
}

在主函数中启动DAC输出,并设置默认的DAC输出值,然后启动定时器和ADC采集,并在主循环中实现按下按键KEY2将DAC输出值增加500,按下按键KEY0将DAC输出值减少500,具体代码如下图所示

源代码如下所示

/*主循环外代码*/
printf("Reset\r\n");
HAL_DAC_Start(&hdac,DAC_CHANNEL_1);
uint32_t DacValue=1000;
HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue);
HAL_ADC_Start_IT(&hadc1);
HAL_TIM_Base_Start(&htim3);


/*主循环中代码*/
/*按键KEY2被按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
    {
        printf("---now DacValue is: %d---\r\n", DacValue);
        DacValue += 500;
        if(DacValue>4095) DacValue=4095;
        HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue);
        while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
    }
}


/*按键KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
    {
        printf("---now DacValue is: %d---\r\n", DacValue);
        DacValue -= 500;
        HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue);
        while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
    }
}

值得提醒的是本实验使用DAC1_IN5(PA5)采集DAC OUT1(PA4)输出值,需要使用跳线帽/杜邦线将PA5和PA4两个引脚短接

4、常用函数

/*DAC软件启动输出函数*/
HAL_StatusTypeDef HAL_DAC_Start(DAC_HandleTypeDef *hdac, uint32_t Channel)

/*DAC软件停止输出函数*/
HAL_StatusTypeDef HAL_DAC_Stop(DAC_HandleTypeDef *hdac, uint32_t Channel)

/*DAC输出值设置*/
HAL_StatusTypeDef HAL_DAC_SetValue(DAC_HandleTypeDef *hdac, uint32_t Channel, uint32_t Alignment, uint32_t Data)

5、烧录验证

烧录程序,开发板上电后打开串口助手,可以看到间隔100ms输出一次采集到的DAC OU1输出值,默认输出值为1000,按下KEY2按键后输出值增加500,按下KEY0按键后输出值减少500,可以从串口输出信息看到采集到的值和我们预想的效果一致,如下图所示为整个过程串口输出信息

6、注释详解

注释1
:图片来源STM32F4xx 中文参考手册

更多内容请浏览
STM32CubeMX+STM32F4系列教程文章汇总贴